diff --git a/.gitignore b/.gitignore index ab11f8441c1690ede967897c0e64745f9ad30f86..c30895c42d4a4bae9f1bea8750f63899102b2fa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# -*- mode:conf; -*- + # configuration files /conf/ext/* !/conf/core/*.template @@ -11,8 +13,11 @@ *.jks # typical build dirs +build/ bin/ target/ +_apidoc/ + # But include server-side scripting !/scripting/bin diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c1afaf3f2a5274ff9e9a3083dbfd788e629709c0..0852bbaa5e63ed6ceae1fc8ddad79db80d9fc16a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,7 +24,7 @@ variables: DEPLOY_REF: dev - CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-server/caosdb-server-testenv:latest + CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/caosdb-server/caosdb-server-testenv:latest image: $CI_REGISTRY_IMAGE stages: @@ -37,16 +37,15 @@ build-testenv: tags: [ cached-dind ] image: docker:19.03 stage: setup + timeout: 3h only: - schedules script: - cd src/test/docker - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY # use here general latest or specific branch latest... - - docker pull $CI_REGISTRY_IMAGE || true - docker build --pull - --cache-from $CI_REGISTRY_IMAGE -t $CI_REGISTRY_IMAGE . - docker push $CI_REGISTRY_IMAGE @@ -68,9 +67,29 @@ trigger_build: stage: deploy script: - /usr/bin/curl -X POST - -F token=$DEPLOY_TRIGGER_TOKEN + -F token=$CI_JOB_TOKEN -F "variables[F_BRANCH]=$CI_COMMIT_REF_NAME" -F "variables[SERVER]=$CI_COMMIT_REF_NAME" -F "variables[TriggerdBy]=SERVER" -F "variables[TriggerdByHash]=$CI_COMMIT_SHORT_SHA" -F ref=$DEPLOY_REF https://gitlab.indiscale.com/api/v4/projects/14/trigger/pipeline + +# Build the sphinx documentation and make it ready for deployment by Gitlab Pages +# Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages +pages: + tags: [ cached-dind ] + stage: deploy + only: + refs: + - /^release-.*$/i + - master + variables: + # run pages only on gitlab.com + - $CI_SERVER_HOST == "gitlab.com" + script: + - echo "Deploying" + - make doc + - cp -r build/doc/html public + artifacts: + paths: + - public diff --git a/CHANGELOG.md b/CHANGELOG.md index fccd7c8ff5d78126d80692728d7941be1d142e29..305dd69b00b84c30b5caa91dec9f97b6beaa4b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* `ETag` property for the query. The `ETag` is assigned to the query cache + each time the cache is cleared (currently whenever the server state is being + updated, i.e. the stored entities change). + This can be used to debug the query cache and also allows a client + to determine whether the server's state has changed between queries. +* Basic caching for queries. The caching is enabled by default and can be + controlled by the usual "cache" flag. + +### Changed + +### Deprecated + +### Removed + +### Fixed + +* #122 - Dead-lock due to error in the DatabaseAccessManager. +* #120 - Editing entities that were created with a no longer existing user + leads to a server error. +* #31 - Queries with keywords in the path (e.g. `... STORED AT 0in.txt`) +* #116 - Queries `FIND [ANY VERSION OF] *` and `FIND [ANY VERSION OF] ENTITY`. + +### Security + +## [0.3.0] - 2021-02-10 + +### Added + +* New version history feature. The "H" container flag retrieves the full + version history during a transaction (e.g. during Retrievals) and constructs + a tree of successors and predecessors of the requested entity version. +* New query functionality: `ANY VERSION OF` modifier. E.g. `FIND ANY VERSION OF + RECORD WITH pname=val` returns all current and old versions of records where + `pname=val`. For further information, examples and limitations see the wiki + page on [CQL](https://gitlab.com/caosdb/caosdb/-/wikis/manuals/CQL/CaosDB%20Query%20Language) +* New server property `SERVER_SIDE_SCRIPTING_BIN_DIRS` which accepts a comma or + space separated list as values. The server looks for scripts in all + directories in the order or the list and uses the first matching file. +* Automated documentation builds: `make doc` + ### Changed +* Select queries would originally only select the returned properties by their + names and would not check if a property is a subtype of a selected property. This + has changed now and select queries will also return subtypes of selected + properties. + ### Deprecated +* `SERVER_SIDE_SCRIPTING_BIN_DIR` property is deprecated. + `SERVER_SIDE_SCRIPTING_BIN_DIRS` should be used instead (note the plural + form!) + ### Removed +* Text user interface (CaosDBTerminal). + ### Fixed +* Bug: When the user password is updated the user is deactivated. +* Semi-fixed a bug which occurs when retrieving old versions of entities which + reference entities which have been deleted in the mean time. The current fix + adds a warning message to the reference property in question and sets the + value to NULL. This might even be desired behavior, however this would have + to finally specified during the Delete/Forget phase of the implementation of + the versioning. +- Inheritance job cannot handle inheritance from same container (!54) +* Bug in the query parser (MR!56) - The parser would throw an error when the + query contains a conjunction or disjunction filter with a first element which + is another disjunction or conjunction and being wrapped into parenthesis. + ### Security -## [0.2] - 2020-09-02 +## [0.2.0] - 2020-09-02 ### Added @@ -50,7 +113,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 package naming conventions while the old was not. This has some implications for configuring the server. See [README_SETUP.md](./README_SETUP.md), section "Migration" for additional information. -- The sever by default now only serves TLS 1.2 and 1.3, all previous versions +- The server by default now only serves TLS 1.2 and 1.3, all previous versions have been disabled in the default settings. Make sure that your clients (especially the Python client) are up to date. @@ -62,6 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Missing handling of list of reference properties in SELECT queries. * #51 - name queries (e.g. `FIND ENTITY WITH name = ...`) - #27 - star matches slashes (e.g. for `FIND ... STORED AT /*.dat`). - #30 - file path cannot be in quotes diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000000000000000000000000000000000000..3744c7b7d654c5f2379b396c5894f8c054907ee4 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,4 @@ +* caosdb-mysqlbackend == 4.0.0 +* Java 11 +* Apache Maven >= 3.6.0 +* make >= 4.2.0 diff --git a/makefile b/Makefile similarity index 98% rename from makefile rename to Makefile index 5c0e6cdda534c34f9f34109a008feac1d79312dc..d73f140f065ca0b3db62651e40162194f4ffb4eb 100644 --- a/makefile +++ b/Makefile @@ -128,3 +128,8 @@ stop-debug-screen: easy-units: .m2-local mvn clean mvn deploy:deploy-file -DgroupId=de.timmfitschen -DartifactId=easy-units -Dversion=0.0.1-SNAPSHOT -Durl=file:./.m2-local/ -DrepositoryId=local-maven-repo -DupdateReleaseInfo=true -Dfile=./lib/easy-units-0.0.1-SNAPSHOT-jar-with-dependencies.jar + +# Compile the standalone documentation +.PHONY: doc +doc: + $(MAKE) -C src/doc html diff --git a/README.md b/README.md index 8b68da0a79f9d66901ddb0176415c3ae4f2ca465..21d5400e5383d4c2571f8a409f50cd72f926187a 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,47 @@ -<!--THIS FILE HAS BEEN GENERATED BY A SCRIPT. PLEASE DON'T CHANGE IT MANUALLY.--> -# Welcome +# README -This is the **CaosDB Server** repository and a part of the CaosDB project. +## Welcome -# Setup +This is the **CaosDB Java Server** repository and a part of the +CaosDB project. + +## Setup Please read the [README_SETUP.md](README_SETUP.md) for instructions on how to setup this code. -# Further Reading +## Further Reading + +Please refer to the [official documentation](https://docs.indiscale.com/caosdb-server/) for more information. + +## Contributing + +Thank you very much to all contributers—[past, present](https://gitlab.com/caosdb/caosdb/-/blob/dev/HUMANS.md), and prospective ones. -Please refer to the [official gitlab repository of the CaosDB -project](https://gitlab.com/caosdb/caosdb) for more information. +### Code of Conduct -# License +By participating, you are expected to uphold our [Code of Conduct](https://gitlab.com/caosdb/caosdb/-/blob/dev/CODE_OF_CONDUCT.md). -Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute for -Dynamics and Self-Organization Göttingen. +### How to Contribute + +* You found a bug, have a question, or want to request a feature? Please +[create an issue](https://gitlab.com/caosdb/caosdb-server/-/issues). +* You want to contribute code? Please fork the repository and create a merge +request in GitLab and choose this repository as target. Make sure to select +"Allow commits from members who can merge the target branch" under Contribution +when creating the merge request. This allows our team to work with you on your request. +- If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-server/), +the preferred way is also a merge request as describe above (the documentation resides in `src/doc`). +However, you can also create an issue for it. +- You can also contact us at **info (AT) caosdb.de**. + +## License + +* Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute + for Dynamics and Self-Organization Göttingen. +* Copyright (C) 2020-2021 Indiscale GmbH <info@indiscale.com> All files in this repository are licensed under a [GNU Affero General Public License](LICENCE.md) (version 3 or later). - diff --git a/README_SETUP.md b/README_SETUP.md index 2d682c34f4065f15082507fc7c80b839a486dcc9..f47f5a08624c20de194829b3534d72c2e06b461c 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -1,25 +1,28 @@ -# Requirements +# Getting Started with the CaosDB Server +Here, you find information on requirements, the installation, configuration and more. -## CaosDB Packages +## Requirements + +### CaosDB Packages * caosdb-webui=0.2.1 * caosdb-mysqlbackend=3.0 -## Third-party Software -* >=Java 8 -* >=Apache Maven 3.0.4 -* >=Python 3.4 -* >=pip 9.0.1 -* >=git 1.9.1 -* >=Make 3.81 -* >=Screen 4.01 -* >=MySQL 5.5 (better >=5.6) or >=MariaDB 10.1 -* libpam (if PAM authentication is required) -* unzip -* openpyxl (for XLS/ODS export) -* openssl (if a custom TLS certificate is required) - -### Install the requirements on Debian +### Third-party Software +* `>=Java 8` +* `>=Apache Maven 3.0.4` +* `>=Python 3.4` +* `>=pip 9.0.1` +* `>=git 1.9.1` +* `>=Make 3.81` +* `>=Screen 4.01` +* `>=MySQL 5.5` (better `>=5.6`) or `>=MariaDB 10.1` +* `libpam` (if PAM authentication is required) +* `unzip` +* `openpyxl` (for XLS/ODS export) +* `openssl` (if a custom TLS certificate is required) + +#### Install the requirements on Debian On Debian, the required packages can be installed with: apt-get install git make mariadb-server maven openjdk-11-jdk-headless \ @@ -28,21 +31,21 @@ On Debian, the required packages can be installed with: Note that installing MariaDB will uninstall existing MySQL packages and vice versa. -## System +### System -* >=Linux 4.0.0, x86\_64, e.g. Ubuntu 14.04.1 +* `>=Linux 4.0.0`, `x86_64`, e.g. Ubuntu 18.04 * Mounted filesytem(s) with enough space * Working internet connection (for up-to-date python and java libraries) -## Extensions ## +### Extensions ## -### Web UI ### +#### Web UI ### - If the WebUI shall run, check out the respective submodule: `git submodule update --init caosdb-webui` - Then configure and compile it according to its - [documentation](caosdb-webui/README_SETUP.md). + [documentation](https://http://caosdb.gitlab.io/caosdb-webui/getting_started.html). -### PAM ### +#### PAM ### Authentication via PAM is possible, for this the PAM development library must be installed and the pam user tool must be compiled: @@ -52,14 +55,14 @@ installed and the pam user tool must be compiled: should print `[FAILED]` and return with a non-zero exit code. Unless there is a user `asdf` with password `ghjk` on your system, of course. -#### Troubleshooting #### +##### Troubleshooting #### If `make` fails with `pam_authentication.c:4:31: fatal error: security/pam_appl.h: No such file or directory` the header files are probably not installed. You can do so under Debian and Ubuntu with `apt-get install libpam0g-dev`. Then try again. -# First Setup +## First Setup After a fresh clone of the repository, this is what you need to setup the server: @@ -68,42 +71,56 @@ server: needs to be an internet connection as packages are downloaded to be integrated in the java file. 1. It is recommended to run the unit tests with `make test`. It may take a - while. + while. 2. Create an SSL certificate somewhere with a `Java Key Store` file. For self-signed certificates (not recommended for production use) you can do: - `mkdir certificates; cd certificates` - `keytool -genkey -keyalg RSA -alias selfsigned -keystore caosdb.jks -validity 375 -keysize 2048 -ext san=dns:localhost` Replace `localhost` by your host name, if you want. - `keytool -importkeystore -srckeystore caosdb.jks -destkeystore caosdb.p12 -deststoretype PKCS12 -srcalias selfsigned` - - `openssl pkcs12 -in caosdb.p12 -nokeys -out cert.pem` + - Export the public part only: `openssl pkcs12 -in caosdb.p12 -nokeys -out cert.pem`. + The resulting ``cert.pem` can safely be given to users to allow ssl verification. - You can check the content of the certificate with `openssl x509 -in cert.pem -text` Alternatively, you can create a keystore from certificate files that you already have: - `openssl pkcs12 -export -inkey privkey.pem -in fullchain.pem -out all-certs.pkcs12` - `keytool -importkeystore -srckeystore all-certs.pkcs12 -srcstoretype PKCS12 -deststoretype pkcs12 -destkeystore caosdb.jks` - -3. Copy `conf/core/server.conf` to `conf/ext/server.conf` and change it +3. Install/configure the MySQL back-end: see the `README_SETUP.md` of the + `caosdb-mysqlbackend` repository +4. Create an authtoken config (e.g. copy `conf/core/authtoken.example.yaml` to `conf/ext/authtoken.yml` and change it) +5. Copy `conf/core/server.conf` to `conf/ext/server.conf` and change it appropriately: - * Setup for MySQL back-end: Assuming that the mysql back-end is installed - (see the `README_SETUP.md` of the `caosdb-mysqlbackend` repository), + * Setup for MySQL back-end: specify the fields `MYSQL_USER_NAME`, `MYSQL_USER_PASSWORD`, - `MYSQL_DATABASE_NAME`, and `MYSQL_HOST`. + `MYSQL_DATABASE_NAME`, and `MYSQL_HOST`. * Choose the ports under which CaosDB will be accessible. * Setup the SSL certificate: Assuming that there is an appropriate `Java Key Store` file (see above), change the fields `CERTIFICATES_KEY_PASSWORD`, `CERTIFICATES_KEY_STORE_PATH`, and `CERTIFICATES_KEY_STORE_PASSWORD`. Make sure that the conf file is not readable by other users because the certificate passwords are stored in plaintext. + - Set the path to the authtoken config (see step 4) * Set the file system paths: - - `FILE_SYSTEM_ROOT`: The root for all the files managed by CaosDB. - - `DROP_OFF_BOX`: Files can be put here for insertion into CaosDB. - - `TMP_FILES`: Temporary files go here, for example during script execution - or when uploading or moving files. - - `SHARED_FOLDER`: Folder for sharing files via cryptographic tokens, also - those created by scripts. + - `FILE_SYSTEM_ROOT`: The root for all the files managed by CaosDB. + - `DROP_OFF_BOX`: Files can be put here for insertion into CaosDB. + - `TMP_FILES`: Temporary files go here, for example during script + execution or when uploading or moving files. + - `SHARED_FOLDER`: Folder for sharing files via cryptographic tokens, + also those created by scripts. + - `SERVER_SIDE_SCRIPTING_BIN_DIRS`: A comma or white space separated list + of directories (relative or absolute) where the server will be looking + for executables which are then callable as server-side scripts. By + default this list only contains `./scripting/bin`. If you want to + include e.g. scripts which are maintained as part of the caosdb-webui + repository (because they are intended for usage by the webui), you + should add `./caosdb-webui/sss_bin/` as well. + - `INSERT_FILES_IN_DIR_ALLOWED_DIRS`: add mounted filesystems here that + shall be accessible by CaosDB * Maybe set another `SESSION_TIMEOUT_MS`. * See also [README_CONFIGURATION.md](README_CONFIGURATION.md) -4. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`. +6. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`. + * You can skip this if you do not want to use an external authentication. + Local users (CaosDB realm) are always available. * Define the users/groups who you want to include/exclude. * Assign at least one user the `administration` role. * For example, if the admin user is called `caosdb`, there should be the @@ -116,11 +133,12 @@ server: Especially that there are no `properties` (aka `keys`) without a `value`. An emtpy value can be represented by `""`. Comments are everything from `#` or `;` to the end of the line. -5. Install the pam caller in `misc/pam_authentication/`. See - [the pam authentication README](misc/pam_authentication/README.md) +7. Possibly install the PAM caller in `misc/pam_authentication/` if you have + not do so already. See above. + Done! -# Start Server +## Start Server `$ make run` @@ -131,12 +149,12 @@ type `https://localhost:10443` in your Browser, assuming you used 10443 as port. Note, that you will get a security warning if you are using a self-signed certificate. -# Run Unit Tests +## Run Unit Tests `$ make test` -# Setup Eclipse +## Setup Eclipse 1. Open Eclipse (recommended version: Oxygen.1a Release (4.7.1a)) 2. `File > New > Java Project`: Choose a project name and specify the location @@ -155,9 +173,9 @@ certificate. Done! -# Migration +## Migration -## From 0.1 to 0.2 +### From 0.1 to 0.2 A major change in the code is the renaming of the java packages (from `caosdb.[...]` to `org.caosdb.[...]`). @@ -172,3 +190,21 @@ before you execute it. ```sh sed -i.bak -e "s/\(\s*\)\([^.]\)caosdb\.server/\1\2org.caosdb.server/g" FILE_TO_BE_CHANGED ``` + +## Build the documentation # + +Stand-alone documentation is built using Sphinx: `make doc` + +### Requirements ## + +- sphinx +- javasphinx :: `pip3 install --user javasphinx` + - Alternative, if javasphinx fails because python3-sphinx is too recent: + (`l_` not found): + +```sh +git clone git@github.com:simgrid/javasphinx.git +cd javasphinx +git checkout 659209069603a +pip3 install . +``` diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index 234ff24f40e9c281bfeb23703864c39369aaea05..5d89ceec00bbd8ba228bb497964f0eeb437891f7 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -18,6 +18,9 @@ guidelines of the CaosDB Project 2. Check all general prerequisites. +3. Update the version property in [pom.xml](./pom.xml) (probably this means to + remove the `-SNAPSHOT`) and in `src/doc/conf.py`. + 4. Merge the release branch into the master branch. 5. Tag the latest commit of the master branch with `v<VERSION>`. @@ -25,3 +28,6 @@ guidelines of the CaosDB Project 6. Delete the release branch. 7. Merge the master branch back into the dev branch. + +8. Update the version property in [pom.xml](./pom.xml) for the next + developlement round (with a `-SNAPSHOT` suffix). diff --git a/caosdb-webui b/caosdb-webui index 82315e1199bd5adcf584f61bfde167bd05624172..8c59cc861d646cbdba0ec749ba052656f67fd58d 160000 --- a/caosdb-webui +++ b/caosdb-webui @@ -1 +1 @@ -Subproject commit 82315e1199bd5adcf584f61bfde167bd05624172 +Subproject commit 8c59cc861d646cbdba0ec749ba052656f67fd58d diff --git a/conf/core/cache.ccf b/conf/core/cache.ccf index b4e1f93596a6170ef04ec373dc7a8d70e51fedcc..9cd12694874dfd5e7739f7b23098602155436c8e 100644 --- a/conf/core/cache.ccf +++ b/conf/core/cache.ccf @@ -1,3 +1,8 @@ +# -*- mode:conf-javaprop; -*- + +# Configuration for the Java Caching System (JCS) which is used by the server. Please look at +# http://commons.apache.org/proper/commons-jcs/getting_started/intro.html for further information. + # default caching options jcs.default.cacheattributes=org.apache.commons.jcs.engine.CompositeCacheAttributes jcs.default.cacheattributes.MaxObjects=1000 @@ -28,8 +33,11 @@ jcs.region.BACKEND_JobRules.cacheattributes.MaxObjects=103 jcs.region.BACKEND_SparseEntities jcs.region.BACKEND_SparseEntities.cacheattributes.MaxObjects=1002 -jcs.region.BACKEND_RetrieveFullVersionInfo -jcs.region.BACKEND_RetrieveFullVersionInfo.cacheattributes.MaxObjects=1006 +jcs.region.BACKEND_RetrieveVersionHistory +jcs.region.BACKEND_RetrieveVersionHistory.cacheattributes.MaxObjects=1006 + +jcs.region.HIGH_LEVEL_QUERY_CACHE +jcs.region.HIGH_LEVEL_QUERY_CACHE.cacheattributes.MaxObjects=1000 # PAM UserSource Caching: Cached Items expire after 60 seconds if they are not requested (idle) and after 600 seconds max. # PAM_UnixUserGroups diff --git a/conf/core/global_entity_permissions.xml b/conf/core/global_entity_permissions.xml index 3cebb79bd1c40564c99219519aa92b59a92e9dcd..3b0cf1b5ccb3f50c73f7a0ad8f4a2651c2dad221 100644 --- a/conf/core/global_entity_permissions.xml +++ b/conf/core/global_entity_permissions.xml @@ -1,4 +1,10 @@ <globalPermissions> + <!-- + 4-store for permissions, implemented as a mapping: + {Grant/Deny, priority, role} -> List of permissions + + Please look at the permission documentation for more information. + --> <Grant priority="false" role="?OWNER?"><Permission name="*"/></Grant> <Grant priority="false" role="?OTHER?"><Permission name="RETRIEVE:*"/></Grant> <Grant priority="false" role="?OTHER?"><Permission name="USE:*"/></Grant> diff --git a/conf/core/log4j2-debug.properties b/conf/core/log4j2-debug.properties index 40ffe0fc02c16eb3c80a9216f58b317faecc41dc..3ae3ed266ef865fcffde45c21458650dc5ccc0c1 100644 --- a/conf/core/log4j2-debug.properties +++ b/conf/core/log4j2-debug.properties @@ -1,3 +1,5 @@ +# This log4j2-debug.properties file is only loaded when the server runs in debug mode. + # override location of log files for debugging and testing property.LOG_DIR = testlog diff --git a/conf/core/log4j2-default.properties b/conf/core/log4j2-default.properties index b1697a73fe6703850865406e1441947614d00f47..974ce34df0f8290d763bf6a993554dab0ec7eeb2 100644 --- a/conf/core/log4j2-default.properties +++ b/conf/core/log4j2-default.properties @@ -1,3 +1,6 @@ +# This log4j2-default.properties file describes the logging settings. See +# https://logging.apache.org/log4j/2.x/ for more information. + name = base_configuration status = TRACE verbose = true diff --git a/conf/core/server.conf b/conf/core/server.conf index 73a7bc871c47409f48dd724988818ab210e0ab87..c9151888e785b8114f104fdcd14c43714fc80b37 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -12,9 +12,10 @@ SERVER_NAME=CaosDB Server # The following paths are relative to the working directory of the server. # -------------------------------------------------- -# The location of the server side scripting binaries. +# The location(s) of the server side scripting binaries. # Put your executable python scripts here, if they need to be called from the scripting API. -SERVER_SIDE_SCRIPTING_BIN_DIR=./scripting/bin/ +# The value is a comma or space separated list or a single directory +SERVER_SIDE_SCRIPTING_BIN_DIRS=./scripting/bin/ # Working directory of the server side scripting API. # On execution of binaries and scripts the server will create a corresponding working directory in this folder. @@ -67,14 +68,14 @@ MYSQL_USER_NAME=caosdb # Password for the user MYSQL_USER_PASSWORD=caosdb # Schema of mysql procedures and tables which is required by this CaosDB instance -MYSQL_SCHEMA_VERSION=v3.0.0-rc2 +MYSQL_SCHEMA_VERSION=v4.0.0 # -------------------------------------------------- # Server options # -------------------------------------------------- # The context root is a prefix which allows running multiple instances of CaosDB using the same -# hostname and port. +# hostname and port. Must start with "/". CONTEXT_ROOT= # HTTPS port of this server instance. SERVER_PORT_HTTPS=443 @@ -111,8 +112,8 @@ CERTIFICATES_KEY_STORE_PASSWORD= # -------------------------------------------------- # The session timeout after which the cookie expires. -# 10 min -SESSION_TIMEOUT_MS=600000 +# 60 min +SESSION_TIMEOUT_MS=3600000 # Time after which one-time tokens expire. # This is only a default value. The actual timeout of tokens can be @@ -120,7 +121,7 @@ SESSION_TIMEOUT_MS=600000 # 7days ONE_TIME_TOKEN_EXPIRES_MS=604800000 -# Path to config file for one time tokens, for example authtoken.yml. +# Path to config file for one time tokens, see authtoken.example.yml. AUTHTOKEN_CONFIG= # Timeout after which a one-time token expires once it has been first consumed, @@ -144,7 +145,7 @@ MAIL_TO_FILE_HANDLER_LOC=./ # -------------------------------------------------- # Admin settings -# # -------------------------------------------------- +# -------------------------------------------------- # Name of the administrator of this instance ADMIN_NAME=CaosDB Admin # Email of the administrator of this instance @@ -181,4 +182,9 @@ CHECK_ENTITY_ACL_ROLES_MODE=MUST # part of any Entity ACL. GLOBAL_ENTITY_PERMISSIONS_FILE=./conf/core/global_entity_permissions.xml +# -------------------------------------------------- +# Extensions +# -------------------------------------------------- + +# If set to true, versioning of entities' history is enabled. ENTITY_VERSIONING_ENABLED=true diff --git a/conf/core/usersources.ini.template b/conf/core/usersources.ini.template index 270a4ad069305ef47b7ad46b36bd69673860fb97..2e0fe2490a65b53022d8d0edff6495f3b92ec10a 100644 --- a/conf/core/usersources.ini.template +++ b/conf/core/usersources.ini.template @@ -1,4 +1,5 @@ -# +# -*- mode:conf; -*- + # ** header v3.0 # This file is a part of the CaosDB Project. # @@ -20,17 +21,35 @@ # # ** end header # + +# This file configures external authentication providers. The CaosDB realm is +# always available (without being defined here). + +# `realms` is a comma and/or space separated list of realms which users can +# use for authentication +# Currently available: PAM realms = PAM + +# This is the default realm, to be used when no other realms is specified defaultRealm = PAM +# Each realm has one section with specific options. The options for a specific +# realm can be looked up in that realm's documentation. +# +# Hint: Realms are implemented by classes which are typically in the +# org.caosdb.server.accessControl.Pam package and implement the UserSource interface. + +# Options for authentication against Linux' PAM. [PAM] class = org.caosdb.server.accessControl.Pam +# The script which does the actual checking. ; pam_script = ./misc/pam_authentication/pam_authentication.sh default_status = ACTIVE +# Only users which fulfill these criteria are accepted. ;include.user = [uncomment and put your users here] ;include.group = [uncomment and put your groups here] ;exclude.user = [uncomment and put excluded users here] ;exclude.group = [uncomment and put excluded groups here] -;it is necessary to add at least one admin +# It is typically necessary to add at least one admin ;user.[uncomment a set a username here].roles = administration diff --git a/pom.xml b/pom.xml index 7b229e5178141e8cd90f85328f77cf4e5b3b0531..4c4435934c97633ecadf7bc1ca2d2b6917198bd6 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>org.caosdb</groupId> <artifactId>caosdb-server</artifactId> - <version>0.3-SNAPSHOT</version> + <version>0.4.0-SNAPSHOT</version> <packaging>jar</packaging> <name>CaosDB Server</name> <properties> @@ -117,11 +117,6 @@ <artifactId>reflections</artifactId> <version>0.9.11</version> </dependency> - <dependency> - <groupId>com.googlecode.lanterna</groupId> - <artifactId>lanterna</artifactId> - <version>2.1.9</version> - </dependency> <dependency> <groupId>org.antlr</groupId> <artifactId>antlr4</artifactId> diff --git a/scripting/home/readme.md b/scripting/home/readme.md index 9c9744a55cec41f33929a3953d9c582fa97f46e0..c79afce2a399089c797e0157d30d9908b33dd23e 100644 --- a/scripting/home/readme.md +++ b/scripting/home/readme.md @@ -1,3 +1,3 @@ # The `home` directory # -This directory will be copied to a temporary directy for each server-side +This directory will be copied to a temporary directly for each server-side scripting invocation and set as the `HOME` environment variable. diff --git a/src/doc/CaosDB-Query-Language.md b/src/doc/CaosDB-Query-Language.md new file mode 100644 index 0000000000000000000000000000000000000000..07fbdbc310b8d22de4642f041918710e6e707488 --- /dev/null +++ b/src/doc/CaosDB-Query-Language.md @@ -0,0 +1,404 @@ +# CaosDB Query Language +**WIP This is going to be the specification. CQL tutorials are in the webui** + +## Example queries + +### Simple FIND Query +The following query will return any entity which has the name _ename_ and all its children. +`FIND ename` + +The following queries are equivalent and will return any entity which has the name _ename_ and all its children, but only if they are genuin records. Of course, the returned set of entities (henceforth referred to as _resultset_) can also be restricted to recordtypes, properties and files. + +`FIND RECORD ename` + +`FIND RECORDS ename` + +Wildcards use `*` for any characters or none at all. Wildcards for single characters (like the '_' wildcard from mysql) are not implemented yet. + +`FIND RECORD en*` returns any entity which has a name beginning with _en_. + +Regular expressions must be surrounded by _<<_ and '>>': + +`FIND RECORD <<e[aemn]{2,5}>>` + +`FIND RECORD <<[cC]am_[0-9]*>>` + +*TODO* (Timm): +Describe escape sequences like `\\ `, `\*`, `\<<` and `\>>`. + +Currently, wildcards and regular expressions are only available for the _simple-find-part_ of the query, i. e. no wildcards/regexps for filters. + +### Simple COUNT Query + +This query counts entities which have certain properties. + +`COUNT ename` +will return the number of entities which have the name _ename_ and all their children. + +The syntax of the COUNT queries is equivalent to the FIND queries in any respect (this also applies to wildcards and regular expressions) but one: The prefix is to be `COUNT` instead of `FIND`. + +Unlike the FIND queries, the COUNT queries do not return any entities. The result of the query is the number of entities which _would be_ returned if the query was a FIND query. + +## Filters + +### POV - Property-Operator-Value + +The following queries are equivalent and will restrict the result set to entities which have a property named _pname1_ that has a value _val1_. + +`FIND ename.pname1=val1` + +`FIND ename WITH pname1=val1` + +`FIND ename WHICH HAS A PROPERTY pname1=val1` + +`FIND ename WHICH HAS A pname1=val1` + +Again, the resultset can be restricted to records: + +`FIND RECORD ename WHICH HAS A pname1=val1` + +_currently known operators:_ `=, !=, <=, <, >=, >` (and cf. next paragraphes!) + +#### Special Operator: LIKE + +The _LIKE_ can be used with wildcards. The `*` is a wildcard for any (possibly empty) sequence of characters. Examples: + +`FIND RECORD ename WHICH HAS A pname1 LIKE va*` + +`FIND RECORD ename WHICH HAS A pname1 LIKE va*1` + +`FIND RECORD ename WHICH HAS A pname1 LIKE *al1` + +_Note:_ The _LIKE_ operator is will only produce expectable results with text properties. + +#### Special Case: References + +In general a reference can be addressed just like a POV filter. So + +`FIND ename1.pname1=ename2` + +will also return any entity named _ename1_ which references the entity with name or id _ename2_ via a reference property named _pname1_. However, it will also return any entity with a text property of that name with the string value _ename2_. In order to restrict the result set to reference properties one may make use of special reference operators: + +_reference operators:_ `->, REFERENCES, REFERENCE TO` + + +The query looks like this: + +`FIND ename1 WHICH HAS A pname1 REFERENCE TO ename2` + +`FIND ename1 WHICH HAS A pname1->ename2` + +#### Time Special Case: DateTime + +_DateTime operators:_ `=, !=, <, >, IN, NOT IN` + +##### `d1=d2`: Equivalence relation. +* ''True'' iff d1 and d2 are equal in every respect (same DateTime flavor, same fields are defined/undefined and all defined fields are equal respectively). +* ''False'' iff they have the same DateTime flavor but have different fields defined or fields with differing values. +* ''Undefined'' otherwise. + +Examples: +* `2015-04-03=2015-04-03T00:00:00` is undefined. +* `2015-04-03T00:00:00=2015-04-03T00:00:00.0` is undefined (second precision vs. nanosecond precision). +* `2015-04-03T00:00:00.0=2015-04-03T00:00:00.0` is true. +* `2015-04-03T00:00:00=2015-04-03T00:00:00` is true. +* `2015-04=2015-05` is false. +* `2015-04=2015-04` is true. + +##### `d1!=d2`: Intransitive, symmetric relation. +* ''True'' iff `d1=d2` is false. +* ''False'' iff `d1=d2` is true. +* ''Undefined'' otherwise. + +Examples: +* `2015-04-03!=2015-04-03T00:00:00` is undefined. +* `2015-04-03T00:00:00!=2015-04-03T00:00:00.0` is undefined. +* `2015-04-03T00:00:00.0!=2015-04-03T00:00:00.0` is false. +* `2015-04-03T00:00:00!=2015-04-03T00:00:00` is false. +* `2015-04!=2015-05` is true. +* `2015-04!=2015-04` is false. + +##### `d1>d2`: Transitive, non-symmetric relation. +Semantics depend on the flavors of d1 and d2. If both are... +###### [UTCDateTime](Datatype#datetime) +* ''True'' iff the time of d1 is after the the time of d2 according to [https://en.wikipedia.org/wiki/Coordinated_Universal_Time](UTC) +* ''False'' otherwise. + +###### [SemiCompleteDateTime](Datatype#datetime) +* ''True'' iff `d1.ILB>d2.EUB` is true or `d1.ILB=d2.EUB` is true. +* ''False'' iff `d1.EUB<d2.ILB}} is true or {{{d1.EUB=d2.ILB` is true. +* ''Undefined'' otherwise. + +Examples: +* `2015>2014` is true. +* `2015-04>2014` is true. +* `2015-01-01T20:15.00>2015-01-01T20:14` is true. +* `2015-04>2015` is undefined. +* `2015>2015-04` is undefined. +* `2015-01-01T20:15>2015-01-01T20:15:15` is undefined. +* `2014>2015` is false. +* `2014-04>2015` is false. +* `2014-01-01>2015-01-01T20:15:30` is false. + +##### `d1<d2`: Transitive, non-symmetric relation. +Semantics depend on the flavors of d1 and d2. If both are... +###### [UTCDateTime](Datatype#datetime) +* ''True'' iff the time of d1 is before the the time of d2 according to [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) +* ''False'' otherwise. + +###### [SemiCompleteDateTime](Datatype#datetime) +* ''True'' iff `d1.EUB<d2.ILB` is true or `d1.EUB=d2.ILB` is true. +* ''False'' iff `d1.ILB>d2.EUB}} is true or {{{d1.ILB=d2.EUB` is true. +* ''Undefined'' otherwise. + +Examples: +* `2014<2015` is true. +* `2014-04<2015` is true. +* `2014-01-01<2015-01-01T20:15:30` is true. +* `2015-04<2015` is undefined. +* `2015<2015-04` is undefined. +* `2015-01-01T20:15<2015-01-01T20:15:15` is undefined. +* `2015<2014` is false. +* `2015-04<2014` is false. +* `2015-01-01T20:15.00<2015-01-01T20:14` is false. + +##### `d1 IN d2`: Transitive, non-symmetric relation. +Semantics depend on the flavors of d1 and d2. If both are... +###### [SemiCompleteDateTime](Datatype#datetime) +* ''True'' iff (`d1.ILB>d2.ILB` is true or `d1.ILB=d2.ILB` is true) and (`d1.EUB<d2.EUB` is true or `d1.EUB=d2.EUB` is true). +* ''False'' otherwise. + +Examples: +* `2015-01-01 IN 2015` is true. +* `2015-01-01T20:15:30 IN 2015-01-01` is true. +* `2015-01-01T20:15:30 IN 2015-01-01T20:15:30` is true. +* `2015 IN 2015-01-01` is false. +* `2015-01-01 IN 2015-01-01T20:15:30` is false. + +##### `d1 NOT IN d2`: Non-symmetric relation. +Semantics depend on the flavors of d1 and d2. If both are... +###### [SemiCompleteDateTime](Datatype#datetime) +* ''True'' iff `d1.ILB IN d2.ILB` is false. +* ''False'' otherwise. + +Examples: +* `2015 NOT IN 2015-01-01` is true. +* `2015-01-01 NOT IN 2015-01-01T20:15:30` is true. +* `2015-01-01 NOT IN 2015` is false. +* `2015-01-01T20:15:30 NOT IN 2015-01-01` is false. +* `2015-01-01T20:15:30 NOT IN 2015-01-01T20:15:30` is false. + +##### Note +These semantics follow a three-valued logic with ''true'', ''false'' and ''undefined'' as truth values. Only ''true'' is truth preserving. I.e. only those expressions which evaluate to ''true'' pass the POV filter. `FIND ... WHICH HAS A somedate=2015-01` only returns entities for which `somedate=2015-01` is true. On the other hand, `FIND ... WHICH DOESN'T HAVE A somedate=2015-01` returns entities for which `somedate=2015-01` is false or undefined. Shortly put, `NOT d1=d2` is not equivalent to `d1!=d2`. The latter assertion is stronger. + +#### Omitting the Property or the Value + +One doesn't have to specify the property or the value at all. The following query filters the result set for entities which have any property with a value greater than _val1_. + +`FIND ename WHICH HAS A PROPERTY > val1` + +`FIND ename . > val1` + +`FIND ename.>val1` + + +And for references... + +`FIND ename1 WHICH HAS A REFERENCE TO ename2` + +`FIND ename1 WHICH REFERENCES ename2` + +`FIND ename1 . -> ename2` + +`FIND ename1.->ename2` + + +The following query returns entities which have a _pname1_ property with any value. + +`FIND ename WHICH HAS A PROPERTY pname1` + +`FIND ename WHICH HAS A pname1` + +`FIND ename WITH pname1` + +`FIND ename . pname1` + +`FIND ename.pname1` + +### TransactionFilter + +*Definition* + + sugar:: `HAS BEEN` | `HAVE BEEN` | `HAD BEEN` | `WAS` | `IS` | + + negated_sugar:: `HAS NOT BEEN` | `HASN'T BEEN` | `WAS NOT` | `WASN'T` | `IS NOT` | `ISN'T` | `HAVN'T BEEN` | +`HAVE NOT BEEN` | `HADN'T BEEN` | `HAD NOT BEEN` + + by_clause:: `BY (ME | username | SOMEONE ELSE (BUT ME)? | SOMEONE ELSE BUT username)` + + date:: A date string of the form `YYYY-MM-DD` + + datetime:: A datetime string of the form `YYYY-MM-DD hh:mm:ss` + + time_clause:: `ON ($date|$datetime) ` Here is plenty of room for more syntactic sugar, e.g. a `TODAY` keyword, and more funcionality, e.g. ranges. + +`FIND ename WHICH ($sugar|$negated_sugar)? (NOT)? (CREATED|INSERTED|UPDATED|DELETED) (by_clause time_clause?| time_clause by_clause?)` + +*Examples* + +`FIND ename WHICH HAS BEEN CREATED BY ME ON 2014-12-24` + +`FIND ename WHICH HAS BEEN CREATED BY SOMEONE ELSE ON 2014-12-24` + +`FIND ename WHICH HAS BEEN CREATED BY erwin ON 2014-12-24` + +`FIND ename WHICH HAS BEEN CREATED BY SOMEONE ELSE BUT erwin ON 2014-12-24` + +`FIND ename WHICH HAS BEEN CREATED BY erwin` + +`FIND ename . CREATED BY erwin ON ` + + + +### File Location + +Search for file objects by their location: + +`FIND FILE WHICH IS STORED AT a/certain/path/` + +#### Wildcards + +_STORED AT_ can be used with wildcards similar to unix wildcards. + * `*` matches any characters or none at all, but not the directory separator `/` + * `**` matches any character or none at all. + * A leading `*` is a shortcut for `/**` + * Asterisks directly between two other asterisks are ignored: `***` is the same as `**`. + * Escape character: `\` (E.g. `\\` is a literal backslash. `\*` is a literal star. But `\\*` is a literal backslash followed by a wildcard.) + +Examples: + +Find any files ending with `.acq`: +`FIND FILE WHICH IS STORED AT *.acq` or +`FIND FILE WHICH IS STORED AT **.acq` or +`FIND FILE WHICH IS STORED AT /**.acq` + +Find files stored one directory below `/data/`, ending with `.acq`: +`FIND FILE WHICH IS STORED AT /data/*/*.acq` + +Find files stored in `/data/`, ending with `.acq`: +`FIND FILE WHICH IS STORED AT /data/*.acq` + +Find files stored in a directory at any depth in the tree below `/data/`, ending with `.acq`: +`FIND FILE WHICH IS STORED AT /data/**.acq` + +Find any file in a directory which begins with `2016-02`: +`FIND FILE WHICH IS STORED AT */2016-02*/*` + + +### Back References + +The back reference filters for entities that are referenced by another entity. The following query returns entities of the type _ename1_ which are referenced by _ename2_ entities via the reference property _pname1_. + +* `FIND ename1 WHICH IS REFERENCED BY ename2 AS A pname1` +* `FIND ename1 WITH @ ename2 / pname1` +* `FIND ename1 . @ ename2 / pname1` + +One may omit the property specification: + +* `FIND ename1 WHICH IS REFERENCED BY ename2` +* `FIND ename1 WHICH HAS A PROPERTY @ ename2` +* `FIND ename1 WITH @ ename2` +* `FIND ename1 . @ ename2` + +### Combining Filters with Propositional Logic + +Any result set can be filtered by logically combining POV filters or back reference filters: + +#### Conjunction (AND) + +* `FIND ename1 WHICH HAS A PROPERTY pname1=val1 AND A PROPERTY pname2=val2 AND A PROPERTY...` +* `FIND ename1 WHICH HAS A PROPERTY pname1=val1 AND A pname2=val2 AND ...` +* `FIND ename1 . pname1=val1 & pname2=val2 & ...` + +#### Disjunction (OR) + +* `FIND ename1 WHICH HAS A PROPERTY pname1=val1 OR A PROPERTY pname2=val2 Or A PROPERTY...` +* `FIND ename1 WHICH HAS A PROPERTY pname1=val1 OR A pname2=val2 OR ...` +* `FIND ename1 . pname1=val1 | pname2=val2 | ...` + +#### Negation (NOT) + +* `FIND ename1 WHICH DOES NOT HAVE A PROPERTY pname1=val1` +* `FIND ename1 WHICH DOESN'T HAVE A pname1=val1` +* `FIND ename1 . NOT pname2=val2` +* `FIND ename1 . !pname2=val2` + +#### ... and combinations with parentheses + +* `FIND ename1 WHICH HAS A pname1=val1 AND DOESN'T HAVE A pname2<val2 AND ((WHICH HAS A pname3=val3 AND A pname4=val4) OR DOES NOT HAVE A (pname5=val5 AND pname6=val6))` +* `FIND ename1 . pname1=val1 & !pname2<val2 & ((pname3=val3 & pname4=val4) | !(pname5=val5 & pname6=val6))` +* `FIND ename1.pname1=val1&!pname2<val2&((pname3=val3&pname4=val4)|!(pname5=val5&pname6=val6))` + +### A Few Important Expressions + +* A:: The indistinct article. This is only syntactic suger. Equivalent expressions: `A, AN` +* AND:: The logical _and_. Equivalent expressions: `AND, &` +* FIND:: The beginning of the query. +* NOT:: The logical negation. Equivalent expressions: `NOT, DOESN'T HAVE A PROPERTY, DOES NOT HAVE A PROPERTY, DOESN'T HAVE A, DOES NOT HAVE A, DOES NOT, DOESN'T, IS NOT, ISN'T, !` +* OR:: The logical _or_. Equivalent expressions: `OR, |` +* RECORD,RECORDTYPE,FILE,PROPERTY:: Role expression for restricting the result set to a specific role. +* WHICH:: The marker for the beginning of the filters. Equivalent expressions: `WHICH, WHICH HAS A, WHICH HAS A PROPERTY, WHERE, WITH, .` +* REFERENCE:: This one is tricky: `REFERENCE TO` expresses a the state of _having_ a reference property. `REFERENCED BY` expresses the state of _being_ referenced by another entity. +* COUNT:: `COUNT` works like `FIND` but doesn't return the entities. + + +## Select Queries + +In contrast to `FIND` queries, which always return the complete entity, there are `SELECT` queries which only return the entity with only those properties which are specified in the query. The syntax is very similar to `FIND` queries - just replace the `FIND` by `SELECT <comma separated list of selectors> FROM`: + +`SELECT p1, p2, p3 FROM Record ename` + +However, the `SELECT` query can also return properties of referenced entities and thereby are a means of joining entities together and return a custom view or projection: + +`SELECT Conductor.Last Name FROM Experiment` + +would return the conductor's last name, when `Conductor` is a reference property of `Experiment` and `Last Name` is a property of the `Conductor` records. + +### Selectors + +Selectors are strings of entity names which are separated by `.` (dot). E.g. `Conductor.Last Name` or `Conductor.Address` or even `Experiment.Conductor.Last name`. Selectors in a `SELECT` queries are separated by `,` (comma). E.g. `Conductor.First Name, Conductor.Last Name`. + +### Evaluation of Selectors + +The query will return all those properties which have the same name as +specified by the selector (case-insensitive). However, `SELECT` queries are +also capable of subtyping in the selectors: + +`SELECT Person FROM Experiment` would return all `Person` properties but all `Conductors` as well, if `Conductor` is a child of `Person`. + +Note: When a property `responsible` with data type `Person` exists, the above `SELECT` statement would not include records that use this property (since `responsible` is not a child of Person). + +## Versioning + +Since Caosdb 0.2 entities are optionally version controlled. The query language will be extended to include versioning in the future. A current minimal implementation introduces the `ANY VERSION OF` modifier which can be used to return all matching versions in the results of `COUNT`, `FIND`, and `SELECT` queries. + +### Example + +* `FIND ANY VERSION OF RECORD WITH pname=value` returns the all past and present versions of records with `pname = value`. + +### Scope and current limitations + +* The `ANY VERSION OF` modifier currently the only expression for taking the versioning into account when using the query language. +* Subproperties are not supported yet, e.g. `FIND ANY VERSION OF ENTITY WHICH IS REFERENCED BY ename WITH ...`. This applies to all cases where you specify properties of *referenced* entities or *referencing* entities. + +### Future + +* Add `(LATEST|LAST|OLDEST|NEWEST|FIRST) VERSION OF` modifiers. +* Add `(ANY|LATEST|LAST|OLDEST|NEWEST|FIRST) VERSION (BEFORE|AFTER) (<timestamp>|<transaction id>|<entity@version>) OF` modifier. +* Add support for subproperties, e.g. `FIND ANY VERSION OF ENTITY WHICH IS REFERENCED BY ename WITH ...`. + +## Future + + * *Sub Queries* (or *Sub Properties*): `FIND ename WHICH HAS A pname WHICH HAS A subpname=val`. This is like: `FIND AN experiment WHICH HAS A camera WHICH HAS A 'serial number'= 1234567890` + * *More Logic*, especially `ANY`, `ALL`, `NONE`, and `SUCH THAT` key words (and equivalents) for logical quantisation: `FIND ename1 SUCH THAT ALL ename2 WHICH HAVE A REFERENCE TO ename1 HAVE A pname=val`. This is like `FIND experiment SUCH THAT ALL person WHICH ARE REFERENCED BY THIS experiment AS conductor HAVE AN 'academic title'=professor.` + diff --git a/src/doc/Data-Model.md b/src/doc/Data-Model.md new file mode 100644 index 0000000000000000000000000000000000000000..3fa64ec2fdab693552b742572d05b1e3be88d11b --- /dev/null +++ b/src/doc/Data-Model.md @@ -0,0 +1,40 @@ + +# CaosDB Data Model + +The data structure is build from some basic building blocks which are shown in the following picture: + + + +It has a base object called **Entity**. Entities are either **Record Types**, **Records**, or **Abstract +Properties** (and Record like objects for files). +What *kind of data* is stored is defined by the Record Types. Actual data is then stored in CaosDB as Records which are of some Record Type. Those Records can have Properties that contain information about the Record. The following is a more detailed explanation (also see this [paper](https://www.mdpi.com/2306-5729/4/2/83)). + +> Record Types and Abstract Properties are used to define the ontology for a particular +> domain in which the RDMS is used. Records are used to store the actual data and therefore +> represent individuals or particular things, e.g. a particular experiment, a particular time series, etc. + +**Record Types** define classes or types of things, e.g. persons, experiments, timeseries, etc. Records +can be viewed as members of the class defined by its Record Type. These classes can contain +Abstract Properties which define key-value relationships for properties of the things along +with the expected data type and possibly the default unit, a default value, or a range of permitted +values. As files on the back-end file system are a major focus of this database management system, +there is a special entity File that encapsulates typical file properties like path, size and checksum. + +**Entities** can be related via binary, directed, transitive is-a relations which model both subtyping +and instantiation, depending on the relata. These relations construct a directed graph of the +Entities. If A is-a B we call A the child of B and B the parent of A. No adamant restrictions are +imposed on the relate of the is-a relation and thus, Entities can be children of multiple Entities. + +Each Entity has a list of Entity Properties, or in short just **Properties**. An Entity Property is not an Entity of its own, but a triple of an Abstract Property, a value or Null, and +an Importance. The values can be numericals, strings, dates, any other valid value that fits into +one of several builtin data types, or, most notably, references to other Entities. The importance +is either obligatory, recommended, suggested, or fix. A valid child of an Entity implicitly +inherits its parent’s Properties according to their Importance, which means that it is obliged, +recommended or only suggested to have a Property with the same Abstract Property (or +any subtype thereof). + +As opposed to Properties with other priorities, **Fixed Properties** have no effect on the Entity’s children. During the creation or update of Entities, the importances of the parents are +being checked by the Server. Missing obligatory Properties invalidate the transaction and +result in an error, by default. Missing Properties, when they are recommended, result in a +warning, but the transaction is considered valid. Entities with missing suggested Properties +are silently accepted as valid. diff --git a/src/doc/Glossary.md b/src/doc/Glossary.md new file mode 100644 index 0000000000000000000000000000000000000000..080c2aea4ff393d349553f7d7211d35518ff7c11 --- /dev/null +++ b/src/doc/Glossary.md @@ -0,0 +1,17 @@ +# Glossary + +## Valid Unique Existing Name + +A name of an exiting entity which is unique among the names of all existing entities. + +## Valid Unique Prospective Name + +A name of a to-be-inserted/updated entity _e_ which is unique among the names of all other entities that are to be inserted or updated along with the entity _e_. + +## Valid ID + +The ID of an existing entity. It is by definition unique among the IDs of all existing entities and is a positive integer." + +## Valid Unique Temporary ID + +The negative integer ID of a to-be-inserted entity _e_ which is unique among the ids of all other entities that are to be inserted along with the entity _e_. diff --git a/src/doc/Makefile b/src/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4f177fd3850423c3005829a75a2b37625dc68a87 --- /dev/null +++ b/src/doc/Makefile @@ -0,0 +1,47 @@ +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Daniel Hornung <d.hornung@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header + +# This Makefile is a wrapper for sphinx scripts. +# +# It is based upon the autocreated makefile for Sphinx documentation. + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -a +SPHINXBUILD ?= sphinx-build +SPHINXAPIDOC ?= javasphinx-apidoc +SOURCEDIR = . +BUILDDIR = ../../build/doc + +.PHONY: doc-help Makefile apidoc + +# Put it first so that "make" without argument is like "make help". +doc-help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile apidoc + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# sphinx-build -M html . ../../build/doc + +apidoc: + @$(SPHINXAPIDOC) -o _apidoc --update --title="CaosDB Server" ../main/ diff --git a/src/doc/Permissions.rst b/src/doc/Permissions.rst new file mode 100644 index 0000000000000000000000000000000000000000..c624092aff4f479bc8ed48f439109530cc5295aa --- /dev/null +++ b/src/doc/Permissions.rst @@ -0,0 +1,96 @@ +#Permissions + +CaosDB has a fine grained role based permission system. Each interaction +with the server is governed by the current rules of the user, by default +this is the ``anonymous`` role. The permissions for an action which +involves one or more objects are set either manually or via default +permissions which can be configured. For more detailed information, +there is separate +:doc:``documentation of the permission system<permissions>``. + +Permissions are needed to perform particular elementary *actions* during +any interaction with the the server. E.g. retrieving an Entity requires +the requesting user to have the ``RETRIEVE:ENTITY`` permission for that +entity, and ``DELETE:ENTITY`` to delete the entity. + +The permissions of every user are calculated from a set of *Permission +Rules*. These rules have several sources. Some are global defaults, some +are defined by administrators or the owners of an entity. + +As a side note on the implementation: The server uses `Apache +Shiro <https://shiro.apache.org/documentation.html>`__ for its +permission system. + +What is a Permission Rule? +-------------------------- + +A Permission Rule consists of: + +- A type: Permission Rules can be of ``Grant`` or ``Deny`` type, either + granting or denying specific permissions. +- A `role <manuals/general/roles>`__ (or user): For which users the + permission shall be granted or denied. +- A permission action: Which action shall be permitted or forbidden, + for example all retrieval, or modifications of a specific entity. +- An optional priority: May be ``true`` or ``false``. Permissions with + priority = ``true`` override those without, see the calculation rules + below. + +Permission calculation +---------------------- + +For each action a user tries to perform, the server tests, in the +following order, which rules apply: + +1. *Grant* rules, without priority. +2. *Deny* rules, without priority. +3. *Grant* rules, with priority. +4. *Deny* rules, with priority. + +If at the end the user’s permission is *granted*, the action may take +place. Otherwise (the result is *denied* or the permission still +undefined), the action can not take place. In other words, if you have +not been given the permission explicitly at some point, you don’t have +it. + +Possible actions +---------------- + +Until it is completely added to this documentation, a detailed +description of the actions governed by these permissions can be found +`in the +sources <https://gitlab.com/caosdb/caosdb-server/blob/dev/src/main/java/caosdb/server/permissions/EntityPermission.java#L119>`__. + +Typical permissions are: + +- ``RETRIEVE:ENTITY`` :: To retrieve the full entity (name, + description, data type, …) with all parents and properties (unless + prohibited by another rule on the property level). +- ``RETRIEVE:ACL`` :: To retrieve the full and final ACL of this + entity. +- ``RETRIEVE:ENTITY:1234`` :: To retrieve the entity ``1234``. +- ``RETRIEVE:*:1234`` :: For all “retrieve” actions concerning the + entity ``1234``. + +How to set permissions +---------------------- + +- Config file :: Some default permissions are typically set in the + ``global_entity_permissions.xml`` file, see also the `documentation + for that + file </manuals/server/conf/global_entity_permissions.xml>`__. +- API :: The REST API allows to set the permissions. (*to be documented + here*) +- The Python library :: The permissions can also conveniently be set + via the Python library. Currently the best documentation is inside + various files which use the permission API: + + - The `example + file <https://gitlab.com/caosdb/caosdb-pylib/blob/HEAD/examples/set_permissions.py>`__ + - The ```caosdb_admin.py`` utility + script <https://gitlab.com/caosdb/caosdb-pylib/blob/HEAD/src/caosdb/utils/caosdb_admin.py>`__ + - The `integration + tests <https://gitlab.com/caosdb/caosdb-pyinttest/blob/HEAD/tests/test_permissions.py>`__ + also cover quite a bit of the permission API. + +- WebUI :: Not implemented (or documented?) yet. diff --git a/src/doc/README_SETUP.md b/src/doc/README_SETUP.md new file mode 120000 index 0000000000000000000000000000000000000000..88332e357f5e06f3de522768ccdcd9e513c15f62 --- /dev/null +++ b/src/doc/README_SETUP.md @@ -0,0 +1 @@ +../../README_SETUP.md \ No newline at end of file diff --git a/src/doc/administration.rst b/src/doc/administration.rst new file mode 100644 index 0000000000000000000000000000000000000000..819ce5a59fcfbeccc5e145cf773d807197a42d0f --- /dev/null +++ b/src/doc/administration.rst @@ -0,0 +1,10 @@ +Administration +============== + +.. toctree:: + :maxdepth: 1 + :glob: + + administration/* + + diff --git a/src/doc/administration/configuration.rst b/src/doc/administration/configuration.rst new file mode 100644 index 0000000000000000000000000000000000000000..bbafcf22bd8576e141cb9d7e8388212ccf24d934 --- /dev/null +++ b/src/doc/administration/configuration.rst @@ -0,0 +1,63 @@ +Configuration +============= + +The server is configured through configuration files. There are two directories with config files: + +``conf/core`` + Upstream defaults are stored here. +``conf/ext`` + User specific configuration should be stored here, settings in ``ext`` override settings in + ``core``. Additionally, configuration files may be stored in ``*.d`` directories here, named + after the original config file name. For example, the general server configuration will be + assembled from ``conf/core/server.conf``, ``conf/ext/server.conf`` and any ``*.conf`` files found + in ``conf/ext/server.conf.d``. + +Configuration files +------------------- + +In each of these directories, the server looks for the following files: + +``server.conf`` + General server configuration options. The possible configuration options are documented inside + the `default file <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/conf/core/server.conf>`_. + +``global_entity_permissions.xml`` + :ref:`Permissions <concepts:Permissions>` which are automatically set, based on user roles. + See the `default file <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/conf/core/global_entity_permissions.xml>`_. + +``usersources.ini`` + This file defines possible sources which are checked when a user tries to authenticate. Each + defined source has a special section, the possible options are defined separately for each user + source. At the moment the best place to look for this specific documentation is at the API + documentation of :java:type:`UserSource` and its implementing classes. The provided `template + file <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/conf/core/usersources.ini.template>`_ + also has some information. The general concept about authentication realms is described in + :java:type:`UserSources`. + +``authtoken.yaml`` + Configuration for dispensed authentication tokens, which can be used to authenticate to CaosDB + without the need of a user/password combination. Possible use cases are server-side scripts or + initial setup after the server start. There is more documentation inside the `template file + <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/conf/core/authtoken.example.yaml>`_. + +``cache.ccf`` + Configuration for the Java Caching System (JCS) which can be used by the server. More + documentation is `upstream + <http://commons.apache.org/proper/commons-jcs/getting_started/intro.html>`_ and inside `the file + <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/conf/core/cache.ccf>`_. + +``log4j2-default.properties``, ``log4j2-debug.properties`` + Configuration for logging, following the standard described by the `log4j library + <https://logging.apache.org/log4j/2.x/>`_. The ``default`` file is always loaded, in debug mode + the ``debug`` file iss added as well. + + +Changing the configuration at runtime +------------------------------------- + +Remark: + Only when the server is in debug mode, the general configuration can be changed at runtime. + +In the debug case, the server provides the ``_server_properties`` resource which allows the ``GET`` +and ``POST`` methods to access the server's properties. The Python client library conveniently +wraps this in the :any:`caosdb-pylib:caosdb.common.administration` module. diff --git a/src/doc/administration/curl-access.rst b/src/doc/administration/curl-access.rst new file mode 100644 index 0000000000000000000000000000000000000000..79b10a712056959bf03a5ee6a41671b9cb3c3119 --- /dev/null +++ b/src/doc/administration/curl-access.rst @@ -0,0 +1,265 @@ +Curl Access for CaosDB +====================== + +As the API that is used to communicate with the CaosDB server is XML +over HTTP also simple HTTP clients, such as +`cURL <https://curl.haxx.se/>`__ can be used. cURL is an old and +established command line program for transferring files over networks +that implements various protocols including HTTP/HTTPS. It is installed +by default on many Linux distributions and can therefore be very useful +for testing and debugging of CaosDB. + +This small manual also gives some practical insights about the CaosDB +protocol itself. + +Doing a simple retrieve +----------------------- + +So, let’s start right away with a few basic examples. + +Let’s do a query on our demo instance: + +.. code:: bash + + curl "https://demo.indiscale.com/Entity/?query=FIND%20Experiment" + +By default cURL sends an HTTP GET request which is needed for doing +queries for CaosDB. CaosDB requires sending them to ``/Entity``. The +query itself is specified after the HTTP query string ``?query=``. +``%20`` is specific to URL encoding and corresponds to a space (see +https://en.wikipedia.org/wiki/Percent-encoding for details). So the +actual query we were doing was ``FIND Experiment`` which should return +all entities with name or with parent ``Experiment``. + +The response should look like: + +.. code:: xml + + <?xml version="1.0" encoding="UTF-8"?> + <?xml-stylesheet type="text/xsl" href="https://demo.indiscale.com/webinterface/webcaosdb.xsl" ?> + <Response srid="b56e3d1a442460c46dde924a54e8afba" timestamp="1561453363382" baseuri="https://demo.indiscale.com" count="3"> + <UserInfo> + <Roles> + <Role>anonymous</Role> + </Roles> + </UserInfo> + <Query string="FIND Experiment" results="3"> + <ParseTree>(cq FIND (entity Experiment) <EOF>)</ParseTree> + <Role /> + <Entity>Experiment</Entity> + <TransactionBenchmark since="Tue Jun 25 11:02:43 CEST 2019" /> + </Query> + <RecordType id="212" name="Experiment"> + <Permissions /> + </RecordType> + <RecordType id="215" name="UnicornExperiment"> + <Permissions /> + <Parent id="212" name="Experiment" /> + </RecordType> + <Record id="230"> + <Permissions /> + <Parent id="215" name="UnicornExperiment" /> + <Property id="216" name="date" datatype="DATETIME" importance="FIX"> + 2025-10-11 + <Permissions /> + </Property> + <Property id="217" name="species" datatype="TEXT" importance="FIX"> + Unicorn + <Permissions /> + </Property> + <Property id="214" name="Conductor" datatype="TEXT" importance="FIX"> + Anton Beta + <Permissions /> + </Property> + <Property id="221" name="Photo" datatype="Photo" importance="FIX"> + 226 + <Permissions /> + </Property> + <Property id="220" name="LabNotes" datatype="LabNotes" importance="FIX"> + 227 + <Permissions /> + </Property> + <Property id="218" name="UnicornVideo" datatype="UnicornVideo" importance="FIX"> + 228 + <Permissions /> + </Property> + <Property id="219" name="UnicornECG" datatype="UnicornECG" importance="FIX"> + 229 + <Permissions /> + </Property> + </Record> + </Response> + +We can see, that in fact two ``RecordTypes`` and one ``Record`` are +returned. Furthermore the response contains some additional information: + +- Attributes in the ``Response`` tag: + + - ``srid="b56e3d1a442460c46dde924a54e8afba"`` A unique identifier + for this request. + - ``timestamp="1561453363382"`` The `UNIX + timestamp <https://en.wikipedia.org/wiki/Unix_time>`__ for this + request. + - ``baseuri="https://demo.indiscale.com"`` The base URI of the + instance of CaosDB that performed the request. + - ``count="3"`` The number of results (2 ``RecordTypes`` and 1 + ``Record``) + +- Information about the user in ``UserInfo``. In this case we have not + logged in, so we are anonymous. +- Detailled information about the query. This includes for example the + parse tree and can be used for debugging and testing. Depending on + the settings of the server instance, this tag includes more or less + detail, for example a more detailled transaction benchmark. + +More details about the retrieve +------------------------------- + +The cURL statement used in the previous section made use of a lot of +default settings for cURL. Let’s have a closer look behind the options. +(I assigned the above URL to a shell variable, to make the statement +more readable.) + +.. code:: bash + + URL="https://demo.indiscale.com/Entity/?query=FIND%20Experiment" + + curl -X GET -b cookie.txt -D head.txt $URL + +This command specifies three more options: + +- ``-X GET`` Do a GET request. This can of course be replaced by POST, + PUT, DELETE or any other HTTP operation. +- ``-b cookie.txt`` This instructs cURL to use cookies from the file + ``cookie.txt`` (which we don’t have yet, see below) +- ``-D head.txt`` Tell cURL to store the received header in the file + ``head.txt``. + +Running this command will give us a similar response than in the +previous section, but additionally a file ``head.txt``: + +:: + + HTTP/1.1 200 OK + Content-Type: text/xml; charset=UTF-8 + Date: Tue, 25 Jun 2019 09:17:23 GMT + Accept-Ranges: bytes + Server: Restlet-Framework/2.3.12 + Vary: Accept-Charset, Accept-Encoding, Accept-Language, Accept + Transfer-Encoding: chunked + +There is nothing special in that header. Most importantly the request +has lead to a response without HTTP error. + +Logging in +---------- + +You might have asked yourself, what we need the cookie for. The simple +answer is: authentication. For many operations done on the CaosDB server +we have to log in first. The demo instance is configured to allow for +anonymous read access by default. But depending on the instance you are +accessing, even this might be disallowed. + +You can log in to the server using cURL with the following command: + +.. code:: bash + + URL="https://demo.indiscale.com/login" + + curl -X POST -c cookie.txt -D head.txt -H "Content-Type: application/x-www-form-urlencoded" -d username=salexan -d password=$PW $URL + +So here we are doing a ``POST`` request instead of a ``GET``. We are +instructing cURL to use the ``cookie.txt`` file to **store** the cookies +it recieves. This is done using the ``-c`` option (instead of the ``-b`` +option above). This time we are explicitely specifying the content type +of the content we are sending (using the ``POST`` request) with the +``-H`` option. The actual content sent are the two fields specified +using the ``-d`` options. This boils down to the two key-value-pairs +“username=” and “password=”. + +This time we are sending the information to a different context +indentified by ``/login``. + +If you don’t want to supply your passowrd in plain text you can for +example use a password manager (like I do), in my case +`pass <https://www.passwordstore.org/>`__ as follows to store your +password in the variable ``$PW``: + +.. code:: bash + + PW=$(pass your/passowrd/identifier) + +Custom Certificates +------------------- + +If you are running your own CaosDB instance it might be necessary to use +a custom SSL certificate. + +You can specify this using the option ``--cacert``, e.g.: + +:: + + --cacert "/path/to/certificate/root.cert.pem" + +Uploading files +--------------- + +According to the specification, a file upload is a POST with multipart +form data. This can be achieved using CURL with the following simple +command line: + +.. code:: bash + + curl -b cookie.txt \ + -F "FileRepresentation=<file.xml" \ + -F "testfile.bla=@testfile.bla" \ + $URL + +Here I am using a previously stored cookie from cookie.txt. + +There are two prerequisites to executing the above command: - The file +representation ``file.xml`` which is in this case stored in a file. - +The actual file to be uploaded with name ``testfile.bla``. The left hand +side of the assignment is very important as the identifier given here +(which in this case is ``testfile.bla``) will be used in the XML for +identifying the file that is described in the corresponding XML tag. + +Let’s have a look at the contents of file.xml: + +.. code:: xml + + <Post> + <File upload="testfile.bla" destination="/test/testfile.bla" description="bla" + checksum="672f8ff4ae8530de295f9dd963724947841e6277edec3b21820b5e44d0a64baef90fb04e22048028453d715f79357acc5bd2d566fe6ede65f981ba3dda06bae4" + size="3"/> + </Post> + +The attributes have the following meaning: - ``upload=testfile.bla`` The +filename given here is actually no filename, but an identifier to find +the multipart data chunk that contains the file. I called it +``testfile.bla`` for simplicity here. - +``destination="/test/testfile.bla"`` The destination path on the CaosDB +server file system. + +Before looking at the other attributes let’s have a look at the file +``testfile.bla`` itself: + +:: + + ok + +The file has size “3” which can be verified on linux using a: + +.. code:: bash + + stat testfile.bla + +It’s hashsum is important for checking the integrity after the transfer +to the server. It can be computed on linux using: + +.. code:: bash + + sha512sum testfile.bla + +These information has to be supplied as the remaining attributes to the +XML. diff --git a/src/doc/administration/maintenance.rst b/src/doc/administration/maintenance.rst new file mode 100644 index 0000000000000000000000000000000000000000..67d8475bf469957416a6f42e495db07b1533f151 --- /dev/null +++ b/src/doc/administration/maintenance.rst @@ -0,0 +1,67 @@ + +Maintenance of the CaosDB Server +================================ + +Creating a Backup +----------------- + +In order to create a full backup of CaosDB, the state of the SQL-Backend (MySQL, MariaDB) +has to be saved and the internal file system of CaosDB (symbolic links to +file systems that are mounted and uploaded files) has to be saved. + +You find the documentation on how to backup the SQL-Backend :any:`caosdb-mysqlbackend:Maintenance` + +In order to save the file backend we recommend to tar the file system. However, +you could use other backup methods that allow to restore the file system. +The CaosDB internal file system is located at the path defined by the +``FILE_SYSTEM_ROOT`` configuration variable (see :any:`configuration`). + +The command could look like:: + + tar czvf /path/to/new/backup /path/to/caosdb/filesystem.tar.gz + + +You can also save the content of CaosDB using XML. This is **not recommended** since it is less reliable than a real SQL backup. However there may be cases in which an XML backup is desirable, e.g., when transferring entities between two different CaosDB instances. +Collect the entities that you want to export in a :any:`caosdb-pylib:caosdb.common.models.Container`, here +named ``cont``. Then you can export the XML with:: + + from caosadvancedtools.export_related import invert_ids + from lxml import etree + invert_ids(cont) + xml = etree.tounicode(cont.to_xml( + local_serialization=True), pretty_print=True) + + with open("caosdb_data.xml"), "w") as fi: + fi.write(xml) + + +Restoring a Backup +------------------ + +.. note : + CaosDB should be offline before restoring data. + +If you want to restore the internal file system, simply replace it. E.g. if your +Backup is a tarball:: + + tar xvf /path/to/caosroot.tar.gz + + +You find the documentation on how to restore the data in the SQL-Backend :any:`caosdb-mysqlbackend:Maintenance` + + +If you want to restore the entities exported to XML, you can do:: + + cont = db.Container() + with open("caosdb_data.xml") as fi: + cont = cont.from_xml(fi.read()) + cont.insert() + +User Management +--------------- +The configuration of authentication mechanisms is done via the +``usersources.ini`` file (see :any:`configuration`). + +We recommend the Python tools (:any:`caosdb-pylib:Administration`) for further administrative tasks (e.g. setting +user passwords). + diff --git a/src/doc/administration/server_side_scripting.rst b/src/doc/administration/server_side_scripting.rst new file mode 100644 index 0000000000000000000000000000000000000000..1f2b9a1d80e11abed48ae89502ab6f80c099507e --- /dev/null +++ b/src/doc/administration/server_side_scripting.rst @@ -0,0 +1,85 @@ +Server-Side Scripting +===================== + +Introduction +------------ + +Small computation task, like some visualization, might be easily implemented in Python or some other language, but cumbersome to integrate into the server. Furthermore, the CaosDB server should stay a general tool without burden from specific projects. Also, one might want to trigger some standardized processing task from the web interface for convenience. For these situations the "server side scripting" is intended. + +Concepts +------------ + +The basic idea is that a script or program (script in the following) can be called to run on the server (or elsewhere in future) to do some calculations. This triggering of the script is done over the API so it can be done with any client. Input arguments can be passed to the script and the STDOUT and STDERR are returned. + +Each script is executed in a temporary home directory, which is automatically clean up. However, scripts can store files in the "$SHARED" folder and for example provide users a link that allows them to download files. + +Write and Install a Script +-------------------------- + +A server-side script must accept at least the ``--auth-token=AUTH_TOKEN`` option. All other command-line parameters which are passed to the script are not specified by the API and maybe defined by the script itself. + +So a minimal bash script would be + +.. code-block:: sh + + #!/bin/bash + echo Hello, World! + +thereby just ignoring the ``--auth-token`` option. + +The script has to be executable and must be placed somewhere in one of the directory trees which are configured by the server config :doc:`SERVER_SIDE_SCRIPTING_BIN_DIRS <configuration>`. + +Users will need the ``SCRIPTING:EXECUTE:path:to:the:script`` permission. Here the path to the script is of course relativet to the ``SERVER_SIDE_SCRIPTING_BIN_DIRS`` where it is located. + +For more information see the :doc:`specification of the API <../specs/Server-side-scripting>` + +Environment +------------ + +The script is called with several special environment variables to accommodate +for its special location. + +`HOME` +^^^^^^^^^^^^ +To be able to run with reduced privileges, the script has its `HOME` environment +variable set to a special directory with write access. This directory will be +deleted after the script has terminated. Its content is freshly copied for each +script invocation from a skeleton directory, located in the server directory, in +`scripting/home/`. By default, this directory contains the following: + +- `readme.md` :: A small text file describing the purpose of the directory. + +Users of CaosDB are invited to populate the directory with whatever their +scripts need (for example a `.pycaosdb.ini` file). + +Invocation +------------ + +Server side scripts are triggered by sending a POST to the `/scripting` resource. There are the following arguments that can be provided: + +- `call`: the name of the script to be called +- `-pN`: positional arguments (e.g. `-p0`, `-p1` etc.) +- `-ONAME`: named arguments (e.g. `-Otest`, `-Onumber` etc.) + +The arguments will be passed to the script. + +An invocation via a button in javascript could look like: + +.. code-block:: javascript + + var _make_sss_button = function (entity) { + const script = "script.py"; + + const scripting_form = $(` + <form class="btn-group-xs ${_css_class_export_button}" + method="POST" + action="/scripting"> + <input type="hidden" name="call" value="${script}"/> + <input name="-p0" value=""/> + <button type="submit" class="btn btn-link">Start script</button> + </form>`); + return scripting_form[0]; + } + +For more information see the :doc:`specification of the API <../specification/Server-side-scripting>` + diff --git a/src/doc/concepts.rst b/src/doc/concepts.rst new file mode 100644 index 0000000000000000000000000000000000000000..0db6186302302767c49735bcdfa81364685573f9 --- /dev/null +++ b/src/doc/concepts.rst @@ -0,0 +1,15 @@ + +Basic concepts of the CaosDB server +=================================== + +.. toctree:: + :hidden: + :glob: + + Data Model <Data-Model> + Permissions + roles + +The CaosDB server provides the HTTP API resources to users and client libraries. It uses a plain +MariaDB/MySQL database as backend for data storage, raw files are stored separately on the file +system. diff --git a/src/doc/conf.py b/src/doc/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..fb3d6264cc4add018b83d08378813aab9d10964d --- /dev/null +++ b/src/doc/conf.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('../caosdb')) + +import sphinx_rtd_theme + +# -- Project information ----------------------------------------------------- + +project = 'caosdb-server' +copyright = '2020, IndiScale GmbH' +author = 'Daniel Hornung' + +# The short X.Y version +version = '0.3' +# The full version, including alpha/beta/rc tags +release = '0.3' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'javasphinx', + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', # For Google style docstrings + "recommonmark", # For markdown files. + "sphinx.ext.autosectionlabel", # Allow reference sections using its title + "sphinx_rtd_theme", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'caosdb-serverdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'caosdb-server.tex', 'caosdb-server Documentation', + 'IndiScale GmbH', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'caosdb-server', 'caosdb-server Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'caosdb-server', 'caosdb-server Documentation', + author, 'caosdb-server', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for javasphinx -------------------------------------------------- +# See also https://bronto-javasphinx.readthedocs.io/en/latest/ + +# javadoc_url_map = { +# '<namespace_here>' : ('<base_url_here>', 'javadoc'), +# } + + +# -- Options for intersphinx ------------------------------------------------- + +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping +intersphinx_mapping = { + "python": ("https://docs.python.org/", None), + "caosdb-pylib": ("https://caosdb.gitlab.io/caosdb-pylib/", None), + "caosdb-mysqlbackend": ("https://caosdb.gitlab.io/caosdb-mysqlbackend/", None), +} + + +# -- Options for autodoc ----------------------------------------------------- +# TODO Which options do we want? +autodoc_default_options = { + 'members': None, + 'undoc-members': None, +} + + +# -- Options for autosectionlabel -------------------------------------------- + +autosectionlabel_prefix_document = True diff --git a/src/doc/development/benchmarking.md b/src/doc/development/benchmarking.md new file mode 100644 index 0000000000000000000000000000000000000000..f2d663f6e69799c7a87a28fbdf32f15fab892ac1 --- /dev/null +++ b/src/doc/development/benchmarking.md @@ -0,0 +1,130 @@ + +# Benchmarking CaosDB # + +Benchmarking CaosDB may encompass several distinct areas: How much time is spent in the server's +Java code, how much time is spent inside the SQL backend, are the same costly methods clalled more +than once? This documentation tries to answer some questions connected with these benchmarking +aspects and give you the tools to answer your own questions. + +## Tools for the benchmarking ## + +For averaging over many runs of comparable requests and for putting the database into a +representative state, Python scripts are used. The scripts can be found in the `caosdb-dev-tools` +repository, located at [https://gitlab.indiscale.com/caosdb/src/caosdb-dev-tools](https://gitlab.indiscale.com/caosdb/src/caosdb-dev-tools) in the folder +`benchmarking`: + +### `fill_database.py` ### + +This commandline script is meant for filling the database with enough data to represeny an actual +real-life case, it can easily create hundreds of thousands of Entities. + +The script inserts predefined amounts of randomized Entities into the database, RecordTypes, +Properties and Records. Each Record has a random (but with defined average) number of Properties, +some of which may be references to other Records which have been inserted before. Actual insertion +of the Entities into CaosDB is done in chunks of a defined size. + +Users can tell the script to store times needed for the insertion of each chunk into a tsv file. + +### `measure_execution_time.py` ### + +A somewhat outdated script which executes a given query a number of times and then save statistics +about the `TransactionBenchmark` readings (see below for more information about the transaction +benchmarks) delivered by the server. + +### Benchmarking SQL commands ### + +MariaDB and MySQL have a feature to enable the logging of SQL queries' times. This logging must be +turned on on the SQL server as described in the [upstream documentation](https://mariadb.com/kb/en/general-query-log/). For the Docker +environment LinkAhead, this can conveniently be done with `linkahead mysqllog {on,off,store}`. + +### External JVM profilers ### + +Additionally to the transaction benchmarks, it is possible to benchmark the server execution via +external Java profilers. For example, [VisualVM](https://visualvm.github.io/) can connect to JVMs running locally or remotely +(e.g. in a Docker container). To enable this in LinkAhead's Docker environment, set + +```yaml +devel: + profiler: true +``` + +Most profilers, like as VisualVM, only gather cumulative data for call trees, they do not provide +complete call graphs (as callgrind/kcachegrind would do). They also do not differentiate between +calls with different query strings, as long as the Java process flow is the same (for example, `FIND +Record 1234` and `FIND Record A WHICH HAS A Property B WHICH HAS A Property C>100` would be handled +equally). + +## How to set up a representative database ## +For reproducible results, it makes sense to start off with an empty database and fill it using the +`fill_database.py` script, for example like this: + +```sh +./fill_database.py -t 500 -p 700 -r 10000 -s 100 --clean +``` + +The `--clean` argument is not strictly necessary when the database was empty before, but it may make +sense when there have been previous runs of the command. This example would create 500 RecordTypes, +700 Properties and 10000 Records with randomized properties, everything is inserted in chunks of 100 +Entities. + +## How to measure request times ## + +If the execution of the Java components is of interest, the VisualVM profiler should be started and +connected to the server before any requests to the server are started. + +When doing performance tests which are used for detailed analysis, it is important that + +1. CaosDB is in a reproducible state, which should be documented +2. all measurements are repeated several times to account for inevitable variance in access (for + example file system caching, network variablity etc.) + +### Filling the database ### + +By simply adding the option `-T logfile.tsv` to the `fill_database.py` command above, the times for +inserting the records are stored in a tsv file and can be analyzed later. + +### Obtain statistics about a query ### + +To repeat single queries a number of times, `measure_execution_time.py` can be used, for example: + +```sh +./measure_execution_time.py -n 120 -q "FIND MusicalInstrument WHICH IS REFERENCED BY Analysis" +``` + +This command executes the query 120 times, additional arguments could even plot the +TransactionBenchmark results directly. + +## What is measured ## + +For a consistent interpretation, the exact definitions of the measured times are as follows: + +### SQL logs ### + +As per https://mariadb.com/kb/en/general-query-log, the logs store only the time at which the SQL +server received a query, not the duration of the query. + +#### Possible future enhancements #### + +- The `query_response_time` plugin may be additionally used in the future, see + https://mariadb.com/kb/en/query-response-time-plugin + +### Transaction benchmarks ### + +Transaction benchmarking manually collects timing information for each transaction. At defined +points, different measurements can be made, accumulated and will finally be returned to the client. +Benchmark objects may consist of sub benchmarks and have a number of measurement objects, which +contain the actual statistics. + +Because transaction benchmarks must be manually added to the server code, they only monitor those +code paths where they are added. On the other hand, their manual nature allows for a more +abstracted analysis of performance bottlenecks. + +### Java profiler ### + +VisualVM records for each thread the call tree, specifically which methods were called how often and +how much time was spent inside these methods. + +### Global requests ### + +Python scripts may measure the global time needed for the execution of each request. +`fill_database.py` obtains its numbers this way. diff --git a/src/doc/entities.png b/src/doc/entities.png new file mode 100644 index 0000000000000000000000000000000000000000..ba3e111a78ddec275636ff0b7e8b324edb13f7be Binary files /dev/null and b/src/doc/entities.png differ diff --git a/src/doc/index.rst b/src/doc/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..6a9013c51720f13b4f473d9213bb64b8d4d27eec --- /dev/null +++ b/src/doc/index.rst @@ -0,0 +1,31 @@ + +Welcome to caosdb-server's documentation! +========================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + :glob: + + Getting started <README_SETUP> + Concepts <concepts> + Query Language <CaosDB-Query-Language> + administration + development/* + specification/index.rst + Glossary + API documentation<_apidoc/packages> + +Welcome to the CaosDB, the flexible semantic data management toolkit! + +This documentation helps you to :doc:`get started<getting_started>`, explains the most important +:doc:`concepts<concepts>` and offers a range of :doc:`tutorials<tutorials>`. + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/src/doc/roles.md b/src/doc/roles.md new file mode 100644 index 0000000000000000000000000000000000000000..80729fb11f8113c64da7e9bf26b04de68fdd6681 --- /dev/null +++ b/src/doc/roles.md @@ -0,0 +1,35 @@ +# Roles # + +## Users and roles ## + +Interaction with CaosDB happens either as an authenticated *user* or without +authentication. In CaosDB, users can have zero, one or more *roles*, several +users may have the same role, and there may be roles without any users. + +## What are users and roles good for? ## + +The user and their roles are always returned by the server in answers to requests +and can thus be interpreted and used by clients. The most important use though +is [permission](manuals/general/permissions) checking in the server: Access and +modification of +entities can be controlled via roles, so that users of a given role are allowed +or denied certain actions. Incidentally, the permission to edit the permissions +of an entity is seen as defining the ownership of an object: Being able to +change the permissions is equivalent to being the owner. + +## Special roles ## + +There are some special roles, which are automatically assigned to users: + +- `anonymous` :: If requests are sent to the server without authentication, so + that no user is defined, the request always has the role `anonymous`. +- *User names* :: An authenticated user implicitly has a role with the same name + as the user name. +- `?OWNER?` :: If a user has the permission to edit the permissions of an + entity, the user automatically has the `?OWNER?` roler for that entity. +- `?OTHER?` :: The `?OTHER?` role is the contrary to the `?OWNER?` role: A user + is either the owner of an entity, or has the role `?OTHER?`. + +Except for the `anonymous` role, these special roles are not returned by the +server, but can nevertheless be used to define +[permissions](manuals/general/permissions). diff --git a/src/doc/specification/AbstractProperty.md b/src/doc/specification/AbstractProperty.md new file mode 100644 index 0000000000000000000000000000000000000000..d594f81967cacd708699ceaeccd9188685a7184d --- /dev/null +++ b/src/doc/specification/AbstractProperty.md @@ -0,0 +1,331 @@ +# AbstractProperty Specification + +## Introduction +An `AbstractProperty` is one of the basal [objects of HeartDB](./HeartDBObject). +An `AbstractProperty` MUST have the following _qualities_ (shortcut in brackets): +* a persistent id (`id`) +* an unique name (`name`) +* a description (`description`) +* a type (`type`) +* a generator `generator`) +* a creator (`creator`) +* a timestamp of it's date of creation (`created`) +* a set of owners and owning groups (`owner`) +* a set rules which controls the access to that `AbstractProperty` (`permission`) +Depending on the `AbstractProperty's` type it MUST have one of the following _qualities_: +* a unit (`unit`) +* a RecordType which is referenced by any instantiating ConcreteProperty. (`reference`) +This is described below. + +## Property Types +An `AbstractProperty` MUST have one of the following 7 types: +* `text` +* `integer` +* `double` +* `datetime` +* `reference` +* `file` + +An `AbstractProperty` of type `text`, `datetime`, or `file` MUST NOT have any unit or referenced RecordType. A `double`, `integer` AbstractProperty MUST have a unit but MUST NOT have a referenced RecordType. A `reference` `AbstractProperty` MUST have referenced RecordType und MUST NOT have a unit. + +## XML Representation of AbstractProperty Objects + +An `AbstractProperty` is represented in xml by a `<Property/>` tag. It's _qualities_ except for `owner` and `permission` are represented by an xml attribute of the same name. E.g `<Property name="length"/> denotes an AbstractProperty which {{{name` is _length_. The `owner` and `permission` qualities are not in use, yet. +Depending on the purpose of the xml document (shall it represent an object in the database or an object _to be posted _to the database?) the `<Property/>` tag may actually have just a few of the mentioned "quality-attributes". + +### GET AbstractProperty +Any xml representation of an `AbstractProperty` that is retrieved from the HeartDB Server MUST have exactly ONE of the following forms, depending on the `AbstractProperty's` type: +#### text + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="text" /> +#### integer + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="integer" unit="$unit" /> +#### double + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="double" unit="$unit" /> +#### datetime + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="datetime" /> +#### reference + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="reference" reference="$reference" /> +#### file + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="file" /> +'''General Notes: +* If the called Property does not exist or if the Property called without permission, the HeartDB Server will return an [Error](./Errorcodes). + +### POST AbstractProperty +Any xml representation of an `AbstractProperty` that is to be posted to the HeartDB server MUST have exactly ONE of the following forms, depending on the `AbstractProperty's` type: +#### text + + <Property name="$name" description="$description" generator="$generator" type="text" /> +#### integer + + <Property name="$name" description="$description" generator="$generator" type="integer" unit="$unit" /> +#### double + + <Property name="$name" description="$description" generator="$generator" type="double" unit="$unit" /> +#### datetime + + <Property name="$name" description="$description" generator="$generator" type="datetime" /> +#### reference + + <Property name="$name" description="$description" generator="$generator" type="reference" reference="$reference" /> +#### file + + <Property name="$name" description="$description" generator="$generator" type="file" /> +*General Notes:* +* The `AbstractProperty's` `id` and timestamp (`created`) will be generated by the HeartDB Server. +* The `AbstractProperty's` creator will be determined by the HeartDB Server depending on it's policy configuration. +* Any given attribute beyond these will be *ignored*. +* If the `<Property/>` tag isn't compliant with these the HeartDB Server will return an [Error](./Errorcodes). + +---- +## Examples +### GET Requests +#### Single-Get +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/1 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="1" name="explanation" type="text" description="explains the thing" generator="Timm (manually)"/> + </Response> +---- +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/explanation + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="1" name="explanation" type="text" description="explains the thing" /> + </Response> +---- +#### Multi-Get +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/explanation&2&3 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="1" name="explanation" type="text" description="explains the thing" /> + <Property id="2" name="comment" type="text" description="A comment" generator="Timm (manually)"/> + <Property id="3" name="name" type="text" description="Name" generator="Timm (manually)" /> + </Response> +---- +#### Get all +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/ + GET http://localhost:8122/mpidsserver/AbstractProperty + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response /> +[View Ticket #2](http://dev.bmp.ds.mpg.de/heartdb/ticket/2) +#### Erroneous Requests +##### Non-existing +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/123456 +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="123456"> + <Error code="201" description="Property does not exist. " /> + </Property> + </Response> +---- +### POST Requests + +*Request is to be sent to:* + + POST http://localhost:8122/mpidsserver/AbstractProperty/ + POST http://localhost:8122/mpidsserver/AbstractProperty +---- +#### Single-Post +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty1" type="integer" description="This is TimmsIntegerProperty1" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="39" name="TimmsIntegerProperty1" type="integer" unit="kg" description="This is TimmsIntegerProperty1" generator="Timm (manually)"/> + </Response> +---- +*Request body (with a reference to Property 12345):* + + <Post> + <Property name="Timms reference property 1" type="reference" reference="12345" description="This is Timms reference property 1"/> + </Post> +---- +#### Multi-Post +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty7" type="integer" description="This is TimmsIntegerProperty7" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty8" type="integer" description="This is TimmsIntegerProperty8" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty9" type="integer" description="This is TimmsIntegerProperty9" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + ... + <Property id="41" name="TimmsIntegerProperty9" type="integer" unit="kg" description="This is TimmsIntegerProperty9" generator="Timm (manually)"/> + </Response> + +---- +#### Erroneous Requests +##### No Description +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" generator="Timm (manually)"/> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg"> + <Error code="202" description="Property has no Description. " /> + </Property> + </Response> +---- +##### No generator +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" description="This is TimmsIntegerProperty4" /> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" > + <Error code="202" description="Property has no generator. " /> + </Property> + </Response> +---- +##### No Type +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty4" description="This is TimmsIntegerProperty4" unit="kg" generator="Timm (manually)"/> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty4" unit="kg" description="This is TimmsIntegerProperty4" generator="Timm (manually)" > + <Error code="252" description="Property has no PropertyType" /> + </Property> + </Response> +---- +##### No Unit +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty5" type="integer" description="This is TimmsIntegerProperty5" generator="Timm (manually)"/> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty5" type="integer" description="This is TimmsIntegerProperty5" generator="Timm (manually)"> + <Error code="202" description="Property has no Unit. " /> + </Property> + </Response> +---- +##### No Name +*Request body:* + + <Post> + <Property type="integer" description="This is TimmsIntegerProperty6" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property type="integer" unit="kg" description="This is TimmsIntegerProperty6" generator="Timm (manually)"> + <Error code="202" description="Property has no Name. " /> + </Property> + </Response> +---- +##### Invalid Name +*Request body:* + + <Post> + <Property id="39" name="TimmsIntegerProperty7" type="integer" description="This is TimmsIntegerProperty7" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="39" name="TimmsIntegerProperty7" type="integer" unit="kg" description="This is TimmsIntegerProperty7" generator="Timm (manually)"> + <Error code="204" description="Property has an invalid Name. " /> + </Property> + </Response> +---- +##### Mixed +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty2" type="integer" description="This is TimmsIntegerProperty2" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty4" description="This is TimmsIntegerProperty4" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty5" type="integer" description="This is TimmsIntegerProperty5" generator="Timm (manually)"/> + <Property type="integer" description="This is TimmsIntegerProperty6" unit="kg" generator="Timm (manually)"/> + <Property id="39" name="TimmsIntegerProperty7" type="integer" description="This is TimmsIntegerProperty7" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property type="integer" unit="kg" description="This is TimmsIntegerProperty6" generator="Timm (manually)"> + <Error code="202" description="Property has no Name. " /> + </Property> + <Property id="39" name="TimmsIntegerProperty7" type="integer" unit="kg" description="This is TimmsIntegerProperty7" generator="Timm (manually)"> + <Error code="204" description="Property has an invalid Name. " /> + </Property> + ... + </Response> + += DELETE AbstractProperties = + +HTTP-DELETE-requests are to be send to `http://${host}:${port}/mpidsserver/AbstractProperty/...`. Default port is 8123. + +*Example* +{{{ +DELETE http://${host}:${port}/mpidsserver/AbstractProperty/1&2&3&4 +}}} + + +## TODO +### UPDATE AbstractProperties +Notes: +* ids are persistent. They cannot be changed. +* Maybe we should take the PropertyTypes as persistent, too or just allow a few changes: Double <-> Integer? +Roadmap: +1) Specify and implement changing the name, description and unit of AbstractProperties. +2) Specify and implement changing the reference of an abstract reference property. +3) Anything else... diff --git a/src/doc/specification/Authentication.md b/src/doc/specification/Authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..9ec5bcd74ffb68ee33084f739d6d547a3e7a9bb8 --- /dev/null +++ b/src/doc/specification/Authentication.md @@ -0,0 +1,82 @@ +# Authentication + Some features of HeartDB are available to registered users only. Making any changes to the data stock via HTTP requires authentication by `username` _plus_ `password`. They are to be send as a HTTP header, while the password is to be hashed by the sha512 algorithm: + +| `username:` | `$username` | +|-------------|-------------|- +| `password:` | `$SHA512ed_password` | + + +## Sessions + +### Login + +#### Request Challenge + + * `GET http://host:port/mpidsserver/login?username=$username` + * `GET http://host:port/mpidsserver/login` with `username` header + +*no password required to be sent over http* + +The request returns an AuthToken with a login challenge as a cookie. The AuthToken is a dictionary of the following form: + + + {scope=$scope; + mode=LOGIN; + offerer=$offerer; + auth=$auth + expires=$expires; + date=$date; + hash=$hash; + session=$session; + } + + $scope:: A uri pattern string. Example: ` {**/*} ` + $mode:: `ONETIME`, `SESSION`, or `LOGIN` + $offerer:: A valid username + $auth:: A valid username + $expires:: A `YYYY-MM-DD HH:mm:ss[.nnnn]` date string + $date:: A `YYYY-MM-DD HH:mm:ss[.nnnn]` date string + $hash:: A string + $session:: A string + +The challenge is solved by concatenating the `$hash` string and the user's `$password` string and calculating the sha512 hash of both. Pseudo code: + + + $solution = sha512($hash + sha512($password)) + +#### Send Solution + +The old $hash string in the cookie has to be replaces by $solution and the cookie is to be send with the next request: + +`PUT http://host:port/mpidsserver/login` + +The server will return the user's entity in the HTTP body, e.g. + + + <Response ...> + <User name="$username" ...> + ... + </User> + </Response> + +and a new AuthToken with `$mode=SESSION` and a new expiration date and so on. This AuthToken cookie is to be send with every request. + +### Logout + +Send + +`PUT http://host:port/mpidsserver/logout` + +with a valid AuthToken cookie. No new AuthToken will be returned and no AuthToken with that `$session` will be accepted anymore. + +### Commandline solution with `curl` ## + +To use curl for talking with the server, first save your password into a variable: +`PW=$(cat)` + +The create a cookie in `cookie.txt` like this (note that this makes your password visible for a short time to everyone on your system: +`curl -X POST -c cookie.txt -D head.txt -H "Content-Type: application/x-www-form-urlencoded" -d username=<USERNAME> -d password="$PW" --insecure "https://<SERVER>/login` + +To use the cookie, pass it on with later requests: +`curl -X GET -b cookie.txt --insecure "https://<SERVER>/Entity/12345"` + diff --git a/src/doc/specification/Datatype.md b/src/doc/specification/Datatype.md new file mode 100644 index 0000000000000000000000000000000000000000..ffc9794ea36cfe748e8c11cee2bcd32adc8ebe0a --- /dev/null +++ b/src/doc/specification/Datatype.md @@ -0,0 +1,95 @@ +# Datatype + +## TEXT +* Description: TEXT stores stores any text values. +* Range: Any [utf-8](https://en.wikipedia.org/wiki/UTF-8) encodable sequence of characters with maximal 65,535 bytes. (Simply put: In most cases, any text with less than 65,535 letters and spaces will work. But if you use special characters like `à`, `€` or non-latin letters then the number of bytes, which are needed to store it, increases. Then the effective maximal length is smaller than 65,535. A bad case scenario would be a text in Chinese. Chinese characters need about three times the space of letters from the latin alphabet. Therefore, only 21845 Chinese characters can be stored within this datatype. Which is still quite a lot I guess :D) +* Examples: + * `Am Faßberg 17, D-37077 Göttingen, Germany` + * `Experiment went well until the problem with the voltmeter occured. Don't use the results after that.` + * `someone@email.org` + * `Abstract: bla bla bla ...` + * `Head of Group` + * `http://www.bmp.ds.mpg.de` + * + + A. Schlemmer, S. Berg, TK Shajahan, S. Luther, U. Parlitz, + Quantifying Spatiotemporal Complexity of Cardiac Dynamics using Ordinal Patterns, + 37th Annual International Conference of the IEEE Engineering in Medicine and Biology Society (EMBC), 2015, doi: 10.1109/EMBC.2015.7319283 + +---- + +## BOOLEAN +* Description: BOOLEAN stores boolean `TRUE` or `FALSE`. It is therefore suitable for any variable that represents that something is the case or not. +* Accepted Values: `TRUE` or `FALSE`, case insensitive (i.e. it doesn't matter if you use capitals or small letters). +* Note: You could also use a TEXT datatype to represent booleans (or even INTEGER or DOUBLE). But it makes a lot of sense to use this special datatype as it ensures that only the two possible values, `TRUE` or `FALSE` are inserted into the database. Every other input would be rejected. This helps to keep the database understandable and to avoid mistakes. + +---- + +## INTEGER +* Description: INTEGER stores integer numbers. If you need floating point variables, take a look at DOUBLE. +* Range: `-2147483648` to `2147483647`, `-0` is interpreted and stored as `0`. +* Note: This rather limited range is just provisional. It can be extended with low effort as soon as requested. + +---- + +## DOUBLE +* Description: DOUBLE stores floating point numbers with a double precision as defined by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +* Range: + * From `2.2250738585072014E-308` to `1.7976931348623157E308` (negative and positive) with a precision of 15 decimals. + * Any other decimal number _might work_ but it is not guaranteed. + * `-0`, `0`, `NaN`, `-inf` and `inf` +* Note: The server generates a warning when the precision of the submitted DOUBLE value is to high to be preserved. + +---- + +## DATETIME +The DateTime data type exists in (currently) three flavors which are dynamically chosen during parsing on the the serverside. The flavors have different ranges, support of time zones and intended use cases. Only the first two flavors are actually implemented for storage and queries. The third one is implemented for queries exclusively. + +### UTCDateTime +* Description: This DATETIME flavor stores values which represent a single point of time according to [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) with the format specified by [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) (Combined date and time). It does support [UTC Leap Seconds](https://en.wikipedia.org/wiki/Leap_second) and time zones. +* Range: From `-9999-01-01T00:00:00.0UTC` to `9999-12-31T23:59:59.999999999UTC` with nanosecond precision. +* Examples: + * `2016-01-01T13:23:00.0CEST` which means _January 1, 2016, 1:23 PM, Central European Summer Time_. + * `-800-01-01T13:23:00.0` which means _January 1, 800 BC, 1:23 PM, UTC_. +* Note: + * It is allowed to ommit the nanosecond part of a UTCDateTime (`2016-01-01T13:23:00CEST`). This indicates a precision of seconds for a UTCDateTime value. + +### Date + Description:: This DATETIME flavor stores values which represent a single date, month or year according to the [gregorian calendar](https://en.wikipedia.org/wiki/Gregorian_Calendar). A month/year is conceived as a single date with the presion of a month/year. This concept is useful if you try to understand the query semantics which are explained [elsewhere](./QueryLanguage#POVDateTime). + Format:: `Y[YYY][-MM[-dd]]` (where square brackets mean that the expression is optional). + Range:: Any valid date according to the gregorian calendar from `-9999-01-01` to `9999-12-31` (and respective dates with lower precision. E.g. the year `-9999`). There is no year `0`. +* Note: Date is a specialization of [#SemiCompleteDateTime]. + +### SemiCompleteDateTime +* Description: A generalization of the _Date_ and _UTCDateTime_ flavors. In general, there is no time zone support. Although this flavor is not yet storable in general, it is implemented for search queries yet. I.e. you might search for `FIND ... date>2015-04-03T20:15` yet. +* Format: `Y[YYY]['-MM[-dd[Thh:[mm[:ss[.ns]]]]]]]`. +* Special Properties: For every SemiCompleteDateTime _d_ there exists a _Inclusive Lower Bound_ (`d.ILB`) and a _Exclusive Upper Bound_ (`d.EUB`). That means, a SemiCompleteDateTime can be interpreted as an interval of time. E.g. `2015-01` is the half-open interval `[2015-01-01T00:00:00.0, 2016-01-01T00:00:00.0)`. ILB and EUB are UTCDateTimes respectively. These properties are important for the semantics of the the query language, especialy the [operators](./QueryLanguage#POVDateTime). + +### Future Flavors +Please file a new feature request as soon as you need them. +* Time:: For a time of the day (without the date). Supports time zones. +* FragmentaryDateTime:: For any fragmentary DateTime. That is an arbitrary combination of year, month, day of week, day of month, day of year, hour of day, minute, seconds (and nanoseconds). This flavor is useful for recurrent events like a bus schedule (_Saturday, 7:30_) or the time of a standing order for money transfer (_third day of the month_). + +---- + +## REFERENCE +* Description: REFERENCE values store the [Valid ID](./Glossary#valid-id) of an existing entity. The are useful to establish links between two entities. +* Accepted Values: Any [Valid ID](./Glossary#valid-id) or [Valid Unique Existing Name](./Glossary#valid-unique-existing-name) or [Valid Unique Temporary ID](./Glossary#valid-unique-temporary-id) or [Valid Unique Prospective Name](./Glossary#valid-unique-prospective-pame). +* Note: + * After beeing processed successfully by the server the REFERENCE value is normalized to a [Valid ID](./Glossary#valid-id). I.e. it is guaranteed that a REFERENCE value of a valid property is a positive integer. + +### FILE +* Description: A FILE is a special REFERENCE. It only allows entity IDS which belong to a File. + +### RecordType as a data type +* Furthermore, any RecordType can be used as a data type. This is a variant of the REFERENCE data type where any entity is a valid value which is a child of the RecordType in question. +* Example: + * Let `Person` be a RecordType, `Bertrand Russel` be a child of `Person`. Then `Bertrand Russel` is a valid value for a property with a `Person` data type. + +## LIST +* Description: A LIST is always a list of something which has another data type. E.g. A LIST of TEXT values, a LIST of REFERENCES value, etc. Here we call TEXT resp. REFERENCE the **Element Data Type**. The LIST data type allows you to store an arbitrary empty or non-empty ordered set (with duplicates) of values of the *same* data type into one property. Each value must be a valid value of the Element Data Type. +* Example: + * LIST of INTEGER: ```[0, 2, 4, 5, 8, 2, 3, 6, 7]``` + * LIST of Person, while `Person` is a RecordType: ```['Bertrand Russel', 'Mahatma Ghandi', 'Mother Therese']``` + + diff --git a/src/doc/specification/Fileserver.md b/src/doc/specification/Fileserver.md new file mode 100644 index 0000000000000000000000000000000000000000..fda2ec183c909e494a407f74eb1f4d03a5a23625 --- /dev/null +++ b/src/doc/specification/Fileserver.md @@ -0,0 +1,142 @@ +# Fileserver + +## Info +There are several ways to utilize the file server component of HeartDB. It is possible to upload a file or a whole folder including subfolders via HTTP and the _drop off box_. It is possible to download a file via HTTP identified by its ID or by its path in the internal file system. Furthermore, it is possible to get the files metadata via HTTP as an xml. + +## File upload +### Drop off box + +The drop off box is a directory on the HeartDB server's local file system, specified in the `server.conf` file in the server's basepath (something like `~/HeartDB/server/server.conf`). The key in the `server.conf` is called `dropoffbox`. Since the drop off box directory is writable for all, users can push their files or complete folders via a `mv` or a `cp` (recommended!) in that folder. The server deletes files older than their maximum lifetime (24 hours by default, specified `in server.conf`). But within their lifetime a user can prompt the server to pick up the file (or folder) from the drop off box in order to transfer it to the internal file system. + +Now, the user may send a pick up request to `POST http://host:port/mpidsserver/FilesDropOff` with a similar body: + + <Post> + <File pickup="$path_dropoffbox" destination="$path_filesystem" description="$description" generator="$generator"/> + ... + </Post> + +where +* $path_dropoffbox is the actual relative path of the dropped file or folder in the DropOffBox, +* $path_filesystem is the designated relative path of that object in the internal file system, +* $description is a description of the file to be uploaded, +* $generator is the tool or client used for pushing this file. + +After a successful pick up the server will return: + + <Response> + <File description="$description" path="$path" id="$id" checksum="$checksum" size="$size" /> + ... + </Response> + +where +* $id is the new generated id of that file and +* $path is the path of the submitted file or folder relative to the file system's root. + +### HTTP upload stream +#### Files + +There is an example on file upload using cURL described in detail in [the curl section of this wiki](manuals/curl/curl-access). + +File upload via HTTP is implemented in a [rfc1867](http://www.ietf.org/rfc/rfc1867.txt) consistent way. This is a de-facto standard that defines a file upload as a part of an HTML form submission. This concept shall not be amplified here. But it has to be noticed that this protocol is not designed for uploads of complete structured folders. Therefore the HeartDB file components have to impose that structure on the upload protocol. + +HeartDB's file upload resource does exclusively accept POST requests of MIME media type `multipart/form-data`. The first part of each POST body is expected to be a form-data text field, containing information about the files to be uploaded. It has to meet the following requirements: +* `Content-type: text/plain; charset=UTF-8` +* `Content-disposition: form-data; name="FileRepresentation"` + +If the content type of the first part is not `text/plain; charset=UTF-8` the server will return error 418. If the body is not actually encoded in UTF-8 the servers behaviour is not defined. If the field name of the first part is not `FileRepresentation` the server will return error 419. + +The body of that first part is to be an xml document of the following form: + + + <Post> + <File upload="$temporary_identifier" destination="$path_filesystem" description="$description" checksum="$checksum" size="$size"/> + ... + </Post> + +where +* $temporary_identifier is simply a arbitrary name, which will be used to identify this `<File>` tag with a uploaded file in the other form-data parts. +* $path_filesystem is the designated relative path of that object in the internal file system, +* $description is a description of the file to be uploaded, +* $size is the files size in bytes, +* $checksum is a SHA-512 Hash of the file. + +The other parts (which must be at least one) may have any appropriate media type. `application/octet-stream` is a good choice for it is the default for any upload file according to [rfc1867](http://www.ietf.org/rfc/rfc1867.txt). Their field name may be any name meeting the requirements of [rfc1867](http://www.ietf.org/rfc/rfc1867.txt) (most notably they must be unique within this POST). But in order to identify the corresponding xml file representation of each file the `filename` parameter of the content-disposition header has to be set to the proper $temporary_identifier. The Content-disposition type must be `form-data`: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier"` + +Finally the body of these parts have to contain the file encoded in the proper `Content-Transfer-Encoding`. + + +If a file part has a `filename` parameter which doesn't occur in the xml file representation the server will return error 420. The file will not be stored anywhere. If an xml file representation has no corresponding file to be uploaded (i.e. there is no part with the same `filename`) the server will return error 421. Some other error might occur if the checksum, the size, the destination etc. are somehow corrupted. + +#### Folders + +Uploading folders works in a similar way. The first part of the `multipart/form-data` document is to be the representation of the folders: + + + <Post> + <File upload="$temporary_identifier" destination="$path_filesystem" description="$description" checksum="$checksum" size="$size"/> + ... + </Post> + +The root folder is represented by a part which has a header of the form: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier/"` +The slash at the end of the `filename` indicates that this is a folder, not a file. Consequently, the body of this part will be ignored and should be empty. +Any file with the name `$filename` in the root folder is represented by a part which has a header of the form: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier/$filename"` +Any sub folder with the name `$subfolder` is represented by a part which has a header of the form: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier/$subfolder/"` + +Likewise, a complete directory tree can be transfered by appending the structure to the `filename` header field. + +**Example**: +Given the structure + + rootfolder/ + rootfolder/file1 + rootfolder/subfolder/ + rootfolder/subfolder/file2 + +an upload document would have the following form: + + ... (HTTP Header) + Content-type: multipart/form-data, boundary=AaB03x + + --AaB03x + content-disposition: form-data; name="FileRepresentation" + + <Post> + <File upload="tmp1234" destination="$path_filesystem" description="$description" checksum="$checksum" size="$size"/> + </Post> + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/" + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/file1" + + Hello, world! This is file1. + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/subfolder/" + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/subfolder/file2" + + Hello, world! This is file2. + + --AaB03x-- + + +(**Timm 2014-06-17: to be continued**) + +## Consistency checks +To start a consistency check on either the complete file system or a subdirectory, add the `fileStorageConsistency` flag to a retrieve query. In a `GET` request, simply add `...?fileStorageConsistency=<OPTIONS>` to the URL. Possible options are (currently, only one of them?): + +- `-t <TIMEOUT>` :: The timeout for the query (in seconds?) +- `-c <TESTCASE>` :: To trigger internal test cases. +- `<PATH>` :: The path in the file system where searching files should start. If omitted or `\`, the full file system will be checked. + +One example, using curl and an existing cookie: +`curl -X GET -G -b cookie.txt -d "fileStorageConsistency=Analysis/VideoAnalysis/masks/" --insecure "https://<SERVER>/Entity/12345"` + + diff --git a/src/doc/specification/Paging.md b/src/doc/specification/Paging.md new file mode 100644 index 0000000000000000000000000000000000000000..fb994639204704618af2b1c7a8ccb32301013af3 --- /dev/null +++ b/src/doc/specification/Paging.md @@ -0,0 +1,25 @@ +# Paging +The Paging flag splits the retrieval of a (possibly huge) number entities into pages. + +## Syntax + + + flag = name, [":", value]; + name = "P"; + value = [ index ], ["L", length]]; + index = ? any positive integer ?; + length = ? any positive integer ?; + +## Semantics + +The `index` (starting with zero) denotes the index of the first entity to be retrieved. The `length` is the number of entities on that page. If `length` is omitted, the default number of entities is returned (as configured by a server contant called ...). If only the `name` is given the paging behaves as if the `index` has been zero. + +## Examples + +`http://localhost:8123/mpidsserver/Entities/all?flags=P:24L50` returns 50 entities starting with the 25th entity which would be retrieved without paging. + +`http://localhost:8123/mpidsserver/Entities/all?flags=P:24` returns the default number of entities starting with the 25th entity which would be retrieved without paging. + +`http://localhost:8123/mpidsserver/Entities/all?flags=P:L50` returns 50 entities starting with the first entity which would be retrieved without paging. + +`http://localhost:8123/mpidsserver/Entities/all?flags=P` returns the default number of entities starting with the first entity which would be retrieved without paging. diff --git a/src/doc/specification/Record.md b/src/doc/specification/Record.md new file mode 100644 index 0000000000000000000000000000000000000000..1e43e372f754d884da2ff15cfb7b71f6813f1785 --- /dev/null +++ b/src/doc/specification/Record.md @@ -0,0 +1,367 @@ +# Record + + += GET records = + +HTTP-GET-requests are to be send to `http://${host}:${port}/mpidsserver/Record/...`. Default port is 8123. + +### Get single record + +{{{ +GET http://${host}:${port}/mpidsserver/Record/1 +}}} + +returns in case of success: + + + <Response id="1353783822304" generated="2012-11-23 11:10:50.507" items="1"> + <Record id="1" generator="X23" timestamp="2012-09-22 00:56:03.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended">100111327</Property> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">Here comes the explanation</Property> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2010-01-20 20:15:00.0</Property> + </Record> + </Response> + + +If a requested record didn't exist, it returns: + + + <Response id="1354256823059" generated="2012-11-23 11:17:06.754" items="1"> + <Record id="1"> + <Error code="101" description="Record does not exist. "/> + </Record> + </Response> + +### Get multiple records + +{{{ +GET http://${host}:${port}/mpidsserver/Record/1&2 +}}} + +returns in case of success: + + + <Response id="1354570488723" generated="2012-11-23 11:12:33.666" items="2"> + <Record id="1" generator="X23" timestamp="2012-09-22 00:56:03.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended">100111327</Property> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">Here comes the explanation</Property> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2010-01-20 20:15:00.0</Property> + </Record> + <Record id="2" generator="G22" timestamp="2012-09-22 00:56:03.0" recordtypename="Heart" recordtypeid="6"> + <Property id="9" name="weightg" type="double" unit="g" description="weight in g" exponent="0" importance="obligatory">1000.0</Property> + </Record> + </Response> + +Same behavior as above if one of these didn't exist. + +## POST records + +The POST-requests are to be send to `http://${host}:${port}/mpidsserver/Record`. Default port is 8123. + +### Post single record + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="6">2012-12-24 18:00:00</Property> + <Property id="1">We investigate if Santa Clause does fit down the chimney</Property> + </Record> + </Post> + +| Remarks | +|---------| +* The obligatory root tag is `<Post>` +* A record has to have a `generator="..."` attribute and a _recordtype_ (i.e. a `recordtypeid="..."` or `recordtypename="..."` attribute). +* Depending on the _recordtype_ of the posted _record_ there are several _concrete properties_ that must be definded (they are _obligatory_). + +In case of success the response will look like: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1353708657159" generated="2012-11-23 11:55:41.223" items="1"> + <Record id="272" generator="Timm" timestamp="2012-11-23 10:55:43.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2012-12-24 18:00:00.0</Property> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">We investigate if Santa Clause does fit down the chimney</Property> + </Record> + </Response> + +| Remarks | +|---------| +* The responces contains the whole record as it lies in the database now. That entails it's final `id`, a timestamp (`generated` attribute) and a full description of the properties with name, type, descpription and so on. +* For further specifications of the _concrete properties_ see [wiki:ConcreteProperty]. + +#### Erroneous properies + +Now, there are a couple of errors that can occur (cf. [wiki:errorcodes]). The errors that occur due to corrupt concrete properies are described in [wiki:ConcreteProperty]. If any Errors occur on the level of properties, the whole record is handled as unqualified to be posted. Suppose the post body reads + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="6">Christmas Eve</Property> + <Property id="1">Does Santa Clause fit down the chimney</Property> + </Record> + </Post> + +The first property cannot be parsed to a datetime format. Therefore, the response will be + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354575364020" generated="2012-11-23 12:37:20.045" items="1"> + <Record generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="103" description="Record has unqualified Properties. " /> + ... + </Record> + </Response> + +| Remarks | +|---------| +* The error "Record has unqualified Properties" occures whenever the posted concrete properties of the record are corrupt (i.e. any `<Error>` tag is child of a `<Property>` tag). +* Erroneous records are not stored to the database. + +#### Erroneous record tags + +1) If the recordtype hasn't been indicated, the server returns + + + ... + <Error code="105" description="Record has no RecordType. " /> + ... + +2) The indicated recordtype does not exist: + + + ... + <Error code="105" description="Record's RecordType does not exist. " /> + ... +view Tickets #16 #17 + +3) The generator has not been indicated: + + + ... + <Error code="105" description="Record has no Generator. " /> + ... + +4) The record's _recordtype_ demands for a property that isn't present: + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="6">2012-12-24 18:00:00</Property> + </Record> + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354701618532" generated="2012-11-23 13:25:41.59" items="1"> + <Record generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="102" description="Obligatory Property 1 (explanation) is missing. " /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2012-12-24 18:00:00.0</Property> + </Record></Response> + + +5) Record does'nt entail any properties: + + + ... + <Error code="105" description="Record has no Properties. " /> + ... + +6) The indicated ID and name of a _recordtype_ are mismatching (at least one of them exists but they don't correspond to each other). + + + ... + <Error code="105" description="Record has invalid RecordType (Name/ID mismatch). " /> + ... + + +### Post multiple records + +It is possible to post several records in one go. Here we go through a few scenarios with independent sets of records and closely intertwined ones. + +#### Independent records + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + ... (properties) ... + </Record> + <Record generator="Timm" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1356209136586" generated="2012-11-30 11:33:39.897" items="2"> + <Record id="274" generator="Timm" timestamp="2012-11-30 10:33:41.0" recordtypename="Experiment" recordtypeid="1"> + ... (properties) ... + </Record> + <Record id="275" generator="Timm" timestamp="2012-11-30 10:33:41.0" recordtypename="Experiment" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Response> + +#### Non-circular interdependent records: + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="10">-1</Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354608915619" generated="2012-11-30 11:40:35.369" items="2"> + <Record id="277" generator="Timm" timestamp="2012-11-30 10:40:37.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">276</Property> + ... (more properties) ... + </Record> + <Record id="276" generator="Timm" timestamp="2012-11-30 10:40:37.0" recordtypename="Experiment" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Response> + +| Remarks | +|---------| +* The record that is to be referenced by another gets an id less than zero (id="-1"). This id is provisional and will be replaced by the final id, which is the one the record is stored with in the database, in the response (id="276"). + +#### Circular dependent records + +*HTTP body:* + + + <Post> + <Record id="-2" generator="Timm" recordtypeid="1"> + <Property id="10">-1</Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypeid="1"> + <Property id="10">-2</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354698071005" generated="2012-11-30 11:52:28.789" items="2"> + <Record id="278" generator="Timm" timestamp="2012-11-30 10:52:30.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">279</Property> + ... (more properties) ... + </Record><Record id="279" generator="Timm" timestamp="2012-11-30 10:52:30.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">278</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Response> + +| Remarks | +|---------| +* Likewise,records that are to be referenced by each other get ids less than zero (id="-1" and id="-2"). These are provisional and will be replaced by the final ids in the response (id="278" and id="279"). + + +#### Erroneous multi-posts + +1) If any error occurs in a single record, all correct records are nevertheless stored (In future this behavior should be controlled better by an _atomic flag_. See #18). + +2) If a referenced record is corrupt, the referencing record is marked with an error tag equally. + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="10">-1</Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypeid="1"> + <Property id="1">Explanation2</Property> + <!-- obligatory property 6 is missing --> + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1356334296695" generated="2012-11-30 12:14:37.246" items="2"> + <Record id="-2" generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="103" description="Record has unqualified Properties. " /> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested"> + <Error code="204" description="Referenced Record is unqualified. " /> + -1 + </Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="102" description="Obligatory Property 6 (startDate) is missing. " /> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">Explanation2</Property> + </Record> + ... (more records) ... + </Response> + +3) Self-referencing is not allowed. A self-referencing record will return + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1355236745833" generated="2012-11-30 12:18:45.066" items="1"> + <Record id="-2" generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="104" description="Self-referencing is not allowed. " /> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">-2</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Response> + +4) Referencing a non-existing provisional id will return (cf. #19): + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354990914009" generated="2012-11-30 13:07:00.45" items="1"> + <Record generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="104" description="Referenced record does not exist. " /> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">-2</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Response> + += DELETE records = + +HTTP-DELETE-requests are to be send to `http://${host}:${port}/mpidsserver/Record/...`. Default port is 8123. + +*Example* +{{{ +DELETE http://${host}:${port}/mpidsserver/Record/1&2&3&4 +}}} + + + + + + diff --git a/src/doc/specification/RecordType.md b/src/doc/specification/RecordType.md new file mode 100644 index 0000000000000000000000000000000000000000..068c433d8c5390882176c0ff8783576127a2da2e --- /dev/null +++ b/src/doc/specification/RecordType.md @@ -0,0 +1,164 @@ +# RecordType +---- +## Overview +RecordTypes function as templates for [[Record|Records]], they provide a description for a type of Record and define which [[Property|Properties]] should be present. Properties come with an _importance_ attribute which tells the user or client program how strongly necessary the Property is. (As all other entities,) RecordTypes can be inherited from other RecordTypes (or any Entities). When RecordTypes inherit from other RecordTypes, the _inheritance_ flag tells which properties shall be inherited. + +### Importance + +Importances are more of a recommendation, Records with omitted Properties can technically still be committed. + +| *Importance* | *Meaning* | *Consequence when omitted* | +|--------------|----------------------|----------------------------| +| `OBLIGATORY` | Property must be present | *FIXME* Can be forced to be committed the server, but usually an error will be returned. | +| `RECOMMENDED` | Property should be present (makes sense for most users) | *FIXME* Usually an error will be returned? | +| `SUGGESTED` (*FIXME* the default?) | Property may be present (may make sense for some users) | *FIXME* No negative consequence? | + +*FIXME* Did I get recommended and suggested right? + +### Inheritance + +The _inheritance_ flag decides which [[Property|Properties]] are inherited from parents. + +| *Inheritance* | *Meaning* | +|----------------|-----------| +| _None_ (default) | Nothing is being inherited | +| `OBLIGATORY` | *FIXME* Properteis of importance `OBLIGATORY` and above? | +| `SUGGESTED` | *FIXME* Properteis of importance `SIGGESTED` and above, so the same effect as `ALL`? | +| `ALL` | Copy everything from the parent. | +| `FIX` | *FIXME* in `tests/test_inheritance.py` of pyint_test this is used, what does it mean???| + + +## Examples +### GET Requests +#### Single-Get +*Request:* + + GET http://localhost:8122/mpidsserver/RecordType/1 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="1" name="Experiment" description="Description Experiment"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="suggested" /> + </RecordType> + </Response> +---- +#### Multi-Get +*Request:* + + GET http://localhost:8122/mpidsserver/RecordType/1&2&3 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="1" name="Experiment" description="Description Experiment"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="suggested" /> + </RecordType> + <RecordType id="2" name="Measurement" description="Description Measurement"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="suggested" /> + </RecordType> + <RecordType id="3" name="Video" description="Description Video"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="obligatory" /> + </RecordType> + </Response> +---- +#### Get all +*Request:* + + GET http://localhost:8122/mpidsserver/RecordType/ + GET http://localhost:8122/mpidsserver/RecordType + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response /> +[View Ticket #2](http://dev.bmp.ds.mpg.de/heartdb/ticket/2) + +---- +### POST Requests + +*Request is to be send to:* + + POST http://localhost:8122/mpidsserver/RecordType/ + POST http://localhost:8122/mpidsserver/RecordType/ +---- +#### Single-Post +*HTTP Body:* + + <Post> + <RecordType name="TimmsRecordType11" > + <Property id="3" importance="recommended"/> + <Property id="4" importance="obligatory"/> + <Property name="age" /> + </RecordType> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="40" name="TimmsRecordType11"> + <Property id="3" name="name" type="text" description="Name" importance="recommended" /> + <Property id="4" name="id" type="integer" description="identifier" importance="suggested" /> + </RecordType> + </Response> +---- +#### Multi-Post +*HTTP Body:* + + <Post> + <RecordType name="TimmsRecordType11" > + <Property id="3" importance="recommended"/> + <Property id="4" importance="obligatory"/> + <Property name="age" /> + </RecordType> + <RecordType name="TimmsRecordType12" > + <Property id="6" importance="recommended"/> + <Property id="7" importance="obligatory"/> + <Property id="15" /> + </RecordType> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="40" name="TimmsRecordType11" description="null"> + <Property id="3" name="name" type="text" description="Name" importance="recommended" /> + <Property id="4" name="id" type="integer" description="identifier" importance="suggested" /> + <Property id="3" name="name" type="text" description="Name" importance="recommended" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="suggested" /> + </RecordType> + <RecordType id="41" name="TimmsRecordType12"> + <Property id="6" name="startDate" type="datetime" description="start" importance="recommended" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="suggested" /> + <Property id="15" name="TimmsIntProperty" type="integer" unit="kg" description="This is TimmsIntProperty" importance="suggested" /> + </RecordType> + </Response> + += DELETE RecordTypes = + +HTTP-DELETE-requests are to be send to `http://${host}:${port}/mpidsserver/RecordType/...`. Default port is 8123. + +*Example* +{{{ +DELETE http://${host}:${port}/mpidsserver/RecordType/1&2&3&4 +}}} + diff --git a/src/doc/specification/Server-side-scripting.md b/src/doc/specification/Server-side-scripting.md new file mode 100644 index 0000000000000000000000000000000000000000..18dfe9eab0201fdd322bb2f61cbc87370ced99d5 --- /dev/null +++ b/src/doc/specification/Server-side-scripting.md @@ -0,0 +1,195 @@ +# Server-Side Scripting API (v0.1) + +The CaosDB Server can execute scripts (bash/python/perl) and +compiled executables. The scripts can be invoked by a remote-procedure-call +(RPC) protocol. Both, the requirements for the scripts and the +RPC are described in this document. + +## Configuration of the Server + +The CaosDB Server has two relevant properties: + +### `SERVER_SIDE_SCRIPTING_BIN_DIR` +is the directory where executable scripts are to be placed. The server will not execute scripts which are out side of this directory. This directory must be readable and executable for the server. But it should not be readable or executable for anyone else. +Executable files in this directory or its subdirectories are called `SSS`. + +All other files in that directory MUST be ignored by the server, i.e. the server will never call them directly. + However, they MAY contain additional data, different implementations, libraries etc. + +A symlink pointing to an executable MUST be treated as SSS, too. + +#### Example SERVER\_SIDE\_SCRIPTING\_BIN\_DIR + +``` +/ ++- script1.py (EXECUTABLE) ++- script2.sh (EXECUTABLE) ++- subdir1/ + +-script3.pl (EXECUTABLE) ++- subdir2/ + +- data_for_script2 + +- another_script.py (EXECUTABLE) ++- script4 -> ./subdir2/another_script.py (SIMLINK to EXECUTABLE) ++- script5 -> /usr/local/bin/external (SIMLINK to EXECUTABLE) +``` + +The files `scripts1.py`, `scripts2.sh`, `scripts3.pl` and `another_script.py` are SSS. +Also the `script4` can be called which would result in calling `another_script.py` +The script5 points to an executable which is not stored in the `SSD_DIR`. + +### `SERVER_SIDE_SCRIPTING_WORKING_DIR` +is the directory under which the server creates temporary working directories. The server needs writing, reading and executing permissions. The temporary working directories are deleted after the scripts have finished and the server has collected the results of the scripts. + + +## Calling a Server-Side Script Via Remote Procedure Call + +* Users can invoke scripts via HTTP under the uri `https://$HOST:$HTTPS_PORT/$CONTEXT_ROOT/scripting`. The server accepts POST requests with content types `application/x-www-form-urlencoded` and `multipart/form-data`. +* There are 6 types of form fields which are processed by the server: + 1. A single parameter form field with name="call" (the path to script, relative to the `SERVER_SIDE_SCRIPTING_BIN_DIR`). If there are more than one fields with that name, the behavior is not defined. + 2. Zero or more parameter form fields with a unique name which starts with the string `-O` (command line options). + 3. Zero or more parameter form fields with a unique name which starts with the string `-p` (positional command line arguments). + 4. Zero or more file form fields which have a unique field name and a unique file name (upload files). If the field names or file names are not unique, the behavior is not defined. + 5. A parameter form field with name="timeout" (the request timeout) + 6. A parameter form field with name="auth-token" and a valid CaosDB AuthToken string or "generate" as value. + +## How Does the Server Call the Script? +* The server executes the script in a temporary working directory, called *PWD* (which will be deleted afterwards). +* The form parameter `call`, the options, the arguments and file fields construct the string which is executed in a shell. The value of *call* begins the command line. +* For any option paramter with the name `-Ooption_name` and a value `option_value` a resulting `--option_name=option_value` is appended to the commandLine in no particular order. +* The values of the positional arguments are appended sorted alphabetically by their field name. E.g. a parameter `-p0` with value `v0` and a parameter `-p1` with value `v1` result in appending `v0 v1` to the command line. +* All files will be loaded into the directory `$PWD/.upload_files` with their file name (i.e. the form field property). If the file names contain slashes '/' they will build sub-directories in the `./upload_files`. +* If a file form field has a field name which begins with either `-p` or `-O` the file name with prefix `.upload_files/` is passed as value of an option or as a positional argument to the script. Thus it is possible to distinguish several uploaded files from one another. +* If there is a "auth-token" field present, another command line options `--auth-token=...` is appended. The value is either the string which was submitted with the POST request, or, if the value was "generate", a refreshed, valid AuthToken which authenticates as the user of the request (why? see below). + +## Example HTML Form + +```html +<form action="/scripting" method="post" enctype="multipart/form-data"> +<input type="hidden" name="call" value="my/script.py"/> +<input type="file" name="-Oconfig-file"/> +<input type="file" name="-p1"/> +<input type="text" name="-p0" value="analyze"/> +<input type="text" name="user"/> +<input type="text" name="-Oalgorithm" value="fast"/> +<input type="submit" value="Submit"> +</form> +``` + +where the user uploads `my.conf` as `-Oconfig-file` and `my.input.tsv` as `-p1`, would result in this command line: + +``` +$SERVER_SIDE_SCRIPTING_BIN_DIR/my/script.py --config-file=.upload_files/my.conf --algorithm=fast analyze .upload_files/my.input.tsv +``` + +## CaosDB Server Response + +The CaosDB Server responds with an xml document. The root element is the usual `/Response`. If no errors occurred (which would be represented with `/Response/Error` elements) the result of the script execution is represented as a `/Response/script/` element. + +* It has a `code` attribute which contains the exit code value of the execution. +* It has `stdout` and `stderr` children which contain the dump of the stdout and stderr file of the execution environment. +* It has a `call` child which contains the command line which was executed (but without a possible `--auth-token` option and with a relative path to the executable). + +### Example XML Response + +```xml +<Response> +<script code="0"> +<call>my/script.py --config-file=.upload_files/my.conf --algorithm=fast analyze .upload_files/my.input.tsv</call> +<stdout>Result: 0.5</stdout> +<stderr>Warning: 8 Lines did not contain enough columns</stderr> +</Response> +``` + +## CaosDB Clients and Authentication Token + +A special use case for server side scripting is the automated execution of CaosDB clients. These clients need to connect to the CaosDB Server and thus need a way to authenticate themselves. +For this special case the server can pass an Authentication Token which can be used by the script to authenticate itself. If the invocation request send a particulare AuthToken in the form, this AuthToken will be passed to the script with the `--auth-token` option. Otherwise, if the `auth-token` field has "generate" as value, a fresh AuthToken is generated which belongs to the user who requested the script execution. +Thus the script is executed (and connects back to the server) as the user who called the script in the first place. + +A CaosDB client might use the python client library to connect to the server with that AuthToken. + +### Example Script + +```python +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""server_side_script.py. + +An example which implements a minimal server-side script. + +1) This script expects to find a *.txt file in the .upload_files dir which is +printed to stdout. + +2) It executes a "Count stars" query and prints the result to stdout. + +3) It will return with code 0 if everything is ok, or with any code that is +specified with the commandline option --exit +""" + +import sys +from os import listdir +from caosdb import configure_connection, execute_query + + +# parse --auth-token option and configure connection +CODE = 0 +QUERY = "COUNT stars" +for arg in sys.argv: + if arg.startswith("--auth-token="): + auth_token = arg[13:] + configure_connection(auth_token=auth_token) + if arg.startswith("--exit="): + CODE = int(arg[7:]) + if arg.startswith("--query="): + QUERY = arg[8:] + + +############################################################ +# 1 # find and print *.txt file ############################ +############################################################ + +try: + for fname in listdir(".upload_files"): + if fname.endswith(".txt"): + with open(".upload_files/{}".format(fname)) as f: + print(f.read()) +except FileNotFoundError: + pass + + +############################################################ +# 2 # query "COUNT stars" ################################## +############################################################ + +RESULT = execute_query(QUERY) +print(RESULT) + +############################################################ +# 3 ######################################################## +############################################################ + +sys.exit(CODE) +``` + diff --git a/src/doc/specification/Specification-of-the-Entity-API.md b/src/doc/specification/Specification-of-the-Entity-API.md new file mode 100644 index 0000000000000000000000000000000000000000..93b39f5333b9f80c9f4485fc00c2f827ad60792a --- /dev/null +++ b/src/doc/specification/Specification-of-the-Entity-API.md @@ -0,0 +1,191 @@ +# Specification of the Entity API +Version: 0.1.0r1 + +Author: Timm Fitschen + +Email: timm.fitschen@ds.mpg.de + +Date: 2017-12-17 + +## Introduction + +CaosDB is a database management system that stores it's data into `Entities`. An `Entity` can be thought of as the equivalent to tables, rows, columns and the tuples that fill the tables of a traditional RDBMS. Entities are not only used to store the data they also define the structure of the data. + +## Formal Definition + +An `Entity` may have + +* a `domain` +* an `id` +* a `role` +* a `name` +* a `data type` +* a `Set of Values` +* a `Set of Properties` +* a `Set of Parents` + +A `domain` contains an `Entity`. + +An `id` is an arbitrary string. + +A `role` is an arbitrary string. Especially, it may be one of the following strings: + +* `RecordType` +* `Record` +* `Relation` +* `Property` +* `File` +* `QueryTemplate` +* `Domain` +* `Unit` +* `Rule` +* `DataType` +* `Remote` + +A `name` is an arbitrary string. + +A `data type` contains an `Entity`. Note: this is not necessarily a `Data Type`. + +### Set of Values + +A `Set of Values` is a mapping from a `indices` to a finite set of `Values`. + +An `index` is an interval of non-negative integers starting with zero. + +#### Value + +A `Value` may have a `data type` and/or a `unit`. + +A `data type` is an `Entity`. Note: this is not necessarily a `Data Type`. + +A `unit` is an arbitrary string. + +### Data Type + +A `Data Type` is an `Entity` with role `DataType`. + +#### Reference Data Type + +A `Reference Data Type` is a `Data Type`. It may have a `scope`. + +A `scope` contains an `Entity`. + +#### Collection Data Type + +A `Collection Data Type` is a `Data Type`. It may have an ordered set of `elements`. + +### Record Type + +A `Record Type` is an `Entity` with role `RecordType`. + +### Record + +A `Record` is an `Entity` with role `Record`. + +### Relation + +A `Relation` is an `Entity` with role `Relation`. + +### Property + +A `Property` is an `Entity` with role `Property`. It is also refered to as `Abstract Property`. + +### File + +A `File` is an `Entity` with role `File`. + +A `File` may have + +* a `path` +* a `size` +* a `checksum` + +A `path` is an arbitrary string. + +A `size` is a non-negative integer. + +A `checksum` is an ordered pair (`method`,`result`). + +A `method` is an arbitrary string. + +A `result` is an arbitrary string. + +### QueryTemplate + +A `QueryTemplate` is an `Entity` with role `QueryTemplate`. + +### Domain + +A `Domain` is an `Entity` with role `Domain`. + +### Unit + +A `Unit` is an `Entity` with role `Unit`. + +### Rule + +A `Rule` is an `Entity` with role `Rule`. + +### Remote + +A `Remote` is an `Entity` with role `Remote`. + +### Set of Parents + +A `Set of Parents` is a set of `Parents`. + +#### Parent + +A `Parent` may contain another `Entity`. + +A `Parent` may have an `affiliation`. + +An `affiliation` may contain of the following strings: + +* `subtyping` +* `instantiation` +* `membership` +* `parthood` +* `realization` + +### Set of Properties + +A `Set of Properties` is a tripple (`index`, set of `Implemented Properties`, `Phrases`). + +An `index` is a bijective mapping from an interval of non-negative integer numbers starting with zero to the set of `Implemented Properties`. + +#### Implemented Property + +An `Implemented Property` contains another `Entity`. + +An `Implemented Property` may have an `importance`. + +An `Implemented Property` may have a `maximum cardinality`. + +An `Implemented Property` may have a `minimum cardinality`. + +An `Implemented Property` may have an `import`. + +An `importance` is an arbitrary string. It may contain of the following strings: + +* `obligatory` +* `recommended` +* `suggested` +* `fix` + +A `maximum cardinality` is a non-negative integer. + +A `minimum cardinality` is a non-negative integer. + +An `import` is an arbitrary string. It may contain of the following strings: + +* `fix` +* `none` + +#### Phrases + +`Phrases` are a mapping from the cartesian product of the `index` with itself to a `predicate`. + +A `predicate` is an arbitrary string. + + diff --git a/src/doc/specification/Specification-of-the-Message-API.md b/src/doc/specification/Specification-of-the-Message-API.md new file mode 100644 index 0000000000000000000000000000000000000000..fc08b343f67ca8c052395cb0979b02ab455e98fa --- /dev/null +++ b/src/doc/specification/Specification-of-the-Message-API.md @@ -0,0 +1,167 @@ +# Specification of the Message API +## Introduction + +API Version 0.1.0 + +A Message is a way of communication between the server and a client. The main purpose is to inform the clients about errors which occured during transactions, issue warnings when entities have a certain state or just explicitly confirm that a transaction was successful. Messages represents information that is not persistent or just the reproducible outcome of a transaction. Messages are not stored aside from logging. + +## Message Classes And Their Properties + +### Message (generic super class) + +A `Message` must be either a `Server Message` or a `Client Message`. + +A `Message` must have a `description`. A `description` is a string and a human-readable explanation of the meaning and/or purpose of the message. The description must not have leading or trailing whitespaces. The description should be kept in English. For the time being there is no mechanism to indicate that the description is written in other languages. This could be changed in later versions of this API. + +### Server Message + +A `Server Message` is a Message issued by the server. It must not be issued by clients. + +A `Server Message` may be either a `Standard Server Message` or a `Non-Standard Server Message` + +#### Standard Server Message + +A `Standard Server Message` is one of a set of predefined messages with a certain meaning. The set of these `Standard Server Messages` is maintained and documented in the Java code of the server. There should be a server resource for these definitions in order to have a always up-to-date documentation of the messages on every server. + +A `Standard Server Message` must have an `id`. An `id` is a non-empty string that uniquely identifies a standard server message. An id should consist only of ASCII compliant upper-case Latin alphabetic letters from `A` to `Z` and the underscore character `_`. +An `id` of a `Standard Server Message` must not start with the string `NSSM_`. + +A `Standard Server Message` must have a `type`. A `type` is one these strings: `Info`, `Warning`, `Error`, or `Success`. + +##### Error Message + +A `Server Message` with type `Error` is also called `Error Message` and sometimes just `Error`. An `Error Message` indicates that a request has *failed*. It informs about the reasons for that failure or the nature of the problems which occurred. The description of each error message should explain the error and indicate if and how the client can remedy the problems with her request. + +##### Warning Message + +A `Server Message` with type `Error` is also called `Warning Message` and sometime just `Warning`. A `Warning Message` indicates that certain *irregularities* occurred during the processing of the request or that the client requested something that is *not recommended but not strictly forbidden*. + +##### Info Message + +A `Server Message` with type `Info` is also called `Info Message` and sometimes just `Info`. An `Info Message` is a means to inform the client about *arbitrary events* which occurred during the processing of the request and which are *not* to be considered *erroneous* or *non-recommended*. These info messages are primarily intended to make the processing of the request more understandable for the client. Info messages are not meant to be used for debugging. + +##### Success Message + +A `Server Message` with type `Success` is also called a `Success Message`. A `Success Message` indicates the successful *state change* due to portions of a request or the whole request. A success message must not be issued if the request fails. + +#### Non-Standard Server Message + +A `Non-Standard Server Message` may be issued by any non-standard server plugin or extension. It is a placeholder for extensions to the Message API. + +A `Non-Standard Server Message` may have an `id`. An `id` is a non-empty string. It should consist only of ASCII compliant upper-case Latin alphabetic letters from `A` to `Z` and the underscore character `_`. However, the id should not be equal to any id from the set of predefined standard server messages. Furthermore, the id of a non-standard server message should start with the string `NSSM_`. + +A `Non-Standard Server Message` may have a `type`. A `type` is a non-empty string. It should consist only of ASCII compliant upper-case or lower-case Latin alphabetic letters from `a` to `z`, from `A` to `Z`, and the underscore character `_`. If the type is equal to one of the above-mentioned types, it must have the same meaning and the same effects on the request as the respective type from above. Especially, a message with type `Error` must not be issued unless the request actually fails. Likewise a `Success` must not be issued unless the request actually caused a *state change* of the server. + +### Client Message + +A `Client Message` may have an `ignore` flag. The `ignore` flag can have one of these values: `no`, `yes`, `warn`, `silent` + +A `Client Message` is a message issued by a client. It should not be issued by the server. A `Client Message` may be completely ignored by clients. A client message must not be ignored by the server. A `Client Message` which cannot be understood by the server must result in an error, unless the `ignore` flag states otherwise. + +#### Ignore Flag + +If the `ignore` flag is set to `no` the server must not ignore the client message. If the server cannot understand the client message an error must be issued. This will cause the transaction to fail. + +If the `ignore` flag is set to `yes` the server must ignore the client message. + +If the `ignore` flag is set to `warn` the server should not ignore the message. If the server cannot understand the client message, a warning must be issued. The transaction will not fail due to this warning. + +### Message Parameters + +A `Message` may have zero or more parameters. A `Message Parameter` is a a triple of a `key`, a `value`. It is intended to facilitate the processing and representation of messages by clients and the server. For example, consider an `Error Message` which states that a certain server state cannot be reached and the reason be that there is an entity with certain features. Then it is useful to refer to the entity via the +parameters. A client can now resolve the entity and just show it or generate a URI for this entity. + +A `key` is a non-empty string which should consist only of ASCII compliant lower-case Latin alphabetic letters from `a` to `z` and the minus character `-`. A `key` must be unique among the keys of the message parameters. + +A `value` is a possibly empty, arbitrary string which must not have leading or trailing white spaces. + +A `Message Parameter` may have a `type`. The `type` of a `Message Parameter` is also called a `Message Parameter Type`. A `Message Parameter Type` is a non-empty string which should consist only of ASCII compliant lower-case Latin alphabetic letters from `a` to `z` and the minus character `-`. A message parameter type may be one these string: `entity-id`, `entity-name`, `entity-cuid`, `property-index`, `parent-id`, `parent-name`. + +A `Message Parameter` with a type which begins with `entity-` is also called an `Entity Message Parameter`. The value of an `Entity Message Parameter` must refer to an entity—via its id, name, or cuid, respectively. + +A `Message Parameter` with a type which begins with `property-` is also called a `Property Message Parameter`. The value of such a parameter must refer to an entity's property. In the case of the `property-index` type the value refers to a property via a zero-based index (among the list of properties of that entity). The list of properties in question must belong to the `Message Bearer` which must in turn be an `Entity`. + +A `Message Parameter` with a type which begins with `parent-` is also called a `Parent Message Parameter`. The value of such a parameter must refer to an entity's parent via its id or name, respectively. + +### Message Bearer + +A `Message` must have a single `Message Bearer`, or, equivalently, a `Message` `belongs to` a single `Message Bearer`. The message is usually considered to carry information about the message bearer if not stated otherwise. The message's subject should be the message bearer itself, so to speak. Although, possibly indicated by a `Message Parameter` the message may be additionally or solely concerned with other things than the message bearer. Please note: The message bearer may also indicate the context of evaluation of the message parameters, e.g. when the type of the message parameter is `property-index`. + +A `Message Bearer` may be an `Entity`, a `Property`, a `Container`, a `Request`, a `Response`, or a `Transaction`. + +## Representation and Serialization + +Messages can be serialized, deserialized by the means of XML. + +### XML Representation + +A `Message` is serialized into a single XML Element Node (hereafter the *root element* with zero or more Child Nodes. + +##### Root Element Tag + +The root element's tag of a `Server Message` must be equal to its `type` if and only if the type is equal to one of the allowed types of a `Standard Server Message` (even if it is a Non-Standard Server Message). Otherwise the root tag is just 'ServerMessage'. + +```xml +<Error/><!--an Error Message--> +<Warning/><!--a Warning Message--> +<Info/><!--an Info Message--> +<Success/><!--a Success Message--> +<ServerMessage/> <!--a Non-Standard Server Message with a non-standard type--> +``` + +The root element's tag of a `Client Message` must be 'ClientMessage'. E.g. +```xml +<ClientMessage/><!--a Client Message--> +``` + +##### Root Element Attributes + +The root element must have the attributes nodes `id`, and/or `ignore` if and only if the messages have corresponding properties. The root element must have a 'type' attribute only if the message has a type property and if the type is not equal to the root element's tag. The values of the attributes must equal the corresponding properties. E.g. + +```xml +<Error id="ENTITY_DOES_NOT_EXIST" type="Error"/><!--this and the next element are equivalent--> +<Error id="ENTITY_DOES_NOT_EXIST"/> +<ServerMessage type="CustomType"/><!--has no id--> +<ServerMessage id="NSSM_MY_ID"/><!--has no type--> +``` + +or + +```xml +<ClientMessage id="CM_MY_ID" ignore="warn"/> +``` + +All other Attributes should be ignored. + +##### Description Element + +The root element must have exactly one Child Element Node with tag 'Description' if and only if the message has a `description` property. The string value of the message's description must be the first Child Text Node of the 'Description' Element. E.g. + +```xml +<ServerMessage> + <Description>This is a description.</Description> +</ServerMessage> +``` + +Please note: Any leading or trailing whitespaces of the Text Node must be stripped during the deserialization. + +All other Attributes and Child Nodes should be ignored. + +##### Parameters Element + +The root element must have exactly one Child Element Node with tag 'Parameters' if the message has at least one `parameter`. The 'Parameters' Element in turn must have a single Child Element Node for each parameter which are called `Parameter Elements`. + +A `Parameter Element` must have a tag equal to the `key` of the parameter. +It must have a `type` attribute equal to the `type` property of the parameter if and only if the parameter has a type. And it must have a first Child Text Node which is equal to the parameter's `value`. E.g. + +```xml +<ClientMessage> + <Parameters> + <param-one type="entity-name">Experiment</param-one><!--One parameter with key="param-one", value="Experiment", and type="entity-name"--> + </Parameters> +</ClientMessage> +``` + +Please note: Any leading or trailing whitespaces of the Text Node must be stripped during the deserialization. + +All other Attributes and Child Nodes below the 'Parameters' Element should be ignored. diff --git a/src/doc/specification/index.rst b/src/doc/specification/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..e9683072f555725ee8b74a0ae468c6a407e244b2 --- /dev/null +++ b/src/doc/specification/index.rst @@ -0,0 +1,22 @@ + +Specification +============= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + AbstractProperty + C-Client + Fileserver + Record + Server-side-scripting + Specification of the Entity API <Specification-of-the-Entity-API> + Authentication + Datatype + Paging + RecordType + Server side scripting <Server-side-scripting-v0.1> + Specification of the Message API <Specification-of-the-Message-API> + diff --git a/src/main/java/org/caosdb/server/CaosDBServer.java b/src/main/java/org/caosdb/server/CaosDBServer.java index c5f50444029178b4fa2b938943ad92ce04a40a60..f06c8ee9700eb94eff4cbbcb74651a264b1adee4 100644 --- a/src/main/java/org/caosdb/server/CaosDBServer.java +++ b/src/main/java/org/caosdb/server/CaosDBServer.java @@ -78,13 +78,9 @@ import org.caosdb.server.resource.Webinterface; import org.caosdb.server.resource.WebinterfaceBuildNumber; import org.caosdb.server.resource.transaction.EntityNamesResource; import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.terminal.CaosDBTerminal; -import org.caosdb.server.terminal.StatsPanel; -import org.caosdb.server.terminal.SystemErrPanel; import org.caosdb.server.transaction.ChecksumUpdater; import org.caosdb.server.utils.FileUtils; import org.caosdb.server.utils.Initialization; -import org.caosdb.server.utils.NullPrintStream; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; @@ -116,7 +112,6 @@ import org.slf4j.LoggerFactory; public class CaosDBServer extends Application { private static Logger logger = LoggerFactory.getLogger(CaosDBServer.class); - private static boolean START_GUI = true; private static Properties SERVER_PROPERTIES = null; private static Component component = null; private static ArrayList<Runnable> postShutdownHooks = new ArrayList<Runnable>(); @@ -146,7 +141,7 @@ public class CaosDBServer extends Application { public static void main(final String[] args) throws SecurityException, FileNotFoundException, IOException { try { - init(args); + parseArguments(args); initScheduler(); initServerProperties(); initTimeZone(); @@ -161,16 +156,22 @@ public class CaosDBServer extends Application { } } - private static void init(final String[] args) { - // Important change: - // Make silent the default option - START_GUI = false; + /** + * Parse the command line arguments. + * + * <ul> + * <li>"nobackend": flag to run caosdb without any backend (for testing purposes) + * <li>"insecure": flag to start only a http server (no https server) + * </ul> + * + * <p>Both flags are only available in the debug mode which is controlled by the `caosdb.debug` + * JVM Property. + * + * @param args + */ + private static void parseArguments(final String[] args) { for (final String s : args) { - if (s.equals("silent")) { - START_GUI = false; - } else if (s.equals("gui")) { - START_GUI = true; - } else if (s.equals("nobackend")) { + if (s.equals("nobackend")) { START_BACKEND = false; } else if (s.equals("insecure")) { INSECURE = true; @@ -354,32 +355,6 @@ public class CaosDBServer extends Application { } } - public static void initGUI() throws InterruptedException { - if (START_GUI) { - final CaosDBTerminal caosDBTerminal = new CaosDBTerminal(); - caosDBTerminal.setName("CaosDBTerminal"); - caosDBTerminal.start(); - - addPreShutdownHook( - new Runnable() { - - @Override - public void run() { - caosDBTerminal.shutDown(); - SystemErrPanel.close(); - } - }); - // wait until the terminal is initialized. - Thread.sleep(1000); - - // add Benchmark - StatsPanel.addStat("TransactionBenchmark", TransactionBenchmark.getRootInstance()); - } else { - logger.info("NO GUI"); - System.setOut(new NullPrintStream()); - } - } - private static void initDatatypes(final Access access) throws Exception { final RetrieveDatatypes t = new RetrieveDatatypes(); t.setAccess(access); diff --git a/src/main/java/org/caosdb/server/ServerProperties.java b/src/main/java/org/caosdb/server/ServerProperties.java index dd12add7f20de16d74b2cda907e543b7f5ddef16..3172a3e88e7790580af1b7865c024fa2b40c017b 100644 --- a/src/main/java/org/caosdb/server/ServerProperties.java +++ b/src/main/java/org/caosdb/server/ServerProperties.java @@ -117,6 +117,7 @@ public class ServerProperties extends Properties { public static final String KEY_SERVER_OWNER = "SERVER_OWNER"; public static final String KEY_SERVER_SIDE_SCRIPTING_BIN_DIR = "SERVER_SIDE_SCRIPTING_BIN_DIR"; + public static final String KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS = "SERVER_SIDE_SCRIPTING_BIN_DIRS"; public static final String KEY_SERVER_SIDE_SCRIPTING_HOME_DIR = "SERVER_SIDE_SCRIPTING_HOME_DIR"; public static final String KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR = "SERVER_SIDE_SCRIPTING_WORKING_DIR"; diff --git a/src/main/java/org/caosdb/server/accessControl/UserSources.java b/src/main/java/org/caosdb/server/accessControl/UserSources.java index 8f69c8c5c86faea903a29049c3a604121b8951ad..a7abd1405f3d566bb672befc86b853dcf2a17357 100644 --- a/src/main/java/org/caosdb/server/accessControl/UserSources.java +++ b/src/main/java/org/caosdb/server/accessControl/UserSources.java @@ -79,8 +79,18 @@ public class UserSources extends HashMap<String, UserSource> { private static UserSources instance = new UserSources(); + /** + * Check whether a user exists. + * + * @param principal - principal of the user. + * @return true iff the user identified by the given {@link Principal} exists. + */ public static boolean isUserExisting(final Principal principal) { - return instance.get(principal.getRealm()).isUserExisting(principal.getUsername()); + UserSource userSource = instance.get(principal.getRealm()); + if (userSource != null) { + return userSource.isUserExisting(principal.getUsername()); + } + return false; } private UserSources() { diff --git a/src/main/java/org/caosdb/server/database/BackendTransaction.java b/src/main/java/org/caosdb/server/database/BackendTransaction.java index 20ec5b71a7ae6f4dd908dc38145df1c41f035e8e..61aebf42a2dfd65fb9ed1c84459064e3aa274c06 100644 --- a/src/main/java/org/caosdb/server/database/BackendTransaction.java +++ b/src/main/java/org/caosdb/server/database/BackendTransaction.java @@ -59,7 +59,6 @@ import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveProp import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveQueryTemplateDefinition; import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveRole; import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveSparseEntity; -import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveTransactionHistory; import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveUser; import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveVersionHistory; import org.caosdb.server.database.backend.implementation.MySQL.MySQLRuleLoader; @@ -116,7 +115,6 @@ import org.caosdb.server.database.backend.interfaces.RetrievePropertiesImpl; import org.caosdb.server.database.backend.interfaces.RetrieveQueryTemplateDefinitionImpl; import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl; import org.caosdb.server.database.backend.interfaces.RetrieveSparseEntityImpl; -import org.caosdb.server.database.backend.interfaces.RetrieveTransactionHistoryImpl; import org.caosdb.server.database.backend.interfaces.RetrieveUserImpl; import org.caosdb.server.database.backend.interfaces.RetrieveVersionHistoryImpl; import org.caosdb.server.database.backend.interfaces.RuleLoaderImpl; @@ -131,6 +129,8 @@ import org.caosdb.server.database.backend.interfaces.UpdateUserImpl; import org.caosdb.server.database.backend.interfaces.UpdateUserRolesImpl; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.TransactionBenchmark; +import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.RetrieveEntity; import org.caosdb.server.utils.UndoHandler; import org.caosdb.server.utils.Undoable; @@ -177,7 +177,6 @@ public abstract class BackendTransaction implements Undoable { setImpl(RetrieveAllImpl.class, MySQLRetrieveAll.class); setImpl(RegisterSubDomainImpl.class, MySQLRegisterSubDomain.class); setImpl(RetrieveDatatypesImpl.class, MySQLRetrieveDatatypes.class); - setImpl(RetrieveTransactionHistoryImpl.class, MySQLRetrieveTransactionHistory.class); setImpl(RetrieveUserImpl.class, MySQLRetrieveUser.class); setImpl(RetrieveParentsImpl.class, MySQLRetrieveParents.class); setImpl(GetFileRecordByPathImpl.class, MySQLGetFileRecordByPath.class); @@ -298,4 +297,9 @@ public abstract class BackendTransaction implements Undoable { this.benchmark.addMeasurement(o, time); } } + + /** Return the type of transaction of an entity, e.g. "Retrieve" for a {@link RetrieveEntity}. */ + public String getTransactionType(EntityInterface e) { + return e.getClass().getSimpleName().replace("Entity", ""); + } } diff --git a/src/main/java/org/caosdb/server/database/DatabaseAccessManager.java b/src/main/java/org/caosdb/server/database/DatabaseAccessManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f12c78e31bf28799b8c8ead1c9ff311a65bb01ef --- /dev/null +++ b/src/main/java/org/caosdb/server/database/DatabaseAccessManager.java @@ -0,0 +1,329 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.database; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import org.caosdb.server.database.access.Access; +import org.caosdb.server.database.access.AccessControlAccess; +import org.caosdb.server.database.access.InfoAccess; +import org.caosdb.server.database.access.InitAccess; +import org.caosdb.server.database.access.TransactionAccess; +import org.caosdb.server.transaction.AccessControlTransaction; +import org.caosdb.server.transaction.TransactionInterface; +import org.caosdb.server.transaction.WriteTransactionInterface; +import org.caosdb.server.utils.Info; +import org.caosdb.server.utils.Initialization; +import org.caosdb.server.utils.Releasable; + +/** + * Acquire and release read access. {@link DatabaseAccessManager} uses this class for managing + * access to the back-end during processing updates, inserts, deletions and retrievals. Read access + * will be granted immediately for every thread that requests it, unless a thread requests that read + * access be blocked (usually by requesting write access). If this happens, all threads that already + * have read access will proceed and release their read access as usual but no NEW read permits will + * be granted. + * + * <p>This is a blockable {@link Semaphore}. Any number of threads may acquire a permit unless it is + * blocked. The blocking thread (requesting a {@link WriteAccessLock}) waits until all threads have + * released their permits. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +class ReadAccessSemaphore extends Semaphore implements Releasable { + + private static final long serialVersionUID = 4384921156838881337L; + private AtomicInteger acquired = new AtomicInteger(0); // how many threads have read access + Semaphore writersBlock = + new Semaphore(1, true); // This semaphore is blocked as long as there are any + // unreleased read permits. + + public ReadAccessSemaphore() { + // this is a fair semaphore with no initial permit. + super(1, true); + } + + /** + * Acquire a read access permit if and only if it has not been blocked via block(). If read access + * is currently blocked, the thread waits until the unblock() method is invoked by any thread. + */ + @Override + public void acquire() throws InterruptedException { + super.acquire(); // Protect the next few lines + if (this.acquired.getAndIncrement() == 0) { + this.writersBlock.acquire(); + } + super.release(); + } + + /** + * Release a read access permit. + * + * <p>If this is the last remaining acquired permit, also release the general writersBlock. + */ + @Override + public void release() { + if (this.acquired.decrementAndGet() == 0) { // Last permit: release + if (this.writersBlock.availablePermits() <= 0) { + this.writersBlock.release(); + } + } + } + + /** + * Acquire the permit to block further read access, if no thread currently has acquired read + * access and no thread currently has a block. + * + * <p>Consequences of calling this method are: + * + * <ul> + * <li>Further read access permits are blocked for any thread. + * <li>Every thread that has a read access already can proceed. The current thread waits until + * all threads have released their read access. + * </ul> + * + * <p>If another thread has invoked this method before, the current thread waits until the + * unblock() method is called. + * + * @throws InterruptedException + */ + public void block() throws InterruptedException { + super.reducePermits(1); + this.writersBlock.acquire(); + } + + /** + * Unblock read access. + * + * <p>This method releases the writersBlock introduced by calling the block() method. + */ + public void unblock() { + if (this.writersBlock.availablePermits() <= 0) { + this.writersBlock.release(); + } + super.release(); + } + + public int waitingAquireAccess() { + return getQueueLength(); + } +} + +/** + * Acquire and release write access. DatabaseAccessManager uses this class for managing access to + * entities while processing updates, inserts, deletions and retrievals. Write access will be + * granted to one and only one thread if no other thread already holds read or write access permits. + * The write access seat has to be reserved before acquiring the lock. + * + * <p>The flow is as follows: + * + * <p> + * + * <pre> + * No new read + * access possible, + * Read access Read access wait for running read + * possible possible threads to fininish + * + * +------+ +----------+ +----------+ + * +--> | no | ----> | reserved | ----> | acquired | --+ + * | | Lock | +----------+ +----------+ | + * | +------+ \ / | + * | (1 seat together) | + * +-----------------------------------------------------+ + * release() + * </pre> + * + * @author Timm Fitschen + */ +class WriteAccessLock extends ReentrantLock implements Releasable { + + private static final long serialVersionUID = 833147084787201103L; + private ReadAccessSemaphore readSem = null; + private Thread reservedBy = null; + + public WriteAccessLock(final ReadAccessSemaphore readSem) { + super(); + this.readSem = readSem; + } + + /** + * Reserve the seat for the next write access. While a write access is reserved but not yet + * acquired, all read access may still be granted. When a write access has been already granted or + * another reservation is active, the thread waits until the write access has been released. + * + * @throws InterruptedException + */ + public void reserve() throws InterruptedException { + super.lock(); + this.reservedBy = Thread.currentThread(); + } + + /** + * Lock the write access seat. This method returns once all current read permits have been + * released. + */ + @Override + public void lockInterruptibly() throws InterruptedException { + if (!super.isHeldByCurrentThread()) { + super.lock(); + } + this.readSem.block(); // Wait until all current read permits have been released. + } + + @Override + public void unlock() { + if (super.isHeldByCurrentThread()) { + this.readSem.unblock(); + this.reservedBy = null; + super.unlock(); + } + } + + @Override + public void release() { + unlock(); + } + + public Thread whoHasReservedAccess() { + return this.reservedBy; + } +} + +/** + * Manages the read and write access to the database. + * + * @author tf + */ +public class DatabaseAccessManager { + + private DatabaseAccessManager() {} + + private static final DatabaseAccessManager instance = new DatabaseAccessManager(); + private final ReadAccessSemaphore readAccess = new ReadAccessSemaphore(); + private final WriteAccessLock writeAccess = new WriteAccessLock(this.readAccess); + + public static DatabaseAccessManager getInstance() { + return instance; + } + + /** + * Return the thread which has successfully reserved write access. This is the thread which will + * be the next to actually acquire write access. + * + * @return + */ + public static Thread whoHasReservedWriteAccess() { + return instance.writeAccess.whoHasReservedAccess(); + } + + /** + * Acquire read access. This method returns the Access object as soon as there are no active write + * permits. + * + * <p>The returned Access object can be used to read in the data base back-end. + * + * <p>Read access can be acquired parallel to other threads having read access or while another + * thread has <em>reserved</em> write access. As soon as any thread has requested to + * <em>acquire</em> write access, all other threads have to wait. + * + * @param t the {@link TransactionInterface} which requests the read access + * @return {@link Access} object which holds and abstract away all connection details. + * @throws InterruptedException + */ + public Access acquireReadAccess(final TransactionInterface t) throws InterruptedException { + this.readAccess.acquire(); + return new TransactionAccess(t, this.readAccess); + } + + /** + * Reserve write access. This method returns the Access object as soon as there is no other + * reserved or acquired write access. + * + * <p>The returned Access object can be used to read in the data base back-end. + * + * <p>The reservation has no effect on granting of read access permits, but only one thread may at + * any time reserve or acquire write access. + * + * @param wt - the {@link WriteTransactionInterface} which request the reservation of the write + * access. + * @return {@link Access} object which holds and abstract away all connection details. + * @throws InterruptedException + */ + public Access reserveWriteAccess(final WriteTransactionInterface wt) throws InterruptedException { + this.writeAccess.reserve(); + return new TransactionAccess(wt, this.writeAccess); + } + + /** + * Acquire write access. This method returns the Access object as soon as all already acquired + * read access permits have been released. When the write access is acquired, no other access can + * be acquired (read or write). + * + * <p>The returned Access object can be used to read and write in the data base back-end. + * + * @param wt - the {@link WriteTransactionInterface} which request the acquisition of the write + * access. + * @return {@link Access} object which holds and abstract away all connection details. + * @throws InterruptedException + */ + public Access acquireWriteAccess(final WriteTransactionInterface wt) throws InterruptedException { + this.writeAccess.lockInterruptibly(); + return wt.getAccess(); + } + + /** + * Special access to be used by {@link Info}. + * + * @param i + * @return + */ + public static Access getInfoAccess(final Info i) { + return new InfoAccess(i); + } + + /** + * Special access to be used during the {@link Initialization} of the caosdb server. + * + * @param initialization + * @return + */ + public static Access getInitAccess(final Initialization initialization) { + return new InitAccess(initialization); + } + + /** + * Special access to be used for {@link AccessControlTransaction}s, mainly authentication and + * authorization. + * + * @param t + * @return + */ + public static Access getAccountAccess(final AccessControlTransaction t) { + return new AccessControlAccess(t); + } +} diff --git a/src/main/java/org/caosdb/server/database/DatabaseMonitor.java b/src/main/java/org/caosdb/server/database/DatabaseMonitor.java deleted file mode 100644 index ed98d1d6d88e3e797ce022c0880640d9a9128a0d..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/database/DatabaseMonitor.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.concurrent.Semaphore; -import java.util.concurrent.locks.ReentrantLock; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.access.AccessControlAccess; -import org.caosdb.server.database.access.InfoAccess; -import org.caosdb.server.database.access.InitAccess; -import org.caosdb.server.database.access.TransactionAccess; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.transaction.AccessControlTransaction; -import org.caosdb.server.transaction.TransactionInterface; -import org.caosdb.server.transaction.WriteTransaction; -import org.caosdb.server.utils.Info; -import org.caosdb.server.utils.Initialization; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; -import org.caosdb.server.utils.Releasable; - -/** - * Acquire and release weak access. DatabaseMonitor uses this class for managing access to entities - * during processing updates, inserts, deletions and retrievals. Weak access will be granted - * immediately for every thread that requests it, unless one or more threads requested for blocking - * the weak access (usually due to requesting for strong access). If this happens, all threads that - * already have weak access will proceed and release their weak access as usual but no NEW permits - * will be granted. - * - * @author Timm Fitschen - */ -class WeakAccessSemaphore extends Semaphore implements Observable, Releasable { - - private static final long serialVersionUID = -262226321839837533L; - private int acquired = 0; // how many thread have weak access - Semaphore block = new Semaphore(1, true); - - public WeakAccessSemaphore() { - // this is a fair semaphore with no initial permit. - super(1, true); - } - - /** - * Acquires a weak access permit if and only if it has not been blocked via block(). If the - * WeakAccess is currently blocked, the thread wait until the unblock() method is invoked by any - * thread. - */ - @Override - public void acquire() throws InterruptedException { - super.acquire(); - if (this.acquired == 0) { - this.block.acquire(); - } - this.acquired++; - notifyObservers(null); - super.release(); - } - - /** Releases a weak access permit. */ - @Override - public void release() { - this.acquired--; - notifyObservers(null); - if (this.acquired <= 0) { - this.acquired = 0; - if (this.block.availablePermits() <= 0) { - this.block.release(); - } - } - } - - /** - * Acquires the permit of a block of WeakAccess if no thread currently has a WeakAccess and no - * thread currently has a block. I.e. it blocks the further permission of weak access for any - * thread. Every thread that has a weak access yet can proceed. The current thread waits until any - * thread has released its weak access. If another thread has invoked this method yet the current - * thread waits until the unblock() method is called. - * - * @throws InterruptedException - */ - public void block() throws InterruptedException { - super.reducePermits(1); - this.block.acquire(); - } - - /** - * Unblock WeakAccess. - * - * @throws InterruptedException - */ - public void unblock() { - if (this.block.availablePermits() <= 0) { - this.block.release(); - } - super.release(); - } - - public int waitingAquireAccess() { - return getQueueLength(); - } - - public int acquiredAccess() { - return this.acquired; - } - - LinkedList<Observer> observers = new LinkedList<Observer>(); - - @Override - public boolean acceptObserver(final Observer o) { - return this.observers.add(o); - } - - @Override - public void notifyObservers(final String e) { - final Iterator<Observer> it = this.observers.iterator(); - while (it.hasNext()) { - final Observer o = it.next(); - if (!o.notifyObserver(e, this)) { - it.remove(); - } - } - } -} - -/** - * Acquire and release strong access. DatabaseMonitor uses this class for managing access to - * entities during processing updates, inserts, deletions and retrievals. Strong access will be - * granted to one and only one thread if no other thread yet holds weak or strong access permits. - * The strong access has to be allocated before requesting it to be permitted. See below. - * - * @author Timm Fitschen - */ -class StrongAccessLock extends ReentrantLock implements Observable, Releasable { - - private static final long serialVersionUID = -262226321839837533L; - private WeakAccessSemaphore wa = null; - private Thread allocator = null; - private Thread acquirer = null; - - public StrongAccessLock(final WeakAccessSemaphore wa) { - super(); - this.wa = wa; - } - - /** - * Allocates the strong access permits. While a strong access is allocated but not yet acquired - * any weak access may still be granted. When a strong access is yet granted or another allocation - * is still active, the thread waits until the strong access has been released. - * - * @throws InterruptedException - */ - public void allocate() throws InterruptedException { - super.lock(); - this.allocator = Thread.currentThread(); - notifyObservers(null); - } - - @Override - public void lockInterruptibly() throws InterruptedException { - if (!super.isHeldByCurrentThread()) { - super.lock(); - } - this.acquirer = Thread.currentThread(); - notifyObservers(null); - this.wa.block(); - } - - @Override - public void unlock() { - if (super.isHeldByCurrentThread()) { - this.wa.unblock(); - this.allocator = null; - this.acquirer = null; - notifyObservers(null); - super.unlock(); - } - } - - @Override - public void release() { - unlock(); - } - - LinkedList<Observer> observers = new LinkedList<Observer>(); - - @Override - public boolean acceptObserver(final Observer o) { - return this.observers.add(o); - } - - @Override - public void notifyObservers(final String e) { - final Iterator<Observer> it = this.observers.iterator(); - while (it.hasNext()) { - final Observer o = it.next(); - if (!o.notifyObserver(e, this)) { - it.remove(); - } - } - } - - public Thread whoHasAllocatedAccess() { - return this.allocator; - } - - public Thread whoHasAcquiredAccess() { - return this.acquirer; - } - - public int waitingAllocateAccess() { - return getQueueLength(); - } -} - -/** - * Manages the read and write access to the database. - * - * @author tf - */ -public class DatabaseMonitor { - - private DatabaseMonitor() {} - - private static final DatabaseMonitor instance = new DatabaseMonitor(); - private final WeakAccessSemaphore wa = new WeakAccessSemaphore(); - private final StrongAccessLock sa = new StrongAccessLock(this.wa); - - public static DatabaseMonitor getInstance() { - return instance; - } - - public void acquireWeakAccess() throws InterruptedException { - this.wa.acquire(); - } - - public void releaseWeakAccess() { - this.wa.release(); - } - - public void allocateStrongAccess() throws InterruptedException { - this.sa.allocate(); - } - - public void acquireStrongAccess() throws InterruptedException { - this.sa.lockInterruptibly(); - } - - public void releaseStrongAccess() { - this.sa.unlock(); - } - - public static final void acceptWeakAccessObserver(final Observer o) { - instance.wa.acceptObserver(o); - } - - public static final void acceptStrongAccessObserver(final Observer o) { - instance.sa.acceptObserver(o); - } - - public static int waitingAllocateStrongAccess() { - return instance.sa.waitingAllocateAccess(); - } - - public static int waitingAcquireWeakAccess() { - return instance.wa.waitingAquireAccess(); - } - - public static int acquiredWeakAccess() { - return instance.wa.acquiredAccess(); - } - - public static Thread whoHasAllocatedStrongAccess() { - return instance.sa.whoHasAllocatedAccess(); - } - - public static Thread whoHasAcquiredStrongAccess() { - return instance.sa.whoHasAcquiredAccess(); - } - - public Access acquiredWeakAccess(final TransactionInterface t) { - acquiredWeakAccess(); - return new TransactionAccess(t, this.wa); - } - - public Access allocateStrongAccess(final WriteTransaction<? extends TransactionContainer> wt) - throws InterruptedException { - allocateStrongAccess(); - return new TransactionAccess(wt, this.sa); - } - - public Access acquireStrongAccess(final WriteTransaction<? extends TransactionContainer> wt) - throws InterruptedException { - acquireStrongAccess(); - return wt.getAccess(); - } - - public static Access getInfoAccess(final Info i) { - return new InfoAccess(i); - } - - public static Access getInitAccess(final Initialization initialization) { - return new InitAccess(initialization); - } - - public static Access getAccountAccess(final AccessControlTransaction t) { - return new AccessControlAccess(t); - } -} diff --git a/src/main/java/org/caosdb/server/database/DatabaseUtils.java b/src/main/java/org/caosdb/server/database/DatabaseUtils.java index c985c1b9bc62270ccf6c5904b1c7f0147bb20de0..4f6a58a4fc50fa91170fad6139384bccb413fa81 100644 --- a/src/main/java/org/caosdb/server/database/DatabaseUtils.java +++ b/src/main/java/org/caosdb/server/database/DatabaseUtils.java @@ -216,10 +216,7 @@ public class DatabaseUtils { ret.fileSize = rs.getLong("FileSize"); ret.fileHash = bytes2UTF8(rs.getBytes("FileHash")); - ret.version = bytes2UTF8(rs.getBytes("Version")); - ret.versionSeconds = rs.getLong("VersionSeconds"); - ret.versionNanos = rs.getInt("VersionNanos"); - + ret.versionId = bytes2UTF8(rs.getBytes("Version")); return ret; } @@ -258,7 +255,8 @@ public class DatabaseUtils { } } - private static void replace(final Property p, final HashMap<Integer, Property> domainMap) { + private static void replace( + final Property p, final HashMap<Integer, Property> domainMap, boolean isHead) { // ... find the corresponding domain and replace it ReferenceValue ref; try { @@ -267,6 +265,17 @@ public class DatabaseUtils { throw new RuntimeException("This should never happen."); } final EntityInterface replacement = domainMap.get((ref.getId())); + if (replacement == null) { + if (isHead) { + throw new NullPointerException("Replacement was null"); + } + // entity has been deleted (we are processing properties of an old entity version) + p.setValue(null); + p.addWarning( + new Message( + "The referenced entity has been deleted in the mean time and is not longer available.")); + return; + } if (replacement.isDescOverride()) { p.setDescOverride(true); p.setDescription(replacement.getDescription()); @@ -314,15 +323,18 @@ public class DatabaseUtils { } // loop over all properties + boolean isHead = + e.getVersion().getSuccessors() == null || e.getVersion().getSuccessors().isEmpty(); for (final Property p : protoProperties) { + // if this is a replacement if (p.getStatementStatus() == StatementStatus.REPLACEMENT) { - replace(p, domainMap); + replace(p, domainMap, isHead); } for (final Property subP : p.getProperties()) { if (subP.getStatementStatus() == StatementStatus.REPLACEMENT) { - replace(subP, domainMap); + replace(subP, domainMap, isHead); } } diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java index 2ad36235bd16e1cecde9908d334f643ebc127e54..b1f445e2b8ad2d79b756e1dd662296d8e6c9a49c 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java @@ -55,7 +55,7 @@ public class MySQLHelper implements DBHelper { * * <p>In the database, this adds a row to the transaction table with SRID, user and timestamp. */ - public void initTransaction(Connection connection, WriteTransaction<?> transaction) + public void initTransaction(Connection connection, WriteTransaction transaction) throws SQLException { try (CallableStatement call = connection.prepareCall("CALL set_transaction(?,?,?,?,?)")) { @@ -89,7 +89,7 @@ public class MySQLHelper implements DBHelper { } else if (transaction instanceof WriteTransaction) { connection.setReadOnly(false); connection.setAutoCommit(false); - initTransaction(connection, (WriteTransaction<?>) transaction); + initTransaction(connection, (WriteTransaction) transaction); } else { connection.setReadOnly(false); connection.setAutoCommit(true); diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java index 1f1f32c69a260d374ea930ba4e1be63e34cf9ca9..1b945acf566db938f12847f758636dbfddeac3d1 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java @@ -57,7 +57,7 @@ public class MySQLInsertSparseEntity extends MySQLTransaction implements InsertS try (final ResultSet rs = insertEntityStmt.executeQuery()) { if (rs.next()) { entity.id = rs.getInt("EntityID"); - entity.version = DatabaseUtils.bytes2UTF8(rs.getBytes("Version")); + entity.versionId = DatabaseUtils.bytes2UTF8(rs.getBytes("Version")); } else { throw new TransactionException("Didn't get new EntityID back."); } diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveTransactionHistory.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveTransactionHistory.java deleted file mode 100644 index 3273f49aa85776c4fd323ed4c027f4d1fcd3fbb0..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveTransactionHistory.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database.backend.implementation.MySQL; - -import static org.caosdb.server.database.DatabaseUtils.bytes2UTF8; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.interfaces.RetrieveTransactionHistoryImpl; -import org.caosdb.server.database.exceptions.TransactionException; -import org.caosdb.server.database.proto.ProtoTransactionLogMessage; - -public class MySQLRetrieveTransactionHistory extends MySQLTransaction - implements RetrieveTransactionHistoryImpl { - - public MySQLRetrieveTransactionHistory(final Access access) { - super(access); - } - - public static final String STMT_RETRIEVE_HISTORY = - "SELECT transaction, realm, username, seconds, nanos FROM transaction_log WHERE entity_id=? "; - - @Override - public ArrayList<ProtoTransactionLogMessage> execute(final Integer id) - throws TransactionException { - try { - final PreparedStatement stmt = prepareStatement(STMT_RETRIEVE_HISTORY); - - final ArrayList<ProtoTransactionLogMessage> ret = new ArrayList<ProtoTransactionLogMessage>(); - stmt.setInt(1, id); - final ResultSet rs = stmt.executeQuery(); - try { - while (rs.next()) { - final String transaction = bytes2UTF8(rs.getBytes("transaction")); - final String realm = bytes2UTF8(rs.getBytes("realm")); - final String username = bytes2UTF8(rs.getBytes("username")); - final Integer seconds = rs.getInt("seconds"); - final Integer nanos = rs.getInt("nanos"); - - ret.add(new ProtoTransactionLogMessage(transaction, realm, username, seconds, nanos)); - } - return ret; - } finally { - rs.close(); - } - } catch (final SQLException e) { - throw new TransactionException(e); - } catch (final ConnectionException e) { - throw new TransactionException(e); - } - } -} diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java index a3c87ae6e97d3b26a99126b61d1d29db1ef6206a..7ecfbfb7ac70a4a5ec248eb0e2be2c590f0676aa 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java @@ -61,12 +61,16 @@ public class MySQLRetrieveVersionHistory extends MySQLTransaction String parentId = DatabaseUtils.bytes2UTF8(rs.getBytes("parent")); Long childSeconds = rs.getLong("child_seconds"); Integer childNanos = rs.getInt("child_nanos"); + String childUsername = DatabaseUtils.bytes2UTF8(rs.getBytes("child_username")); + String childRealm = DatabaseUtils.bytes2UTF8(rs.getBytes("child_realm")); VersionHistoryItem v = result.get(childId); if (v == null) { v = new VersionHistoryItem(); v.id = childId; v.seconds = childSeconds; v.nanos = childNanos; + v.username = childUsername; + v.realm = childRealm; result.put(childId, v); } diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java index 18b59ef9121bd2f488efcb657a3bf158b6e14308..1c8039253b4c5e74a8054dfa937fec23c55440e8 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java @@ -80,7 +80,7 @@ public class MySQLUpdateSparseEntity extends MySQLTransaction implements UpdateS ResultSet rs = updateEntityStmt.executeQuery(); if (rs.next()) { - spe.version = DatabaseUtils.bytes2UTF8(rs.getBytes("Version")); + spe.versionId = DatabaseUtils.bytes2UTF8(rs.getBytes("Version")); } } catch (final SQLIntegrityConstraintViolationException e) { diff --git a/src/main/java/org/caosdb/server/database/backend/interfaces/RetrieveTransactionHistoryImpl.java b/src/main/java/org/caosdb/server/database/backend/interfaces/RetrieveTransactionHistoryImpl.java deleted file mode 100644 index b02e4d9347e572941ab31e3a1a371470e1d8ee5c..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/database/backend/interfaces/RetrieveTransactionHistoryImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database.backend.interfaces; - -import java.util.ArrayList; -import org.caosdb.server.database.exceptions.TransactionException; -import org.caosdb.server.database.proto.ProtoTransactionLogMessage; - -public interface RetrieveTransactionHistoryImpl extends BackendTransactionImpl { - - public ArrayList<ProtoTransactionLogMessage> execute(Integer id) throws TransactionException; -} diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityTransaction.java similarity index 92% rename from src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityTransaction.java index 7f3d3435e1d4913d99ea0c9b7b064da85e12ac02..f8ebcd2af6ff73dd11f1b845d7e5aef58ff925f3 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityTransaction.java @@ -27,11 +27,11 @@ import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; -public class DeleteEntity extends BackendTransaction { +public class DeleteEntityTransaction extends BackendTransaction { private final TransactionContainer container; - public DeleteEntity(final TransactionContainer container) { + public DeleteEntityTransaction(final TransactionContainer container) { this.container = container; } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityTransaction.java similarity index 94% rename from src/main/java/org/caosdb/server/database/backend/transaction/InsertEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityTransaction.java index 42439aa54e06e2556cb7b814f1c9056afb35f9d7..9c86ba33bf47b8051bc365bb32a6f36d0342ff92 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityTransaction.java @@ -27,11 +27,11 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.utils.EntityStatus; -public class InsertEntity extends BackendTransaction { +public class InsertEntityTransaction extends BackendTransaction { private final TransactionContainer container; - public InsertEntity(final TransactionContainer container) { + public InsertEntityTransaction(final TransactionContainer container) { this.container = container; } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java index 1800168ebd2cd6ef4180e23046f95066597086e5..22720f836e5cbd4912f3aedccd25398e80a19324 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java @@ -69,6 +69,7 @@ public class InsertSparseEntity extends BackendTransaction { public void cleanUp() {} }); this.entity.setId(e.id); - this.entity.setVersion(new Version(e.version)); + this.entity.setVersion(new Version(e.versionId)); + this.entity.getVersion().setHead(true); } } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java index db7393b04f0afe3c15622dfb750cf31d42050521..9b798de4279de8034168c4ecfbc1738c70370a65 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java @@ -30,22 +30,24 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.utils.EntityStatus; +/** + * Record the current transaction in the entities transaction history. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class InsertTransactionHistory extends BackendTransaction { private final TransactionContainer container; - private final String transaction; private final UTCDateTime datetime; private final String user; private final String realm; public InsertTransactionHistory( final TransactionContainer container, - final String transaction, final String realm, final String user, final UTCDateTime timestamp) { this.container = container; - this.transaction = transaction; this.user = user; this.datetime = timestamp; this.realm = realm; @@ -60,7 +62,7 @@ public class InsertTransactionHistory extends BackendTransaction { || e.getEntityStatus() == EntityStatus.VALID) { t.execute( - this.transaction, + getTransactionType(e), this.realm, this.user, this.datetime.getUTCSeconds(), diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/IsSubType.java b/src/main/java/org/caosdb/server/database/backend/transaction/IsSubType.java index d75fb2c2453613c50915678d81f53f696589afd4..683903f396a186c8243b6bba20c0d19e29a61688 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/IsSubType.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/IsSubType.java @@ -29,19 +29,36 @@ import org.caosdb.server.database.exceptions.TransactionException; public class IsSubType extends BackendTransaction { private final Integer child; - private final Integer parent; + private Integer parent = null; + private String parentName = null; public IsSubType(final Integer child, final Integer parent) { this.child = child; this.parent = parent; } - private boolean isSubType; + public IsSubType(Integer child, String parent) { + this.parentName = parent; + this.child = child; + } + + private Boolean isSubType = null; @Override public void execute() throws TransactionException { final IsSubTypeImpl t = getImplementation(IsSubTypeImpl.class); - this.isSubType = t.execute(this.child, this.parent); + + if (this.parent == null) { + this.isSubType = false; + for (Integer parent : execute(new GetIDByName(parentName, false)).getList()) { + this.isSubType = t.execute(this.child, parent); + if (this.isSubType) { + return; + } + } + } else { + this.isSubType = t.execute(this.child, this.parent); + } } public boolean isSubType() { diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTransaction.java similarity index 51% rename from src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTransaction.java index 2601367b7312331a981d2b352040962ac3a98a43..6bac0ff187fe2a08d2d0c1c7450ac954bc023d5e 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTransaction.java @@ -3,9 +3,9 @@ * This file is a part of the CaosDB Project. * * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020-2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -27,7 +27,9 @@ package org.caosdb.server.database.backend.transaction; import java.util.LinkedList; import java.util.List; import org.caosdb.server.database.BackendTransaction; -import org.caosdb.server.datatype.ReferenceDatatype; +import org.caosdb.server.database.exceptions.EntityDoesNotExistException; +import org.caosdb.server.datatype.CollectionValue; +import org.caosdb.server.datatype.IndexedSingleValue; import org.caosdb.server.datatype.ReferenceValue; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; @@ -51,21 +53,21 @@ import org.caosdb.server.utils.EntityStatus; * * @author Timm Fitschen <t.fitschen@indiscale.com> */ -public class RetrieveFullEntity extends BackendTransaction { +public class RetrieveFullEntityTransaction extends BackendTransaction { private final Container<? extends EntityInterface> container; - public RetrieveFullEntity(final EntityInterface entity) { + public RetrieveFullEntityTransaction(final EntityInterface entity) { final Container<EntityInterface> c = new Container<>(); c.add(entity); this.container = c; } - public RetrieveFullEntity(final Container<? extends EntityInterface> container) { + public RetrieveFullEntityTransaction(final Container<? extends EntityInterface> container) { this.container = container; } - public RetrieveFullEntity(Integer id) { + public RetrieveFullEntityTransaction(Integer id) { this(new RetrieveEntity(id)); } @@ -102,12 +104,12 @@ public class RetrieveFullEntity extends BackendTransaction { execute(new RetrieveSparseEntity(e)); if (e.getEntityStatus() == EntityStatus.VALID) { + execute(new RetrieveVersionInfo(e)); if (e.getRole() == Role.QueryTemplate) { execute(new RetrieveQueryTemplateDefinition(e)); } execute(new RetrieveParents(e)); execute(new RetrieveProperties(e)); - execute(new RetrieveVersionInfo(e)); // recursion! retrieveSubEntities calls retrieveFull sometimes, but with reduced selectors. if (selections != null && !selections.isEmpty()) { @@ -116,6 +118,53 @@ public class RetrieveFullEntity extends BackendTransaction { } } + /** + * Recursively resolve the reference values of the list of reference property `p` (but only the + * selected sub-properties). + */ + private void resolveReferenceListProperty( + Property p, List<Selection> selections, String propertyName) { + try { + p.parseValue(); + } catch (Message m) { + p.addError(m); + } + + CollectionValue values = (CollectionValue) p.getValue(); + for (IndexedSingleValue sv : values) { + resolveReferenceValue((ReferenceValue) sv.getWrapped(), selections, propertyName); + } + } + + /** + * Recursively resolve the reference values of the reference property `p` (but only the selected + * sub-properties). + */ + private void resolveReferenceProperty( + Property p, List<Selection> selections, String propertyName) { + try { + p.parseValue(); + } catch (Message m) { + p.addError(m); + } + + resolveReferenceValue((ReferenceValue) p.getValue(), selections, propertyName); + } + + /** + * Recursively resolve the reference value. + * + * <p>To be called by {@link #resolveReferenceListProperty(Property, List, String)} and {@link + * #resolveReferenceProperty(Property, List, String)}. + */ + private void resolveReferenceValue( + ReferenceValue value, List<Selection> selections, String propertyName) { + RetrieveEntity ref = new RetrieveEntity(value.getId()); + // recursion! (Only for the matching selections) + retrieveFullEntity(ref, getSubSelects(selections, propertyName)); + value.setEntity(ref, true); + } + /** * Retrieve the Entities which match the selections and are referenced by the Entity 'e'. * @@ -124,27 +173,55 @@ public class RetrieveFullEntity extends BackendTransaction { */ public void retrieveSubEntities(EntityInterface e, List<Selection> selections) { for (final Selection s : selections) { - if (s.getSubselection() != null) { - - String propertyName = s.getSelector(); - - // Find matching (i.e. referencing) Properties - for (Property p : e.getProperties()) { - // get reference properties by name. - if (propertyName.equalsIgnoreCase(p.getName()) - && p.getDatatype() instanceof ReferenceDatatype) { - if (p.getValue() != null) { + String propertyName = s.getSelector(); + for (Property p : e.getProperties()) { + if (s.getSubselection() != null) { + // The presence of sub-selections means that the properties are + // expected to be references (list or plain). + if (p.getValue() != null) { + if (propertyName.equalsIgnoreCase(p.getName())) { + if (p.isReference()) { + // handle (single) reference properties with matching name... + resolveReferenceProperty(p, selections, propertyName); + continue; + } else if (p.isReferenceList()) { + // handle (list) reference properties with matching name... + resolveReferenceListProperty(p, selections, propertyName); + continue; + } + } else { try { - p.parseValue(); - } catch (Message m) { - p.addError(m); + boolean isSubtype = execute(new IsSubType(p.getId(), propertyName)).isSubType(); + if (isSubtype) { + // ... handle reference properties that are a subtype of `propertyName`. + if (p.getValue() != null) { + if (p.isReference()) { + resolveReferenceProperty(p, selections, propertyName); + } else if (p.isReferenceList()) { + resolveReferenceListProperty(p, selections, propertyName); + } + } + + // the name is set the the super-types name! Otherwise, clients + // wouldn't know what this property is supposed to be. + p.setName(propertyName); + } + } catch (EntityDoesNotExistException exc) { + // unknown parent name. } - - ReferenceValue value = (ReferenceValue) p.getValue(); - RetrieveEntity ref = new RetrieveEntity(value.getId()); - // recursion! (Only for the matching selections) - retrieveFullEntity(ref, getSubSelects(selections, propertyName)); - value.setEntity(ref, true); + } + } + } else { + // no subselections - no need to resolve any references... + if (!propertyName.equalsIgnoreCase(p.getName())) { + // ... we only need to cover property sub-typing + try { + boolean isSubtype = execute(new IsSubType(p.getId(), propertyName)).isSubType(); + if (isSubtype) { + p.setName(propertyName); + } + } catch (EntityDoesNotExistException exc) { + // unknown parent name. } } } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveTransactionHistory.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveTransactionHistory.java deleted file mode 100644 index 632ce20cfe71d0e116bbe3987a60c45b11085eec..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveTransactionHistory.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2019 IndiScale GmbH - * Copyright (C) 2019 Timm Fitschen (t.fitschen@indiscale.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database.backend.transaction; - -import java.util.ArrayList; -import org.caosdb.datetime.UTCDateTime; -import org.caosdb.server.database.BackendTransaction; -import org.caosdb.server.database.backend.interfaces.RetrieveTransactionHistoryImpl; -import org.caosdb.server.database.exceptions.TransactionException; -import org.caosdb.server.database.proto.ProtoTransactionLogMessage; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.utils.TransactionLogMessage; - -public class RetrieveTransactionHistory extends BackendTransaction { - - private final EntityInterface entity; - - public RetrieveTransactionHistory(final EntityInterface entity) { - this.entity = entity; - } - - @Override - protected void execute() { - final RetrieveTransactionHistoryImpl t = - getImplementation(RetrieveTransactionHistoryImpl.class); - process(t.execute(entity.getId())); - } - - private void process(final ArrayList<ProtoTransactionLogMessage> l) throws TransactionException { - for (final ProtoTransactionLogMessage t : l) { - final UTCDateTime dateTime = UTCDateTime.UTCSeconds(t.seconds, t.nanos); - - this.entity.addTransactionLog( - new TransactionLogMessage(t.transaction, this.entity, t.username, dateTime)); - } - } -} diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java index 6077d6491fda93b96262b48783092578af37f4f8..a5079c4e033882c4b1068615551b5132e4ee0cba 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java @@ -22,58 +22,43 @@ */ package org.caosdb.server.database.backend.transaction; -import java.util.Collection; import java.util.HashMap; -import org.caosdb.server.database.CacheableBackendTransaction; -import org.caosdb.server.database.backend.interfaces.RetrieveVersionHistoryImpl; +import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.proto.VersionHistoryItem; import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.Version; -public abstract class RetrieveVersionHistory - extends CacheableBackendTransaction<Integer, HashMap<String, VersionHistoryItem>> { - - // TODO - // private static final ICacheAccess<String, Version> cache = - // Cache.getCache("BACKEND_RetrieveVersionHistory"); - private EntityInterface entity; - private HashMap<String, VersionHistoryItem> map; - - public static void removeCached(Integer entityId) { - // TODO - } +public class RetrieveVersionHistory extends VersionTransaction { public RetrieveVersionHistory(EntityInterface e) { - super(null); // TODO caching - this.entity = e; - } - - @Override - public HashMap<String, VersionHistoryItem> executeNoCache() throws TransactionException { - RetrieveVersionHistoryImpl impl = getImplementation(RetrieveVersionHistoryImpl.class); - return impl.execute(getKey()); + super(e); } - /** After this method call, the version map is available to the object. */ @Override protected void process(HashMap<String, VersionHistoryItem> map) throws TransactionException { - this.map = map; + super.process(map); + if (!map.isEmpty()) getEntity().setVersion(getHistory()); } @Override - protected Integer getKey() { - return entity.getId(); - } - - public HashMap<String, VersionHistoryItem> getMap() { - return this.map; - } - - public EntityInterface getEntity() { - return this.entity; + protected Version getVersion(String id) { + Version v = new Version(id); + VersionHistoryItem i = getHistoryItems().get(v.getId()); + if (i != null) { + v.setDate(UTCDateTime.UTCSeconds(i.seconds, i.nanos)); + v.setUsername(i.username); + v.setRealm(i.realm); + } + return v; } - public Collection<VersionHistoryItem> getList() { - return this.map.values(); + private Version getHistory() { + Version v = getVersion(getEntity().getVersion().getId()); + v.setSuccessors(getSuccessors(v.getId(), true)); + v.setPredecessors(getPredecessors(v.getId(), true)); + v.setHead(v.getSuccessors().isEmpty()); + v.setCompleteHistory(true); + return v; } } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java index 7d9fe5fd7ea42cf3d6aa29a127a7f82f7ec1764c..8d0e54c95d17d0eb83af541b9fee2fb605b17ec9 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java @@ -23,14 +23,12 @@ package org.caosdb.server.database.backend.transaction; import java.util.HashMap; -import java.util.LinkedList; -import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.proto.VersionHistoryItem; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Version; -public class RetrieveVersionInfo extends RetrieveVersionHistory { +public class RetrieveVersionInfo extends VersionTransaction { public RetrieveVersionInfo(EntityInterface e) { super(e); @@ -39,46 +37,19 @@ public class RetrieveVersionInfo extends RetrieveVersionHistory { @Override protected void process(HashMap<String, VersionHistoryItem> map) throws TransactionException { super.process(map); // Make the map available to the object. - if (!map.isEmpty()) getVersion(); + if (!map.isEmpty()) getEntity().setVersion(getVersion()); } - public Version getVersion() { - Version v = getEntity().getVersion(); - VersionHistoryItem i = getMap().get(v.getId()); - if (i != null) v.setDate(UTCDateTime.UTCSeconds(i.seconds, i.nanos)); - - v.setPredecessors(getPredecessors(v.getId())); - v.setSuccessors(getSuccessors(v.getId())); - return v; - } - - /** Return a list of direct children. */ - private LinkedList<Version> getSuccessors(String id) { - LinkedList<Version> result = new LinkedList<>(); - - outer: - for (VersionHistoryItem i : getList()) { - if (i.parents != null) - for (String p : i.parents) { - if (id.equals(p)) { - Version successor = new Version(i.id, i.seconds, i.nanos); - result.add(successor); - continue outer; - } - } - } - return result; + @Override + protected Version getVersion(String id) { + return new Version(id); } - /** Return a list of direct parents. */ - private LinkedList<Version> getPredecessors(String id) { - LinkedList<Version> result = new LinkedList<>(); - if (getMap().containsKey(id) && getMap().get(id).parents != null) - for (String p : getMap().get(id).parents) { - VersionHistoryItem i = getMap().get(p); - Version predecessor = new Version(i.id, i.seconds, i.nanos); - result.add(predecessor); - } - return result; + public Version getVersion() { + Version v = getVersion(getEntity().getVersion().getId()); + v.setPredecessors(getPredecessors(v.getId(), false)); + v.setSuccessors(getSuccessors(v.getId(), false)); + v.setHead(v.getSuccessors().isEmpty()); + return v; } } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java b/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java index 2bc6a92a2449a8ba2d42119c0ba743129bcee2ac..c73023bd4429db19ca0a968aa99b657f0d0cbea4 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java @@ -33,9 +33,16 @@ import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.proto.Rule; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.Job; import org.caosdb.server.transaction.Transaction; +/** + * Load transaction rules (e.g. "Referenced entities must always exist") and their configuration + * from the back-end. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Rule>> { private static final ICacheAccess<String, ArrayList<Rule>> cache = @@ -45,6 +52,7 @@ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Ru private final Integer entity; private final Integer domain; private ArrayList<Job> jobs; + private String transactionType; public RuleLoader( final Integer domain, @@ -55,18 +63,17 @@ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Ru this.domain = domain; this.entity = entity; this.e = e; + if (e instanceof Property) { + this.transactionType = getTransactionType(((Property) e).getDomainEntity()); + } else { + this.transactionType = getTransactionType(e); + } this.transaction = transaction; } @Override protected String getKey() { - return "<" - + this.domain - + "," - + this.entity - + "," - + this.transaction.getClass().getSimpleName() - + ">"; + return "<" + this.domain + "," + this.entity + "," + this.transactionType + ">"; } public ArrayList<Job> getJobs() { @@ -76,7 +83,7 @@ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Ru @Override public ArrayList<Rule> executeNoCache() throws TransactionException { final RuleLoaderImpl t = getImplementation(RuleLoaderImpl.class); - return t.executeNoCache(this.domain, this.entity, this.transaction.getClass().getSimpleName()); + return t.executeNoCache(this.domain, this.entity, this.transactionType); } @Override diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntityTransaction.java similarity index 90% rename from src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntityTransaction.java index 9c20cb1d2d112e8d64ce5be2cfa8a7f8da20fe10..275947abd9b42a2424141bc28111889859fb1475 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntityTransaction.java @@ -28,11 +28,11 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.utils.EntityStatus; -public class UpdateEntity extends BackendTransaction { +public class UpdateEntityTransaction extends BackendTransaction { private final TransactionContainer container; - public UpdateEntity(final TransactionContainer container) { + public UpdateEntityTransaction(final TransactionContainer container) { this.container = container; } @@ -54,6 +54,7 @@ public class UpdateEntity extends BackendTransaction { execute(new InsertEntityProperties(e)); + VersionTransaction.removeCached(e.getId()); execute(new RetrieveVersionInfo(e)); } } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java index 2385ec843a3d499d1184b58eca06f8ae05cbe4f8..8548fb0477de9d8b52b18b8132af15295dbc9353 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java @@ -52,6 +52,6 @@ public class UpdateSparseEntity extends BackendTransaction { t.execute(spe); - this.entity.setVersion(new Version(spe.version)); + this.entity.setVersion(new Version(spe.versionId)); } } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/VersionTransaction.java b/src/main/java/org/caosdb/server/database/backend/transaction/VersionTransaction.java new file mode 100644 index 0000000000000000000000000000000000000000..ee617a1c1110e9a01e9a6d6ee4c799e44cd66ab3 --- /dev/null +++ b/src/main/java/org/caosdb/server/database/backend/transaction/VersionTransaction.java @@ -0,0 +1,159 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.database.backend.transaction; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.jcs.access.behavior.ICacheAccess; +import org.caosdb.server.caching.Cache; +import org.caosdb.server.database.CacheableBackendTransaction; +import org.caosdb.server.database.backend.interfaces.RetrieveVersionHistoryImpl; +import org.caosdb.server.database.exceptions.TransactionException; +import org.caosdb.server.database.proto.VersionHistoryItem; +import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.Version; + +/** + * Abstract base class which retrieves and caches the full, but flat version history. The + * implementations then use the flat version history to construct either single version information + * items (see {@link RetrieveVersionInfo}) or the complete history as a tree (see {@link + * RetrieveVersionHistory}) + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public abstract class VersionTransaction + extends CacheableBackendTransaction<Integer, HashMap<String, VersionHistoryItem>> { + + private static final ICacheAccess<Integer, HashMap<String, VersionHistoryItem>> cache = + Cache.getCache("BACKEND_RetrieveVersionHistory"); + private EntityInterface entity; + + /** A map of all history items which belong to this entity. The keys are the version ids. */ + private HashMap<String, VersionHistoryItem> historyItems; + + /** + * Invalidate a cache item. This should be called upon update of entities. + * + * @param entityId + */ + public static void removeCached(Integer entityId) { + cache.remove(entityId); + } + + public VersionTransaction(EntityInterface e) { + super(cache); + this.entity = e; + } + + @Override + public HashMap<String, VersionHistoryItem> executeNoCache() throws TransactionException { + RetrieveVersionHistoryImpl impl = getImplementation(RetrieveVersionHistoryImpl.class); + return impl.execute(getKey()); + } + + /** After this method call, the version map is available to the object. */ + @Override + protected void process(HashMap<String, VersionHistoryItem> historyItems) + throws TransactionException { + this.historyItems = historyItems; + } + + @Override + protected Integer getKey() { + return entity.getId(); + } + + public HashMap<String, VersionHistoryItem> getHistoryItems() { + return this.historyItems; + } + + public EntityInterface getEntity() { + return this.entity; + } + + /** + * Return a list of direct predecessors. The predecessors are constructed by {@link + * #getVersion(String)}. + * + * <p>If transitive is true, this function is called recursively on the predecessors as well, + * resulting in a list of trees of predecessors, with the direct predecessors at the root(s). + * + * @param versionId + * @param transitive + * @return A list of predecessors. + */ + protected List<Version> getPredecessors(String versionId, boolean transitive) { + LinkedList<Version> result = new LinkedList<>(); + if (getHistoryItems().containsKey(versionId) + && getHistoryItems().get(versionId).parents != null) + for (String p : getHistoryItems().get(versionId).parents) { + Version predecessor = getVersion(p); + if (transitive) { + predecessor.setPredecessors(getPredecessors(p, transitive)); + } + result.add(predecessor); + } + return result; + } + + /** + * To be implemented by the base class. The idea is, that the base class decides which information + * is being included into the Version instance. + * + * @param versionId - the id of the version + * @return + */ + protected abstract Version getVersion(String versionId); + + /** + * Return a list of direct successors. The successors are constructed by {@link + * #getVersion(String)}. + * + * <p>If transitive is true, this function is called recursively on the successors as well, + * resulting in a list of trees of successors, with the direct successors at the root(s). + * + * @param versionId + * @param transitive + * @return A list of successors. + */ + protected List<Version> getSuccessors(String versionId, boolean transitive) { + LinkedList<Version> result = new LinkedList<>(); + + outer: + for (VersionHistoryItem i : getHistoryItems().values()) { + if (i.parents != null) + for (String p : i.parents) { + if (versionId.equals(p)) { + Version successor = getVersion(i.id); + result.add(successor); + if (transitive) { + successor.setSuccessors(getSuccessors(i.id, transitive)); + } + continue outer; + } + } + } + return result; + } +} diff --git a/src/main/java/org/caosdb/server/database/proto/ProtoTransactionLogMessage.java b/src/main/java/org/caosdb/server/database/proto/ProtoTransactionLogMessage.java deleted file mode 100644 index dd5cdbebcda7dd2c5c7f9ca93be69972ecef0e2c..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/database/proto/ProtoTransactionLogMessage.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database.proto; - -import java.io.Serializable; - -public class ProtoTransactionLogMessage implements Serializable { - - public ProtoTransactionLogMessage( - final String transaction, - final String realm, - final String username, - final long seconds, - final int nanos) { - this.transaction = transaction; - this.realm = realm; - this.username = username; - this.seconds = seconds; - this.nanos = nanos; - } - - public ProtoTransactionLogMessage() {} - - private static final long serialVersionUID = -5856887517281480754L; - public String transaction = null; - public String realm; - public String username = null; - public Long seconds = null; - public Integer nanos = null; -} diff --git a/src/main/java/org/caosdb/server/database/proto/SparseEntity.java b/src/main/java/org/caosdb/server/database/proto/SparseEntity.java index 3d5a29c9c4a69670d49347864b7ed885e758192f..d2292c58825ac6807e24f3ad3d086b29dd8a6e7e 100644 --- a/src/main/java/org/caosdb/server/database/proto/SparseEntity.java +++ b/src/main/java/org/caosdb/server/database/proto/SparseEntity.java @@ -38,9 +38,7 @@ public class SparseEntity extends VerySparseEntity { public String filePath = null; public Long fileSize = null; public Long fileChecked = null; - public String version = null; - public Long versionSeconds = null; - public Integer versionNanos = null; + public String versionId = null; @Override public String toString() { @@ -54,9 +52,7 @@ public class SparseEntity extends VerySparseEntity { .append(this.fileHash) .append(this.filePath) .append(this.fileSize) - .append(this.version) - .append(this.versionSeconds) - .append(this.versionNanos) + .append(this.versionId) .toString(); } } diff --git a/src/main/java/org/caosdb/server/database/proto/VersionHistoryItem.java b/src/main/java/org/caosdb/server/database/proto/VersionHistoryItem.java index 26109760ecde53ea20405bd02095cd951818677d..4c0a60ce57cce03cfaa7a2c4d41af7039c463838 100644 --- a/src/main/java/org/caosdb/server/database/proto/VersionHistoryItem.java +++ b/src/main/java/org/caosdb/server/database/proto/VersionHistoryItem.java @@ -1,13 +1,27 @@ package org.caosdb.server.database.proto; import java.io.Serializable; -import java.util.LinkedList; +import java.util.List; +/** + * This class is a flat, data-only representation of a single item of version information. This + * class is an intermediate representation which abstracts away the data base results and comes in a + * form which is easily cacheable. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class VersionHistoryItem implements Serializable { - private static final long serialVersionUID = 6319362462701459355L; + private static final long serialVersionUID = 428030617967255942L; public String id = null; - public LinkedList<String> parents = null; + public List<String> parents = null; public Long seconds = null; public Integer nanos = null; + public String username = null; + public String realm = null; + + @Override + public String toString() { + return id; + } } diff --git a/src/main/java/org/caosdb/server/datatype/SQLiteDatatype.java b/src/main/java/org/caosdb/server/datatype/SQLiteDatatype.java deleted file mode 100644 index ffd4341013176992e802e8e0c8c4bc2d58b78ad3..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/datatype/SQLiteDatatype.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.datatype; - -@DatatypeDefinition(name = "SQLite") -public class SQLiteDatatype extends ReferenceDatatype {} diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java index 28f42308e303c0315b432c017fd2147119c5b346..aa4c96127cfc97b03f308a50cb587dc12949c105 100644 --- a/src/main/java/org/caosdb/server/entity/Entity.java +++ b/src/main/java/org/caosdb/server/entity/Entity.java @@ -34,7 +34,6 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.Permission; import org.apache.shiro.subject.Subject; -import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.CaosDBException; import org.caosdb.server.database.proto.SparseEntity; import org.caosdb.server.database.proto.VerySparseEntity; @@ -42,6 +41,7 @@ import org.caosdb.server.datatype.AbstractCollectionDatatype; import org.caosdb.server.datatype.AbstractDatatype; import org.caosdb.server.datatype.CollectionValue; import org.caosdb.server.datatype.GenericValue; +import org.caosdb.server.datatype.ReferenceDatatype; import org.caosdb.server.datatype.Value; import org.caosdb.server.entity.Message.MessageType; import org.caosdb.server.entity.container.ParentContainer; @@ -957,7 +957,8 @@ public class Entity extends AbstractObservable implements EntityInterface { @Override public String toString() { - return (hasId() ? "(" + getId().toString() + ")" : "()") + return (getRole().toString()) + + (hasId() ? "(" + getId().toString() + ")" : "()") + (hasCuid() ? "[" + getCuid() + "]" : "[]") + (hasName() ? "(" + getName() + ")" : "()") + (hasDatatype() ? "{" + getDatatype().toString() + "}" : "{}"); @@ -1078,11 +1079,9 @@ public class Entity extends AbstractObservable implements EntityInterface { setId(spe.id); this.setRole(spe.role); setEntityACL(spe.acl); - UTCDateTime versionDate = null; - if (spe.versionSeconds != null) { - versionDate = UTCDateTime.UTCSeconds(spe.versionSeconds, spe.versionNanos); + if (spe.versionId != null) { + this.version = new Version(spe.versionId); } - this.version = new Version(spe.version, versionDate); if (!isNameOverride()) { setName(spe.name); @@ -1161,4 +1160,16 @@ public class Entity extends AbstractObservable implements EntityInterface { } return getId().toString(); } + + @Override + public boolean isReference() { + return this.hasDatatype() && this.getDatatype() instanceof ReferenceDatatype; + } + + @Override + public boolean isReferenceList() { + return this.hasDatatype() + && this.getDatatype() instanceof AbstractCollectionDatatype + && ((AbstractCollectionDatatype) getDatatype()).getDatatype() instanceof ReferenceDatatype; + } } diff --git a/src/main/java/org/caosdb/server/entity/EntityInterface.java b/src/main/java/org/caosdb/server/entity/EntityInterface.java index ef97630808536f367869851c5be06d05e6247ac2..954bcc47ca5b6117d3164ba3c9c585dbd373d4f8 100644 --- a/src/main/java/org/caosdb/server/entity/EntityInterface.java +++ b/src/main/java/org/caosdb/server/entity/EntityInterface.java @@ -194,4 +194,13 @@ public interface EntityInterface public abstract void setVersion(Version version); public abstract void addToElement(Element element, SetFieldStrategy strategy); + + /** Return true iff the data type is present and is an instance of ReferenceDatatype. */ + public abstract boolean isReference(); + + /** + * Return true iff the data type is present, an instance of AbstractCollectionDatatype and the + * AbstractCollectionDatatype's elements' data type is an instance of ReferenceDatatype. + */ + public abstract boolean isReferenceList(); } diff --git a/src/main/java/org/caosdb/server/entity/FileProperties.java b/src/main/java/org/caosdb/server/entity/FileProperties.java index 0c10288e0cbaa6a4f55a2959f72ac09db9d6f394..c2c2c10fae44d4d84af56a33a052f5ed589debed 100644 --- a/src/main/java/org/caosdb/server/entity/FileProperties.java +++ b/src/main/java/org/caosdb/server/entity/FileProperties.java @@ -289,4 +289,8 @@ public class FileProperties { public void removeOnCleanUp(final String tempPath) { this.tempPath = tempPath; } + + public boolean hasTmpIdentifier() { + return this.tmpIdentifyer != null; + } } diff --git a/src/main/java/org/caosdb/server/entity/InsertEntity.java b/src/main/java/org/caosdb/server/entity/InsertEntity.java index 56eeb12b1c2e7b9d5259a4d418e87c79e8068f51..4f7c33015e6ba70fb3d062ff4b32c27ebd80b8a8 100644 --- a/src/main/java/org/caosdb/server/entity/InsertEntity.java +++ b/src/main/java/org/caosdb/server/entity/InsertEntity.java @@ -29,4 +29,8 @@ public class InsertEntity extends WritableEntity { public InsertEntity(final Element element) { super(element); } + + public InsertEntity(String name, Role role) { + super(name, role); + } } diff --git a/src/main/java/org/caosdb/server/entity/StatementStatus.java b/src/main/java/org/caosdb/server/entity/StatementStatus.java index ec6b0b8ec8cc33fc0fa2c41119e82b2b807aa89f..b3d7a613d26eba432308272b3bd932d2fdb54b26 100644 --- a/src/main/java/org/caosdb/server/entity/StatementStatus.java +++ b/src/main/java/org/caosdb/server/entity/StatementStatus.java @@ -22,12 +22,21 @@ */ package org.caosdb.server.entity; +/** + * The statement status has two purposes. + * + * <p>1. Storing the importance of an entity (any of OBLIGATORY, RECOMMENDED, SUGGESTED, or FIX). 2. + * Marking an entity as a REPLACEMENT which is needed for flat representation of deeply nested + * properties. This constant is only used for internal processes and has no meaning in the API. That + * is also the reason why this enum is not called "Importance". Apart from that, in most cases its + * meaning is identical to the importance of an entity. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public enum StatementStatus { OBLIGATORY, RECOMMENDED, SUGGESTED, FIX, - SUBTYPING, - INHERITANCE, REPLACEMENT } diff --git a/src/main/java/org/caosdb/server/entity/ValidEntity.java b/src/main/java/org/caosdb/server/entity/ValidEntity.java deleted file mode 100644 index 6378e044e65afe4b7340cefd04e0b09153e5ca94..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/ValidEntity.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity; - -public class ValidEntity extends Entity { - - public ValidEntity(final int id) { - super(id); - } -} diff --git a/src/main/java/org/caosdb/server/entity/Version.java b/src/main/java/org/caosdb/server/entity/Version.java index 3f9e148583686887fab39038aa427bccf1c69b25..5529c7c5f5e8c83d95ad5afae7c1ae786806df89 100644 --- a/src/main/java/org/caosdb/server/entity/Version.java +++ b/src/main/java/org/caosdb/server/entity/Version.java @@ -20,7 +20,7 @@ package org.caosdb.server.entity; -import java.util.LinkedList; +import java.util.List; import org.caosdb.datetime.UTCDateTime; /** @@ -31,25 +31,35 @@ import org.caosdb.datetime.UTCDateTime; public class Version { private String id = null; - private LinkedList<Version> predecessors = null; - private LinkedList<Version> successors = null; + private String username = null; + private String realm = null; + private List<Version> predecessors = null; + private List<Version> successors = null; private UTCDateTime date = null; + private boolean isHead = false; + private boolean isCompleteHistory = false; public Version(String id, long seconds, int nanos) { - this(id, UTCDateTime.UTCSeconds(seconds, nanos)); + this(id, UTCDateTime.UTCSeconds(seconds, nanos), null, null); } - public Version(String id, UTCDateTime date) { + public Version(String id, UTCDateTime date, String username, String realm) { this.id = id; this.date = date; + this.username = username; + this.realm = realm; } public Version(String id) { - this(id, null); + this(id, null, null, null); } public Version() {} + public Version(String id, UTCDateTime timestamp) { + this(id, timestamp, null, null); + } + public UTCDateTime getDate() { return date; } @@ -66,19 +76,55 @@ public class Version { this.id = id; } - public LinkedList<Version> getSuccessors() { + public List<Version> getSuccessors() { return successors; } - public void setSuccessors(LinkedList<Version> successors) { + public void setSuccessors(List<Version> successors) { this.successors = successors; } - public LinkedList<Version> getPredecessors() { + public List<Version> getPredecessors() { return predecessors; } - public void setPredecessors(LinkedList<Version> predecessors) { + public void setPredecessors(List<Version> predecessors) { this.predecessors = predecessors; } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setDate(Long timestamp) { + this.date = UTCDateTime.SystemMillisToUTCDateTime(timestamp); + } + + public boolean isHead() { + return isHead; + } + + public void setHead(boolean isHead) { + this.isHead = isHead; + } + + public boolean isCompleteHistory() { + return isCompleteHistory; + } + + public void setCompleteHistory(boolean isCompleteHistory) { + this.isCompleteHistory = isCompleteHistory; + } } diff --git a/src/main/java/org/caosdb/server/entity/WritableEntity.java b/src/main/java/org/caosdb/server/entity/WritableEntity.java index d9dc3edc4fc1fa886370bafb5c40ce0e732b3e4b..ab8ccbb995430e267e84d1f2a235c0d928e6762d 100644 --- a/src/main/java/org/caosdb/server/entity/WritableEntity.java +++ b/src/main/java/org/caosdb/server/entity/WritableEntity.java @@ -29,4 +29,8 @@ public class WritableEntity extends Entity { public WritableEntity(final Element element) { super(element); } + + public WritableEntity(String name, Role role) { + super(name, role); + } } diff --git a/src/main/java/org/caosdb/server/entity/container/DeleteContainer.java b/src/main/java/org/caosdb/server/entity/container/DeleteContainer.java deleted file mode 100644 index b0c2f21ff3b0e0dd96e24f0ef042a29e843c54f0..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/container/DeleteContainer.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity.container; - -import java.util.HashMap; -import org.apache.shiro.subject.Subject; -import org.caosdb.server.entity.DeleteEntity; - -public class DeleteContainer extends EntityByIdContainer { - - private static final long serialVersionUID = -1279198934417710461L; - - public DeleteContainer( - final Subject user, - final Long timestamp, - final String srid, - final HashMap<String, String> flags) { - super(user, timestamp, srid, flags); - } - - @Override - public void add(final int id) { - add(new DeleteEntity(id)); - } - - @Override - public void add(int id, String version) { - add(new DeleteEntity(id, version)); - } -} diff --git a/src/main/java/org/caosdb/server/entity/container/InsertContainer.java b/src/main/java/org/caosdb/server/entity/container/InsertContainer.java deleted file mode 100644 index 99128a8a56758e67ce1529785de0151b1fe624d4..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/container/InsertContainer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity.container; - -import java.util.HashMap; -import org.apache.shiro.subject.Subject; -import org.caosdb.server.entity.InsertEntity; -import org.jdom2.Element; - -public class InsertContainer extends WritableContainer { - - public InsertContainer( - final Subject user, - final Long timestamp, - final String srid, - final HashMap<String, String> flags) { - super(user, timestamp, srid, flags); - } - - private static final long serialVersionUID = 8896630069350477274L; - - @Override - public void add(final Element entity) { - add(new InsertEntity(entity)); - } -} diff --git a/src/main/java/org/caosdb/server/entity/container/UpdateContainer.java b/src/main/java/org/caosdb/server/entity/container/UpdateContainer.java deleted file mode 100644 index 1c2a807b7759ff680d30ed0f1f9f14d5952e98f8..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/container/UpdateContainer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity.container; - -import java.util.HashMap; -import org.apache.shiro.subject.Subject; -import org.caosdb.server.entity.UpdateEntity; -import org.jdom2.Element; - -public class UpdateContainer extends WritableContainer { - - private static final long serialVersionUID = 8489287672264669883L; - - public UpdateContainer( - final Subject user, - final Long timestamp, - final String srid, - final HashMap<String, String> flags) { - super(user, timestamp, srid, flags); - } - - @Override - public void add(final Element entity) { - add(new UpdateEntity(entity)); - } -} diff --git a/src/main/java/org/caosdb/server/entity/container/WritableContainer.java b/src/main/java/org/caosdb/server/entity/container/WritableContainer.java index 2f9148651c890a0e7188acb035bf8fc55609834b..5fdb8c39ec7f01280278bec0a1f2928bfed7f5c2 100644 --- a/src/main/java/org/caosdb/server/entity/container/WritableContainer.java +++ b/src/main/java/org/caosdb/server/entity/container/WritableContainer.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,12 +25,12 @@ package org.caosdb.server.entity.container; import java.util.HashMap; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.caosdb.server.entity.FileProperties; -import org.jdom2.Element; -public abstract class WritableContainer extends TransactionContainer { - private static final long serialVersionUID = 3011242254959617091L; +public class WritableContainer extends TransactionContainer { + private static final long serialVersionUID = -4097777313518959519L; public WritableContainer( final Subject user, @@ -38,7 +40,9 @@ public abstract class WritableContainer extends TransactionContainer { super(user, timestamp, srid, flags); } - public abstract void add(final Element entity); + public WritableContainer() { + this(SecurityUtils.getSubject(), System.currentTimeMillis(), null, null); + } @Override public void addFile(final String uploadId, final FileProperties fileProperties) { diff --git a/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java b/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java index e042c2326cc43e5d98cc3a27b61c21301bf9e90d..c83cb67883f5a616109268fb6d62bae40cd73e6b 100644 --- a/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java +++ b/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java @@ -580,4 +580,14 @@ public class EntityWrapper implements EntityInterface { public void addToElement(Element element, SetFieldStrategy strategy) { this.entity.addToElement(element, strategy); } + + @Override + public boolean isReference() { + return this.entity.isReference(); + } + + @Override + public boolean isReferenceList() { + return this.entity.isReferenceList(); + } } diff --git a/src/main/java/org/caosdb/server/entity/wrapper/Property.java b/src/main/java/org/caosdb/server/entity/wrapper/Property.java index 305a9d83cbfad835baa547670fae8e186417330d..b3ce45b56527e40382d3971a61b43f9bc054d21f 100644 --- a/src/main/java/org/caosdb/server/entity/wrapper/Property.java +++ b/src/main/java/org/caosdb/server/entity/wrapper/Property.java @@ -120,7 +120,7 @@ public class Property extends EntityWrapper { @Override public String toString() { - return "IMPLPROPERTY " + this.entity.toString(); + return "IMPLPROPERTY (" + this.getStatementStatus() + ")" + this.entity.toString(); } public void setIsName(final boolean b) { diff --git a/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java index e6dc2b21d088ceeff9e0e7a9b60547b288893f18..89420c596894da9be35e33a507821c96d10f5b7d 100644 --- a/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java +++ b/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java @@ -4,8 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020-2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,6 +25,8 @@ package org.caosdb.server.entity.xml; import org.apache.shiro.SecurityUtils; +import org.caosdb.server.datatype.CollectionValue; +import org.caosdb.server.datatype.IndexedSingleValue; import org.caosdb.server.datatype.ReferenceValue; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; @@ -87,7 +89,7 @@ public class EntityToElementStrategy implements ToElementStrategy { if (setFieldStrategy.isToBeSet("id") && entity.hasId()) { element.setAttribute("id", Integer.toString(entity.getId())); } - if (setFieldStrategy.isToBeSet("version") && entity.hasVersion()) { + if (entity.hasVersion()) { Element v = new VersionXMLSerializer().toElement(entity.getVersion()); element.addContent(v); } @@ -137,8 +139,7 @@ public class EntityToElementStrategy implements ToElementStrategy { // CheckValueParsable job. } - if (entity.getValue() instanceof ReferenceValue - && setFieldStrategy.isToBeSet("_referenced")) { + if (entity.isReference() && setFieldStrategy.isToBeSet("_referenced")) { // Append the complete entity. This needs to be done when we are // processing SELECT Queries. EntityInterface ref = ((ReferenceValue) entity.getValue()).getEntity(); @@ -151,6 +152,26 @@ public class EntityToElementStrategy implements ToElementStrategy { // adding the reference id as well. return; } + } else if (entity.isReferenceList() && setFieldStrategy.isToBeSet("_referenced")) { + // Append the all referenced entities. This needs to be done when we are + // processing SELECT Queries. + boolean skipValue = false; + for (IndexedSingleValue sv : ((CollectionValue) entity.getValue())) { + EntityInterface ref = ((ReferenceValue) sv.getWrapped()).getEntity(); + if (ref != null) { + if (entity.hasDatatype()) { + setDatatype(entity, element); + } + Element valueElem = new Element("Value"); + ref.addToElement(valueElem, setFieldStrategy); + element.addContent(valueElem); + skipValue = true; + } + } + if (skipValue) + // the referenced entity has been appended. Return here to suppress + // adding the reference id as well. + return; } if (setFieldStrategy.isToBeSet("value")) { diff --git a/src/main/java/org/caosdb/server/entity/xml/VersionXMLSerializer.java b/src/main/java/org/caosdb/server/entity/xml/VersionXMLSerializer.java index 65ec9880dfef43a4e460bba692ece6c9f87faa7e..40d19a5f3a0ef6d9448ed8567840e610be712391 100644 --- a/src/main/java/org/caosdb/server/entity/xml/VersionXMLSerializer.java +++ b/src/main/java/org/caosdb/server/entity/xml/VersionXMLSerializer.java @@ -30,28 +30,43 @@ import org.jdom2.Element; * @author Timm Fitschen <t.fitschen@indiscale.com> */ class VersionXMLSerializer { + public Element toElement(Version version) { - Element result = new Element("Version"); - result.setAttribute("id", version.getId()); - if (version.getDate() != null) { - result.setAttribute("date", version.getDate().toDateTimeString(TimeZone.getDefault())); - } + return toElement(version, "Version"); + } + + private Element toElement(Version version, String tag) { + Element element = new Element(tag); + setAttributes(version, element); if (version.getPredecessors() != null) { for (Version p : version.getPredecessors()) { - Element predecessor = new Element("Predecessor"); - predecessor.setAttribute("id", p.getId()); - predecessor.setAttribute("date", p.getDate().toDateTimeString(TimeZone.getDefault())); - result.addContent(predecessor); + element.addContent(toElement(p, "Predecessor")); } } if (version.getSuccessors() != null) { for (Version s : version.getSuccessors()) { - Element successor = new Element("Successor"); - successor.setAttribute("id", s.getId()); - successor.setAttribute("date", s.getDate().toDateTimeString(TimeZone.getDefault())); - result.addContent(successor); + element.addContent(toElement(s, "Successor")); } } - return result; + return element; + } + + private void setAttributes(Version version, Element element) { + element.setAttribute("id", version.getId()); + if (version.getUsername() != null) { + element.setAttribute("username", version.getUsername()); + } + if (version.getRealm() != null) { + element.setAttribute("realm", version.getRealm()); + } + if (version.getDate() != null) { + element.setAttribute("date", version.getDate().toDateTimeString(TimeZone.getDefault())); + } + if (version.isHead()) { + element.setAttribute("head", "true"); + } + if (version.isCompleteHistory()) { + element.setAttribute("completeHistory", "true"); + } } } diff --git a/src/main/java/org/caosdb/server/jobs/Job.java b/src/main/java/org/caosdb/server/jobs/Job.java index d1e3fef39fcd6a2fb5175e4dd396686bcf773b4e..28dc512b669eb6f1df0cfd24e32e2051f734b697 100644 --- a/src/main/java/org/caosdb/server/jobs/Job.java +++ b/src/main/java/org/caosdb/server/jobs/Job.java @@ -37,7 +37,7 @@ import org.caosdb.server.CaosDBException; import org.caosdb.server.database.BackendTransaction; import org.caosdb.server.database.backend.transaction.GetIDByName; import org.caosdb.server.database.backend.transaction.IsSubType; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.database.backend.transaction.RetrieveParents; import org.caosdb.server.database.backend.transaction.RetrieveSparseEntity; import org.caosdb.server.database.backend.transaction.RuleLoader; @@ -57,6 +57,11 @@ import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; import org.reflections.Reflections; +/** + * This is a Job. + * + * @todo Describe me. + */ public abstract class Job extends AbstractObservable implements Observer { private Transaction<? extends TransactionContainer> transaction = null; private Mode mode = null; @@ -213,7 +218,7 @@ public abstract class Job extends AbstractObservable implements Observer { } protected final EntityInterface retrieveValidEntity(Integer id) { - return execute(new RetrieveFullEntity(id)).getContainer().get(0); + return execute(new RetrieveFullEntityTransaction(id)).getContainer().get(0); } protected final Integer retrieveValidIDByName(final String name) { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java b/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java index e2dd63b1d733d737e09a6b4e86f3fe4ae4bd0e30..037b38b90c94827265d47876e722686d5d030cc0 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java @@ -26,10 +26,10 @@ import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.caosdb.server.accessControl.AuthenticationUtils; import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.jobs.ContainerJob; import org.caosdb.server.permissions.EntityACI; -import org.caosdb.server.transaction.Insert; -import org.caosdb.server.transaction.Update; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -37,12 +37,9 @@ public class CheckEntityACLRoles extends ContainerJob { @Override protected void run() { - if (!(getTransaction() instanceof Update || getTransaction() instanceof Insert)) { - return; - } - for (final EntityInterface entity : getContainer()) { - if (entity.getEntityACL() != null) { + if ((entity instanceof UpdateEntity || entity instanceof InsertEntity) + && entity.getEntityACL() != null) { for (final EntityACI aci : entity.getEntityACL().getRules()) { if (!AuthenticationUtils.isResponsibleAgentExistent(aci.getResponsibleAgent())) { if (CaosDBServer.getServerProperty(ServerProperties.KEY_CHECK_ENTITY_ACL_ROLES_MODE) diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java index c592eca81394e4a92582fd3d2286b5ac0507dcce..7aca2bac3dd3709006497f9ce8f556f267c4ca81 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java @@ -22,7 +22,7 @@ */ package org.caosdb.server.jobs.core; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.StatementStatus; import org.caosdb.server.jobs.EntityJob; @@ -48,7 +48,7 @@ public class CheckParOblPropPresent extends EntityJob { runJobFromSchedule(getEntity(), CheckParValid.class); if (getEntity().hasParents()) { - execute(new RetrieveFullEntity(getEntity().getParents())); + execute(new RetrieveFullEntityTransaction(getEntity().getParents())); } // inherit properties diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java index 42476f87b43496e162b6eb036480b6b745ee91f2..c9d06cb5487ff62a276ff1028c22864afa850562 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java @@ -4,8 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020-2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -26,10 +26,8 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.database.exceptions.EntityDoesNotExistException; import org.caosdb.server.database.exceptions.EntityWasNotUniqueException; -import org.caosdb.server.datatype.AbstractCollectionDatatype; import org.caosdb.server.datatype.CollectionValue; import org.caosdb.server.datatype.IndexedSingleValue; -import org.caosdb.server.datatype.ReferenceDatatype; import org.caosdb.server.datatype.ReferenceValue; import org.caosdb.server.entity.Entity; import org.caosdb.server.entity.EntityInterface; @@ -49,7 +47,7 @@ public class CheckRefidValid extends EntityJob { @Override public final void run() { try { - if (isReference(getEntity())) { + if (assureReference(getEntity())) { if (getEntity().hasValue()) { // parse referenced id @@ -58,11 +56,9 @@ public class CheckRefidValid extends EntityJob { return; } - if (getEntity().getDatatype() instanceof ReferenceDatatype) { + if (getEntity().isReference()) { checkRefValue((ReferenceValue) getEntity().getValue()); - } else if (getEntity().getDatatype() instanceof AbstractCollectionDatatype - && ((AbstractCollectionDatatype) getEntity().getDatatype()).getDatatype() - instanceof ReferenceDatatype) { + } else if (getEntity().isReferenceList()) { final CollectionValue vals = (CollectionValue) getEntity().getValue(); for (final IndexedSingleValue v : vals) { if (v != null && v.getWrapped() != null) { @@ -175,14 +171,15 @@ public class CheckRefidValid extends EntityJob { return true; } - private final boolean isReference(final EntityInterface entity) { + /** + * Return true if this is a reference or a list of reference property. + * + * <p>If the data type is not present (yet), append a data type listener which calls this job + * again when the data type is present. + */ + private final boolean assureReference(final EntityInterface entity) { if (entity.hasDatatype()) { - if (entity.getDatatype() instanceof ReferenceDatatype) { - return true; - } else if (entity.getDatatype() instanceof AbstractCollectionDatatype) { - return ((AbstractCollectionDatatype) entity.getDatatype()).getDatatype() - instanceof ReferenceDatatype; - } + return entity.isReference() || entity.isReferenceList(); } else { entity.acceptObserver(this); } diff --git a/src/main/java/org/caosdb/server/jobs/core/History.java b/src/main/java/org/caosdb/server/jobs/core/History.java index a7c58979f49b2a7abdc313924dc8fb6b86bbffd4..5594b5afdb841436dbdd63f96b8e2c44faef51c6 100644 --- a/src/main/java/org/caosdb/server/jobs/core/History.java +++ b/src/main/java/org/caosdb/server/jobs/core/History.java @@ -23,7 +23,7 @@ package org.caosdb.server.jobs.core; import org.apache.shiro.authz.AuthorizationException; -import org.caosdb.server.database.backend.transaction.RetrieveTransactionHistory; +import org.caosdb.server.database.backend.transaction.RetrieveVersionHistory; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; @@ -32,6 +32,11 @@ import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; +/** + * Retrieves the complete version history of each entity and appends it to the entity. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ @JobAnnotation(time = JobExecutionTime.POST_TRANSACTION, flag = "H") public class History extends FlagJob { @@ -42,7 +47,7 @@ public class History extends FlagJob { if (entity.getId() != null && entity.getId() > 0) { try { entity.checkPermission(EntityPermission.RETRIEVE_HISTORY); - final RetrieveTransactionHistory t = new RetrieveTransactionHistory(entity); + final RetrieveVersionHistory t = new RetrieveVersionHistory(entity); execute(t); } catch (final AuthorizationException e) { entity.setEntityStatus(EntityStatus.UNQUALIFIED); diff --git a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java index 3c08f9f281335c64a2af28ae131ff99d2c5ed6ca..d49bec09c909c5e884eae80619a5911a50b1f333 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java +++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java @@ -23,26 +23,36 @@ package org.caosdb.server.jobs.core; import java.util.ArrayList; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import java.util.List; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.entity.Entity; import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.InsertEntity; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.StatementStatus; +import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; -import org.caosdb.server.transaction.Insert; -import org.caosdb.server.transaction.Update; import org.caosdb.server.utils.EntityStatus; +/** + * Add all those properties from the parent to the child which have the same importance (or higher). + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class Inheritance extends EntityJob { + /* + * Storing which properties of the properties of the parents should be inherited by the child. + */ public enum INHERITANCE_MODE { - NONE, - ALL, - OBLIGATORY, - FIX, - RECOMMENDED, - DONE + NONE, // inherit no properties from this parent + ALL, // inherit all inheritable properties, alias for suggested + OBLIGATORY, // inherit only obligatory properties + RECOMMENDED, // inherit obligatory and recommended properties + SUGGESTED, // inherit all inheritable properties, alias for all + FIX, // inherit fix properties only (deprecated) }; public static final Message ILLEGAL_INHERITANCE_MODE = @@ -53,7 +63,7 @@ public class Inheritance extends EntityJob { @Override protected void run() { - if (getTransaction() instanceof Insert || getTransaction() instanceof Update) { + if (getEntity() instanceof InsertEntity || getEntity() instanceof UpdateEntity) { if (getEntity().hasParents()) { final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>(); parentLoop: @@ -66,51 +76,22 @@ public class Inheritance extends EntityJob { INHERITANCE_MODE.valueOf(parent.getFlags().get("inheritance").toUpperCase()); // mark inheritance flag as done - parent.setFlag("inheritance", "done"); - if (inheritance == INHERITANCE_MODE.NONE || inheritance == INHERITANCE_MODE.DONE) { + parent.setFlag("inheritance", null); + if (inheritance == INHERITANCE_MODE.NONE) { break parentLoop; } runJobFromSchedule(getEntity(), CheckParValid.class); - execute(new RetrieveFullEntity(parent)); - - if (parent.hasProperties()) { - // loop over all properties of the parent and - // collect - // properties to be transfered - for (final EntityInterface parProperty : parent.getProperties()) { - switch (inheritance) { - case ALL: - transfer.add(parProperty); - break; - case RECOMMENDED: - if (parProperty - .getStatementStatus() - .toString() - .equalsIgnoreCase(INHERITANCE_MODE.RECOMMENDED.toString())) { - transfer.add(parProperty); - } - case OBLIGATORY: - if (parProperty - .getStatementStatus() - .toString() - .equalsIgnoreCase(INHERITANCE_MODE.OBLIGATORY.toString())) { - transfer.add(parProperty); - } - case FIX: - if (parProperty - .getStatementStatus() - .toString() - .equalsIgnoreCase(INHERITANCE_MODE.FIX.toString())) { - transfer.add(parProperty); - } - break; - default: - break; - } - } + // try to get the parent entity from the current transaction container + EntityInterface foreign = getEntityByName(parent.getName()); + if (foreign == null) { + // was not in container -> retrieve from database. + execute(new RetrieveFullEntityTransaction(parent)); + foreign = parent; } + + collectInheritedProperties(transfer, foreign, inheritance); } catch (final IllegalArgumentException e) { parent.addWarning(ILLEGAL_INHERITANCE_MODE); break parentLoop; @@ -142,9 +123,10 @@ public class Inheritance extends EntityJob { } final INHERITANCE_MODE inheritance = INHERITANCE_MODE.valueOf(property.getFlags().get("inheritance").toUpperCase()); + // mark inheritance flag as done - property.setFlag("inheritance", "done"); - if (inheritance == INHERITANCE_MODE.NONE || inheritance == INHERITANCE_MODE.DONE) { + property.setFlag("inheritance", null); + if (inheritance == INHERITANCE_MODE.NONE) { break propertyLoop; } @@ -162,50 +144,16 @@ public class Inheritance extends EntityJob { } } } else { - execute(new RetrieveFullEntity(validProperty)); + execute(new RetrieveFullEntityTransaction(validProperty)); } - - if (validProperty.getEntityStatus() == EntityStatus.VALID - && validProperty.hasProperties()) { - // loop over all properties of the property and - // collect - // properties to be transfered - for (final EntityInterface propProperty : validProperty.getProperties()) { - switch (inheritance) { - case ALL: - transfer.add(propProperty); - break; - case RECOMMENDED: - if (propProperty - .getStatementStatus() - .toString() - .equalsIgnoreCase(INHERITANCE_MODE.RECOMMENDED.toString())) { - transfer.add(propProperty); - } - case OBLIGATORY: - if (propProperty - .getStatementStatus() - .toString() - .equalsIgnoreCase(INHERITANCE_MODE.OBLIGATORY.toString())) { - transfer.add(propProperty); - } - case FIX: - if (propProperty - .getStatementStatus() - .toString() - .equalsIgnoreCase(INHERITANCE_MODE.FIX.toString())) { - transfer.add(propProperty); - } - break; - default: - break; - } - } + if (validProperty.getEntityStatus() == EntityStatus.VALID) { + collectInheritedProperties(transfer, validProperty, inheritance); } } catch (final IllegalArgumentException e) { property.addWarning(ILLEGAL_INHERITANCE_MODE); break propertyLoop; } + // transfer properties if they are not implemented yet outerLoop: for (final EntityInterface prop : transfer) { @@ -222,4 +170,52 @@ public class Inheritance extends EntityJob { } } } + + /** + * Put all those properties from the `from` entity into the `transfer` List which match the + * INHERITANCE_MODE. + * + * <p>That means: + * + * @param transfer + * @param from + * @param inheritance + */ + private void collectInheritedProperties( + List<EntityInterface> transfer, EntityInterface from, INHERITANCE_MODE inheritance) { + if (from.hasProperties()) { + for (final EntityInterface propProperty : from.getProperties()) { + switch (inheritance) { + // the following cases are ordered according to their importance level and use a + // fall-through. + case ALL: + case SUGGESTED: + if (propProperty.getStatementStatus() == StatementStatus.SUGGESTED) { + transfer.add(propProperty); + } + // fall-through! + case RECOMMENDED: + if (propProperty.getStatementStatus() == StatementStatus.RECOMMENDED) { + transfer.add(propProperty); + } + // fall-through! + case OBLIGATORY: + if (propProperty.getStatementStatus() == StatementStatus.OBLIGATORY) { + transfer.add(propProperty); + } + break; + case FIX: + if (propProperty.getStatementStatus() == StatementStatus.FIX) { + transfer.add(propProperty); + propProperty.addWarning( + new Message( + MessageType.Warning, + "DeprecationWarning: The inheritance of fix properties is deprecated and will be removed from the API in the near future. Clients have to copy fix properties by themselves, if necessary.")); + } + default: + break; + } + } + } + } } diff --git a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java index 73d7ee3f5be0b8847ce322f6774719b7f5a768c2..35ce49297291accd41a7c293fc909cc0a27f9317 100644 --- a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java +++ b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -34,16 +36,19 @@ import org.caosdb.server.FileSystem; import org.caosdb.server.ServerProperties; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.RollBackHandler; -import org.caosdb.server.entity.Entity; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.FileProperties; +import org.caosdb.server.entity.InsertEntity; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.RetrieveEntity; import org.caosdb.server.entity.Role; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.Job; import org.caosdb.server.jobs.JobAnnotation; import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.transaction.Retrieve; +import org.caosdb.server.transaction.WriteTransactionInterface; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.FileUtils; import org.caosdb.server.utils.Undoable; @@ -208,7 +213,7 @@ public class InsertFilesInDir extends FlagJob { } else { i++; final String targetPath = root + sub.getName(); - final Entity newFileEntity = new Entity(sub.getName(), Role.File); + final EntityInterface newFileEntity = createInsertFileEntity(sub.getName()); final long size = sub.length(); final FileProperties fp = new FileProperties(null, targetPath, size); newFileEntity.setFileProperties(fp); @@ -227,7 +232,7 @@ public class InsertFilesInDir extends FlagJob { continue; } - // add create symlink and file record to this + // add create symlink and add file record to this // container if the target // path is allowed if (FileSystem.checkTarget( @@ -253,6 +258,22 @@ public class InsertFilesInDir extends FlagJob { return i; } + /** + * Create a new InsertEntity (if this is an actual run) or a new RetrieveEntity (in dry-run mode) + * with {@link Role.File}. + * + * @param name the file name + * @return new File entity + */ + private EntityInterface createInsertFileEntity(String name) { + if (getTransaction() instanceof WriteTransactionInterface) { + return new InsertEntity(name, Role.File); + } + EntityInterface result = new RetrieveEntity(name); + result.setRole(Role.File); + return result; + } + boolean isExcluded(File f) throws IOException { return this.exclude != null && this.exclude.matcher(f.getCanonicalPath()).find(); } @@ -317,7 +338,7 @@ public class InsertFilesInDir extends FlagJob { return true; } - private void loadJobs(final Entity e) { + private void loadJobs(final EntityInterface e) { final List<Job> loadJobs = loadJobs(e, getTransaction()); getTransaction().getSchedule().addAll(loadJobs); } diff --git a/src/main/java/org/caosdb/server/query/Backreference.java b/src/main/java/org/caosdb/server/query/Backreference.java index 5f8105c823a231a14853e1db370649c7a7189649..fec64b19b761418a4da148ca4adff9d9145960af 100644 --- a/src/main/java/org/caosdb/server/query/Backreference.java +++ b/src/main/java/org/caosdb/server/query/Backreference.java @@ -103,9 +103,12 @@ public class Backreference implements EntityFilterInterface, QueryInterface { return "@(" + getEntity() + "," + getProperty() + ")"; } - /** */ @Override public void apply(final QueryInterface query) throws QueryException { + if (query.isVersioned() && hasSubProperty()) { + throw new UnsupportedOperationException( + "Versioned queries are not supported for subqueries yet. Please file a feature request."); + } final long t1 = System.currentTimeMillis(); this.query = query; this.targetSet = query.getTargetSet(); @@ -113,7 +116,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { initBackRef(query); final CallableStatement callApplyBackRef = - getConnection().prepareCall("call applyBackReference(?,?,?,?,?)"); + getConnection().prepareCall("call applyBackReference(?,?,?,?,?,?)"); callApplyBackRef.setString(1, getSourceSet()); // sourceSet this.statistics.put("sourceSet", getSourceSet()); this.statistics.put( @@ -127,6 +130,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(2, VARCHAR); } if (this.propertiesTable != null) { // propertiesTable + getQuery().filterEntitiesWithoutRetrievePermission(this.propertiesTable); callApplyBackRef.setString(3, this.propertiesTable); this.statistics.put("propertiesTable", this.propertiesTable); this.statistics.put( @@ -136,6 +140,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(3, VARCHAR); } if (this.entitiesTable != null) { // entitiesTable + getQuery().filterEntitiesWithoutRetrievePermission(this.entitiesTable); callApplyBackRef.setString(4, this.entitiesTable); this.statistics.put("entitiesTable", this.entitiesTable); this.statistics.put( @@ -145,6 +150,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(4, VARCHAR); } callApplyBackRef.setBoolean(5, hasSubProperty()); // subQuery? + callApplyBackRef.setBoolean(6, this.isVersioned()); executeStmt(query, callApplyBackRef); callApplyBackRef.close(); @@ -214,7 +220,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { final long t3 = System.currentTimeMillis(); try (final PreparedStatement callFinishSubProperty = - getConnection().prepareCall("call finishSubProperty(?,?,?)")) { + getConnection().prepareCall("call finishSubProperty(?,?,?,?)")) { callFinishSubProperty.setString(1, query.getSourceSet()); // sourceSet if (query.getTargetSet() != null) { // targetSet callFinishSubProperty.setString(2, query.getTargetSet()); @@ -222,6 +228,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callFinishSubProperty.setNull(2, VARCHAR); } callFinishSubProperty.setString(3, this.sourceSet); // list + callFinishSubProperty.setBoolean(4, this.isVersioned()); final ResultSet rs2 = callFinishSubProperty.executeQuery(); rs2.next(); this.statistics.put("finishSubPropertyStmt", rs2.getString("finishSubPropertyStmt")); @@ -320,4 +327,17 @@ public class Backreference implements EntityFilterInterface, QueryInterface { public void addBenchmark(final String str, final long time) { this.query.addBenchmark(this.getClass().getSimpleName() + "." + str, time); } + + @Override + public boolean isVersioned() { + return this.query.isVersioned(); + } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append(toString()); + if (this.hasSubProperty()) sb.append(getSubProperty().getCacheKey()); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/CQLLexer.g4 b/src/main/java/org/caosdb/server/query/CQLLexer.g4 index 62cb12c09c00a2f0005a8b9e2b7d5cc8f6b753e5..85061d30cacb222dd8c866d84a7abfb525a592a9 100644 --- a/src/main/java/org/caosdb/server/query/CQLLexer.g4 +++ b/src/main/java/org/caosdb/server/query/CQLLexer.g4 @@ -23,77 +23,100 @@ lexer grammar CQLLexer; AS_A: - [Aa][Ss] (EMPTY_SPACE? A)? + [Aa][Ss] (WHITE_SPACE_f? A)? WHITE_SPACE_f? ; IS_REFERENCED: - (IS_f EMPTY_SPACE?)? [Rr][Ee][Ff][Ee][Rr][Ee][Nn][Cc][Ee][Dd] + (IS_f WHITE_SPACE_f?)? [Rr][Ee][Ff][Ee][Rr][Ee][Nn][Cc][Ee][Dd] WHITE_SPACE_f? ; BY: - [Bb][Yy] + [Bb][Yy] WHITE_SPACE_f? +; + +fragment +OF_f: + [Oo][Ff] +; + +fragment +ANY_f: + [Aa][Nn][Yy] +; + +fragment +VERSION_f: + [Vv][Ee][Rr][Ss][Ii][Oo][Nn] +; + +ANY_VERSION_OF: + (ANY_f WHITE_SPACE_f VERSION_f WHITE_SPACE_f OF_f) WHITE_SPACE_f? ; SELECT: - [Ss][Ee][Ll][Ee][Cc][Tt] -> pushMode(SELECT_MODE) + [Ss][Ee][Ll][Ee][Cc][Tt] WHITE_SPACE_f? -> pushMode(SELECT_MODE) ; INSERTED: - [Ii][Nn][Ss][Ee][Rr][Tt][Ee][Dd] + [Ii][Nn][Ss][Ee][Rr][Tt][Ee][Dd] WHITE_SPACE_f? ; CREATED: - [Cc][Rr][Ee][Aa][Tt][Ee][Dd] + [Cc][Rr][Ee][Aa][Tt][Ee][Dd] WHITE_SPACE_f? ; UPDATED: - [Uu][Pp][Dd][Aa][Tt][Ee][Dd] + [Uu][Pp][Dd][Aa][Tt][Ee][Dd] WHITE_SPACE_f? ; ON: - [Oo][Nn] + [Oo][Nn] WHITE_SPACE_f? ; IN: - [Ii][Nn] + [Ii][Nn] WHITE_SPACE_f? ; IS_STORED_AT: - (IS_f EMPTY_SPACE?)? [Ss][Tt][Oo][Rr][Ee][Dd] (EMPTY_SPACE? AT)? + (IS_f WHITE_SPACE_f?)? [Ss][Tt][Oo][Rr][Ee][Dd] (WHITE_SPACE_f? AT)? WHITE_SPACE_f? ; AT: - [Aa][Tt] + [Aa][Tt] WHITE_SPACE_f? ; FIND: - [Ff][Ii][Nn][Dd] + [Ff][Ii][Nn][Dd] WHITE_SPACE_f? ; COUNT: - [Cc][Oo][Uu][Nn][Tt] + [Cc][Oo][Uu][Nn][Tt] WHITE_SPACE_f? ; AND: - ( - [Aa][Nn][Dd] - ) - | '&' + ( + ( + [Aa][Nn][Dd] + ) + | '&' + ) WHITE_SPACE_f? ; OR: - ( - [Oo][Rr] - ) - | '|' + ( + ( + [Oo][Rr] + ) + | '|' + ) WHITE_SPACE_f? ; LPAREN: - '(' + '(' WHITE_SPACE_f? ; RPAREN: - ')' + ')' WHITE_SPACE_f? ; SINGLE_QUOTE_START: @@ -112,19 +135,19 @@ OPERATOR: | '>' | '!=' | '->' - | [Rr][Ee][Ff][Ee][Rr][Ee][Nn][Cc][Ee]([Ss]|EMPTY_SPACE? [Tt][Oo]) (EMPTY_SPACE? A {_input.LA(1) == ' '}?)? {setText("->");} + | [Rr][Ee][Ff][Ee][Rr][Ee][Nn][Cc][Ee]([Ss]|WHITE_SPACE_f? [Tt][Oo]) (WHITE_SPACE_f? A {_input.LA(1) == ' '}?)? {setText("->");} ; LIKE: - [Ll][Ii][Kk][Ee] + [Ll][Ii][Kk][Ee] WHITE_SPACE_f? ; IS_NULL: - IS_f EMPTY_SPACE NULL_f + IS_f WHITE_SPACE_f NULL_f WHITE_SPACE_f? ; IS_NOT_NULL: - IS_f EMPTY_SPACE NOT_f EMPTY_SPACE NULL_f + IS_f WHITE_SPACE_f NOT_f WHITE_SPACE_f NULL_f WHITE_SPACE_f? ; fragment @@ -144,7 +167,7 @@ NOT_f: fragment DOESNT_f: - DOES_f EMPTY_SPACE? NOT_f + DOES_f WHITE_SPACE_f? NOT_f | DOES_f [Nn] SINGLE_QUOTE [Tt] ; @@ -225,88 +248,92 @@ WASNT_f: ; NEGATION: - '!' - | DOESNT_f (WHITE_SPACE? HAVE_A_f)? - | DONT_f (WHITE_SPACE? HAVE_A_f)? - | HASNT_f (WHITE_SPACE? BEEN_f)? - | ISNT_f (WHITE_SPACE? BEEN_f)? - | NOT_f (WHITE_SPACE? BEEN_f)? - | WERENT_f (WHITE_SPACE? BEEN_f)? - | WASNT_f (WHITE_SPACE? BEEN_f)? - | HAVENT_f (WHITE_SPACE? BEEN_f)? - | HADNT_f (WHITE_SPACE? BEEN_f)? + ( + '!' + | DOESNT_f (WHITE_SPACE? HAVE_A_f)? + | DONT_f (WHITE_SPACE? HAVE_A_f)? + | HASNT_f (WHITE_SPACE? BEEN_f)? + | ISNT_f (WHITE_SPACE? BEEN_f)? + | NOT_f (WHITE_SPACE? BEEN_f)? + | WERENT_f (WHITE_SPACE? BEEN_f)? + | WASNT_f (WHITE_SPACE? BEEN_f)? + | HAVENT_f (WHITE_SPACE? BEEN_f)? + | HADNT_f (WHITE_SPACE? BEEN_f)? + ) WHITE_SPACE_f? ; WITH: - [Ww][Ii][Tt][Hh] + [Ww][Ii][Tt][Hh] WHITE_SPACE_f? ; THE: - [Tt][Hh][Ee] + [Tt][Hh][Ee] WHITE_SPACE_f? ; GREATEST: - [Gg][Rr][Ee][Aa][Tt][Ee][Ss][Tt] + [Gg][Rr][Ee][Aa][Tt][Ee][Ss][Tt] WHITE_SPACE_f? ; SMALLEST: - [Ss][Mm][Aa][Ll][Ll][Ee][Ss][Tt] + [Ss][Mm][Aa][Ll][Ll][Ee][Ss][Tt] WHITE_SPACE_f? ; A: - [Aa][Nn]? + [Aa][Nn]? WHITE_SPACE_f? ; ME: - [Mm][Ee] + [Mm][Ee] WHITE_SPACE_f? ; SOMEONE: - [Ss][Oo][Mm][Ee][Oo][Nn][Ee] + [Ss][Oo][Mm][Ee][Oo][Nn][Ee] WHITE_SPACE_f? ; ELSE: - [Ee][Ll][Ss][Ee] + [Ee][Ll][Ss][Ee] WHITE_SPACE_f? ; WHERE: - [Ww][Hh][Ee][Rr][Ee] + [Ww][Hh][Ee][Rr][Ee] WHITE_SPACE_f? ; WHICH: - [Ww][Hh][Ii][Cc][Hh] + [Ww][Hh][Ii][Cc][Hh] WHITE_SPACE_f? ; HAS_A: - (HAS_f | HAD_f | HAVE_f | WERE_f | WAS_f | IS_f) ( - (EMPTY_SPACE? A) - | (EMPTY_SPACE? BEEN_f) - )? + (HAS_f | HAD_f | HAVE_f | WERE_f | WAS_f | IS_f) + ( + (WHITE_SPACE_f? A) + | (WHITE_SPACE_f? BEEN_f) + )? + ) WHITE_SPACE_f? ; PROPERTY: - [Pp][Rr][Oo][Pp][Ee][Rr][Tt]([Yy]|[Ii][Ee][Ss]) + [Pp][Rr][Oo][Pp][Ee][Rr][Tt]([Yy]|[Ii][Ee][Ss]) WHITE_SPACE_f? ; RECORDTYPE: - [Rr][Ee][Cc][Oo][Rr][Dd][Tt][Yy][Pp][Ee]([Ss])? + [Rr][Ee][Cc][Oo][Rr][Dd][Tt][Yy][Pp][Ee]([Ss])? WHITE_SPACE_f? ; RECORD: - [Rr][Ee][Cc][Oo][Rr][Dd]([Ss])? + [Rr][Ee][Cc][Oo][Rr][Dd]([Ss])? WHITE_SPACE_f? ; FILE: - [Ff][Ii][Ll][Ee]([Ss])? + [Ff][Ii][Ll][Ee]([Ss])? WHITE_SPACE_f? ; ENTITY: - [Ee][Nn][Tt][Ii][Tt]([Yy]|[Ii][Ee][Ss]) + [Ee][Nn][Tt][Ii][Tt]([Yy]|[Ii][Ee][Ss]) WHITE_SPACE_f? ; QUERYTEMPLATE: - [Qq][Uu][Ee][Rr][yY][Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee] + [Qq][Uu][Ee][Rr][yY][Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee] WHITE_SPACE_f? ; fragment @@ -315,12 +342,12 @@ IS_f: ; fragment -EMPTY_SPACE: +WHITE_SPACE_f: [ \t\n\r]+ ; WHITE_SPACE: - [ \t\n\r]+ -> channel(HIDDEN) + [ \t\n\r]+ ; fragment @@ -346,12 +373,11 @@ REGEXP_END: ; ID: - [Ii][Dd] + [Ii][Dd] WHITE_SPACE_f? ; -// Multiple slashes should be allowed in paths instead of a single slash. -SLASHES: - '/'+ +SLASH: + '/' ; STAR: @@ -363,26 +389,26 @@ DOT: ; QMARK: - '?' + '?' WHITE_SPACE_f? ; BUT: - [Bb][Uu][Tt] + [Bb][Uu][Tt] WHITE_SPACE_f? ; ESC_REGEXP_END: ESC_MARKER - '>>' + '>>' WHITE_SPACE_f? ; ESC_STAR: ESC_MARKER - '*' + '*' WHITE_SPACE_f? ; ESC_BS: ESC_MARKER - '\\' + '\\' WHITE_SPACE_f? ; fragment @@ -391,7 +417,7 @@ ESC_MARKER: ; TODAY: - [Tt][Oo][Dd][Aa][Yy] + [Tt][Oo][Dd][Aa][Yy] WHITE_SPACE_f? ; HYPHEN: @@ -403,11 +429,17 @@ COLON: ; NUM: + NUM_f + | DOT NUM_f + | NUM_f DOT NUM_f +; + +NUM_f: ('0'..'9')+ ; TXT: - ('a'..'z' | 'A'..'Z' | '_' | '-' {_input.LA(1) != '>'}? | '+' | '&' | ';' | ',' | '$' | ':' | '%' | '^' | '~' {_input.LA(1) != '='}? | '`' | '´' | 'ö' | 'ä' | 'ß' | 'ü' | 'Ö' | 'Ä' | 'Ü' | '@' | '[' | ']' | '{' | '}' )+ + ('a'..'z' | 'A'..'Z' | '0'..'9' | '_' | '-' {_input.LA(1) != '>'}? | '+' | '&' | ';' | ',' | '$' | ':' | '%' | '^' | '~' {_input.LA(1) != '='}? | '`' | '´' | 'ö' | 'ä' | 'ß' | 'ü' | 'Ö' | 'Ä' | 'Ü' | '@' | '[' | ']' | '{' | '}' )+ ; UNKNOWN_CHAR: . ; @@ -454,7 +486,7 @@ mode DOUBLE_QUOTE_MODE; mode SELECT_MODE; FROM: - [Ff][Rr][Oo][Mm] -> mode(DEFAULT_MODE) + [Ff][Rr][Oo][Mm]([ \t\n\r])? -> mode(DEFAULT_MODE) ; SELECT_DOT: diff --git a/src/main/java/org/caosdb/server/query/CQLParser.g4 b/src/main/java/org/caosdb/server/query/CQLParser.g4 index 8b5e5a94aaea5d62ba60068a1ad652f7b51f1bd4..452321c670d4101bc603e657023143025f12c22f 100644 --- a/src/main/java/org/caosdb/server/query/CQLParser.g4 +++ b/src/main/java/org/caosdb/server/query/CQLParser.g4 @@ -31,11 +31,12 @@ options { tokenVocab = CQLLexer; } import java.util.List; } -cq returns [Query.Type t, List<Query.Selection> s, Query.Pattern e, Query.Role r, EntityFilterInterface filter] +cq returns [Query.Type t, List<Query.Selection> s, Query.Pattern e, Query.Role r, EntityFilterInterface filter, VersionFilter v] @init{ $s = null; $e = null; $r = null; + $v = VersionFilter.UNVERSIONED; $filter = null; } : @@ -44,18 +45,27 @@ cq returns [Query.Type t, List<Query.Selection> s, Query.Pattern e, Query.Role r SELECT prop_sel {$s = $prop_sel.s;} FROM {$t = Query.Type.FIND;} | FIND {$t = Query.Type.FIND;} | COUNT {$t = Query.Type.COUNT;}) - ( - ( - role {$r = $role.r;} - (entity {$e = $entity.ep;})? - ) | entity {$e = $entity.ep;} - ) + (version {$v = $version.v;})? + ( role {$r = $role.r;} )? ( entity_filter {$filter = $entity_filter.filter;} - )?? + | + entity WHITE_SPACE?{$e = $entity.ep;} + ( + entity_filter {$filter = $entity_filter.filter;} + )? + )? EOF ; +version returns [VersionFilter v] + @init{ + $v = null; + } +: + ANY_VERSION_OF {$v = VersionFilter.ANY_VERSION;} +; + prop_sel returns [List<Query.Selection> s] @init{ $s = new LinkedList<Query.Selection>(); @@ -90,7 +100,7 @@ entity_filter returns [EntityFilterInterface filter] which_exp ( ( - LPAREN + LPAREN WHITE_SPACE? ( filter_expression {$filter = $filter_expression.efi;} | conjunction {$filter = $conjunction.c;} @@ -110,7 +120,7 @@ which_exp: | HAS_A (PROPERTY)? | WITH (A (PROPERTY)?)? | WHERE - | DOT + | DOT WHITE_SPACE? ; filter_expression returns [EntityFilterInterface efi] @@ -138,6 +148,7 @@ idfilter returns [IDFilter filter] locals [String o, String v, String a] ID ( OPERATOR {$o = $OPERATOR.text;} + WHITE_SPACE? value {$v = $value.str;} )? ; @@ -185,15 +196,14 @@ username returns [Query.Pattern ep] locals [int type] $ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL); } : - ( STAR {$type = Query.Pattern.TYPE_LIKE;} | ESC_STAR | TXT | DOT | ESC_REGEXP_END | COLON )+ + ( STAR {$type = Query.Pattern.TYPE_LIKE;} | ~(STAR | WHITE_SPACE) )+ ; transaction_time returns [String tqp] : ( (ON | IN) - (value {$tqp = $value.text;} - | entity {$tqp = $entity.ep.toString();}) + (value {$tqp = $value.text;}) ) | TODAY {$tqp = TransactionFilter.TODAY;} ; @@ -239,10 +249,10 @@ pov returns [POV filter] locals [Query.Pattern p, String o, String v, String a] property {$p = $property.pp; $a=$property.agg;} ( ( - LIKE {$o = $LIKE.text;} + LIKE {$o = $LIKE.text.trim();} ( like_pattern {$v = $like_pattern.ep.toString();} | value {$v = $value.str;} ) - | OPERATOR {$o = $OPERATOR.text;} value {$v = $value.str;} + | OPERATOR {$o = $OPERATOR.text;} WHITE_SPACE? value {$v = $value.str;} ) | IS_NULL {$o = "0";} | IS_NOT_NULL {$o = "!0";} @@ -256,12 +266,12 @@ pov returns [POV filter] locals [Query.Pattern p, String o, String v, String a] ( like_pattern {$v = $like_pattern.ep.toString();} | value {$v = $value.str;} ) ) - | ( OPERATOR {$o = $OPERATOR.text;} value {$v = $value.str;} + | ( OPERATOR {$o = $OPERATOR.text;} WHITE_SPACE? value {$v = $value.str;} ( AS_A property {$p = $property.pp;} )? ) ) - + WHITE_SPACE? ; @@ -286,9 +296,11 @@ backreference returns [Backreference ref] locals [Query.Pattern e, Query.Pattern IS_REFERENCED (BY A? entity {$e=$entity.ep;})? ( + WHITE_SPACE? AS_A property {$p=$property.pp;} )? + WHITE_SPACE? ; storedat returns [StoredAt filter] locals [String loc] @@ -301,6 +313,7 @@ storedat returns [StoredAt filter] locals [String loc] : IS_STORED_AT location {$loc = $location.str;} + WHITE_SPACE? ; conjunction returns [Conjunction c] locals [Conjunction dummy] @@ -311,11 +324,16 @@ conjunction returns [Conjunction c] locals [Conjunction dummy] ( f1 = filter_expression {$c.add($f1.efi);} | - LPAREN - f4 = filter_expression {$c.add($f4.efi);} + LPAREN WHITE_SPACE? + ( + f4 = filter_expression {$c.add($f4.efi);} + | disjunction {$c.add($disjunction.d);} + | c3=conjunction {$c.addAll($c3.c);} + ) RPAREN ) ( + WHITE_SPACE? AND ( ( which_exp | A (PROPERTY)?? ) @@ -323,11 +341,11 @@ conjunction returns [Conjunction c] locals [Conjunction dummy] ( f2 = filter_expression {$c.add($f2.efi);} | ( - LPAREN + LPAREN WHITE_SPACE? ( f3 = filter_expression {$c.add($f3.efi);} | disjunction {$c.add($disjunction.d);} - | c2=conjunction {$c.addAll($c2.c);} + | c2=conjunction {$c.addAll($c2.c);} ) RPAREN ) @@ -343,8 +361,12 @@ disjunction returns [Disjunction d] ( f1 = filter_expression {$d.add($f1.efi);} | - LPAREN - f4 = filter_expression {$d.add($f4.efi);} + LPAREN WHITE_SPACE? + ( + f4 = filter_expression {$d.add($f4.efi);} + | conjunction {$d.add($conjunction.c);} + | d3 = disjunction {$d.addAll($d3.d);} + ) RPAREN ) ( @@ -355,7 +377,7 @@ disjunction returns [Disjunction d] ( f2 = filter_expression {$d.add($f2.efi);} | ( - LPAREN + LPAREN WHITE_SPACE? ( f3 = filter_expression {$d.add($f3.efi);} | conjunction {$d.add($conjunction.c);} @@ -375,7 +397,7 @@ negation returns [Negation n] ( f1 = filter_expression {$n = new Negation($f1.efi);} | ( - LPAREN + LPAREN WHITE_SPACE? ( f2 = filter_expression {$n = new Negation($f2.efi);} | disjunction {$n = new Negation($disjunction.d);} @@ -390,9 +412,10 @@ entity returns [Query.Pattern ep] : regexp_pattern {$ep = $regexp_pattern.ep;} | like_pattern {$ep = $like_pattern.ep;} - | ( double_quoted {$ep = $double_quoted.ep;} ) + | ( double_quoted {$ep = $double_quoted.ep;} ) | ( single_quoted {$ep = $single_quoted.ep;} ) - | (~(SINGLE_QUOTE_START|DOUBLE_QUOTE_START|WHITE_SPACE|WHICH|DOT|WHERE|HAS_A|WITH|STAR|AND|OR))+? {$ep = new Query.Pattern((String) $text, Query.Pattern.TYPE_NORMAL);} + | ENTITY {$ep = new Query.Pattern((String) $text.trim(), Query.Pattern.TYPE_NORMAL);} + | ( ~(ENTITY |WHITE_SPACE | DOT) )+ {$ep = new Query.Pattern((String) $text.trim(), Query.Pattern.TYPE_NORMAL);} ; regexp_pattern returns [Query.Pattern ep] locals [StringBuffer sb] @@ -410,12 +433,10 @@ like_pattern returns [Query.Pattern ep] locals [StringBuffer sb] $sb = new StringBuffer(); } : - - (m=~(WHITE_SPACE|WHICH|DOT|WHERE|HAS_A|WITH|STAR|LIKE|OPERATOR|AS_A|AND|OR|IS_STORED_AT|IS_REFERENCED) {$sb.append($m.text);})*? - ( - STAR {$sb.append('*');} - (m=~(WHITE_SPACE|WHICH|DOT|WHERE|HAS_A|WITH|STAR|LIKE|OPERATOR|AS_A|AND|OR) {$sb.append($m.text);})*? - )+? {$ep = new Query.Pattern((String) $sb.toString(), Query.Pattern.TYPE_LIKE);} + ~(WHITE_SPACE|DOT|LIKE|OPERATOR|AS_A|AND|OR|IS_STORED_AT|IS_REFERENCED)*? + STAR + ~(WHITE_SPACE|DOT|STAR)*? + {$ep = new Query.Pattern((String) $text, Query.Pattern.TYPE_LIKE);} ; property returns [Query.Pattern pp, String agg]locals [StringBuffer sb] @@ -428,10 +449,11 @@ property returns [Query.Pattern pp, String agg]locals [StringBuffer sb] ( regexp_pattern {$pp = $regexp_pattern.ep;} | like_pattern {$pp = $like_pattern.ep;} - | ( double_quoted {$pp = $double_quoted.ep;} ) + | ( double_quoted {$pp = $double_quoted.ep;} ) | ( single_quoted {$pp = $single_quoted.ep;} ) | ((m=TXT | m=NUM | m=REGEXP_MARKER | m=ENTITY){$sb.append($m.text);})+ {$pp = new Query.Pattern($sb.toString(), Query.Pattern.TYPE_NORMAL);} ) + WHITE_SPACE? ; minmax returns [String agg] @@ -444,29 +466,37 @@ minmax returns [String agg] value returns [String str] : - number {$str = $text;} + number_with_unit {$str = $number_with_unit.text;} | datetime {$str = $datetime.text;} | atom {$str = $atom.ep.toString();} + WHITE_SPACE? +; + +number_with_unit +: + HYPHEN?? + NUM + (WHITE_SPACE?? unit)? ; -number +unit : - HYPHEN?? NUM* (DOT NUM+)? + TXT + | NUM SLASH TXT ; location returns [String str] : atom {$str = $atom.ep.str;} | - SLASHES ? - ((WHICH | WITH)+ SLASHES |( TXT | COLON | HYPHEN | NUM | DOT | ESC_STAR | ESC_BS | ESC_REGEXP_END | STAR )+ SLASHES ?)* {$str = $text; } + (~WHITE_SPACE)+ {$str = $text; } ; atom returns [Query.Pattern ep] : double_quoted {$ep = $double_quoted.ep;} | single_quoted {$ep = $single_quoted.ep;} - | (TXT | NUM | REGEXP_MARKER | STAR | A )+ {$ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL);} + | (~(WHITE_SPACE | DOT ))+ {$ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL);} ; single_quoted returns [Query.Pattern ep] locals [StringBuffer sb, int patternType] diff --git a/src/main/java/org/caosdb/server/query/Conjunction.java b/src/main/java/org/caosdb/server/query/Conjunction.java index a8fd9cd16296dfdf846bd81d2c25676537cd590c..03b331242e239a26f108973ca7a37624ab80ea64 100644 --- a/src/main/java/org/caosdb/server/query/Conjunction.java +++ b/src/main/java/org/caosdb/server/query/Conjunction.java @@ -34,7 +34,8 @@ import org.caosdb.server.database.access.Access; import org.caosdb.server.query.Query.QueryException; import org.jdom2.Element; -public class Conjunction extends EntityFilterContainer implements QueryInterface { +public class Conjunction extends EntityFilterContainer + implements QueryInterface, EntityFilterInterface { private String sourceSet = null; private String targetSet = null; @@ -63,8 +64,9 @@ public class Conjunction extends EntityFilterContainer implements QueryInterface // generate empty temporary targetSet if query.getTargetSet() is not // empty anyways. final PreparedStatement callInitEmptyTarget = - getConnection().prepareStatement("call initEmptyTargetSet(?)"); + getConnection().prepareStatement("call initEmptyTargetSet(?,?)"); callInitEmptyTarget.setString(1, query.getTargetSet()); + callInitEmptyTarget.setBoolean(2, query.isVersioned()); final ResultSet initEmptyTargetResultSet = callInitEmptyTarget.executeQuery(); if (initEmptyTargetResultSet.next()) { this.targetSet = bytes2UTF8(initEmptyTargetResultSet.getBytes("newTableName")); @@ -149,4 +151,20 @@ public class Conjunction extends EntityFilterContainer implements QueryInterface public void addBenchmark(final String str, final long time) { this.query.addBenchmark(this.getClass().getSimpleName() + "." + str, time); } + + @Override + public boolean isVersioned() { + return this.query.isVersioned(); + } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("Conj("); + for (EntityFilterInterface filter : getFilters()) { + sb.append(filter.getCacheKey()); + } + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Disjunction.java b/src/main/java/org/caosdb/server/query/Disjunction.java index 3bd2f32fdd3bfb64ad2a07c527b91136e965c22b..edd2e6daf243cd25ea112fdbfb946f2b80ee80b1 100644 --- a/src/main/java/org/caosdb/server/query/Disjunction.java +++ b/src/main/java/org/caosdb/server/query/Disjunction.java @@ -34,7 +34,8 @@ import org.caosdb.server.database.access.Access; import org.caosdb.server.query.Query.QueryException; import org.jdom2.Element; -public class Disjunction extends EntityFilterContainer implements QueryInterface { +public class Disjunction extends EntityFilterContainer + implements QueryInterface, EntityFilterInterface { private String targetSet = null; private int targetSetCount = -1; @@ -51,7 +52,8 @@ public class Disjunction extends EntityFilterContainer implements QueryInterface // targetTable // which will be used to collect the entities. final PreparedStatement callInitDisjunctionFilter = - getConnection().prepareStatement("call initDisjunctionFilter()"); + getConnection().prepareStatement("call initDisjunctionFilter(?)"); + callInitDisjunctionFilter.setBoolean(1, this.isVersioned()); final ResultSet initDisjuntionFilterResultSet = callInitDisjunctionFilter.executeQuery(); if (initDisjuntionFilterResultSet.next()) { this.targetSet = bytes2UTF8(initDisjuntionFilterResultSet.getBytes("newTableName")); @@ -69,11 +71,12 @@ public class Disjunction extends EntityFilterContainer implements QueryInterface if (query.getTargetSet() == null) { // calculate the difference and store to sourceSet - final CallableStatement callFinishNegationFilter = - getConnection().prepareCall("call calcIntersection(?,?)"); - callFinishNegationFilter.setString(1, getSourceSet()); - callFinishNegationFilter.setString(2, this.targetSet); - callFinishNegationFilter.execute(); + final CallableStatement callFinishDisjunctionFilter = + getConnection().prepareCall("call calcIntersection(?,?,?)"); + callFinishDisjunctionFilter.setString(1, getSourceSet()); + callFinishDisjunctionFilter.setString(2, this.targetSet); + callFinishDisjunctionFilter.setBoolean(3, this.isVersioned()); + callFinishDisjunctionFilter.execute(); } // ELSE: the query.getTargetSet() is identical to targetSet and is // not @@ -136,4 +139,20 @@ public class Disjunction extends EntityFilterContainer implements QueryInterface public void addBenchmark(final String str, final long time) { this.query.addBenchmark(this.getClass().getSimpleName() + "." + str, time); } + + @Override + public boolean isVersioned() { + return this.query.isVersioned(); + } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("Disj("); + for (EntityFilterInterface filter : getFilters()) { + sb.append(filter.getCacheKey()); + } + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/EntityFilterContainer.java b/src/main/java/org/caosdb/server/query/EntityFilterContainer.java index c2409f90ecc43cfb318a17cc8a9f3ba6df60beb0..1b270e9b120f2c336381db16c88769862b68a725 100644 --- a/src/main/java/org/caosdb/server/query/EntityFilterContainer.java +++ b/src/main/java/org/caosdb/server/query/EntityFilterContainer.java @@ -24,7 +24,7 @@ package org.caosdb.server.query; import java.util.LinkedList; -public abstract class EntityFilterContainer implements EntityFilterInterface { +public abstract class EntityFilterContainer { private final LinkedList<EntityFilterInterface> filters = new LinkedList<EntityFilterInterface>(); public void add(final EntityFilterInterface filter) { diff --git a/src/main/java/org/caosdb/server/query/EntityFilterInterface.java b/src/main/java/org/caosdb/server/query/EntityFilterInterface.java index 3bf113edbfa6aeb3bbe1c4ead7c2bbca0aca0d03..be8484c2f122ae301ea78a014c771e116b0fc012 100644 --- a/src/main/java/org/caosdb/server/query/EntityFilterInterface.java +++ b/src/main/java/org/caosdb/server/query/EntityFilterInterface.java @@ -30,4 +30,12 @@ public interface EntityFilterInterface { void apply(QueryInterface query) throws QueryException; public Element toElement(); + + /** + * Return a string which can serve as a cache key. The string must describe all relevant + * parameters of the filter. + * + * @return a cache key. + */ + public String getCacheKey(); } diff --git a/src/main/java/org/caosdb/server/query/IDFilter.java b/src/main/java/org/caosdb/server/query/IDFilter.java index 26054bb16f0eb39a581a4fa0da6c62a67c6fc385..ce01f5f242f914c6f6f812f0875332f7ca004c07 100644 --- a/src/main/java/org/caosdb/server/query/IDFilter.java +++ b/src/main/java/org/caosdb/server/query/IDFilter.java @@ -59,7 +59,7 @@ public class IDFilter implements EntityFilterInterface { final Connection connection = query.getConnection(); // applyIDFilter(sourceSet, targetSet, o, vInt, agg) final CallableStatement callIDFilter = - connection.prepareCall("call applyIDFilter(?,?,?,?,?)"); + connection.prepareCall("call applyIDFilter(?,?,?,?,?,?)"); callIDFilter.setString(1, query.getSourceSet()); // sourceSet if (query.getTargetSet() != null) { // targetSet @@ -89,6 +89,8 @@ public class IDFilter implements EntityFilterInterface { callIDFilter.setString(5, getAggregate()); } + // versioning + callIDFilter.setBoolean(6, query.isVersioned()); callIDFilter.execute(); callIDFilter.close(); } catch (final SQLException e) { @@ -123,4 +125,17 @@ public class IDFilter implements EntityFilterInterface { public String getAggregate() { return this.aggregate; } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("ID("); + if (this.aggregate != null) sb.append(this.aggregate); + sb.append(","); + if (this.operator != null) sb.append(this.operator); + sb.append(","); + if (this.value != null) sb.append(this.value); + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Negation.java b/src/main/java/org/caosdb/server/query/Negation.java index d4243580bf52511d05699c7c80e69015ea2a0dac..cfdfd05e286030f525b8d4f3c291e2bdfb27b100 100644 --- a/src/main/java/org/caosdb/server/query/Negation.java +++ b/src/main/java/org/caosdb/server/query/Negation.java @@ -55,14 +55,6 @@ public class Negation implements EntityFilterInterface, QueryInterface { } return this.neg; } - - @Override - public void apply(final QueryInterface query) {} - - @Override - public Element toElement() { - return null; - } } private String targetSet = null; @@ -90,8 +82,8 @@ public class Negation implements EntityFilterInterface, QueryInterface { // generate empty temporary targetSet if query.getTargetSet() is not // empty anyways. final PreparedStatement callInitEmptyTarget = - getConnection().prepareStatement("call initEmptyTargetSet(?)"); - callInitEmptyTarget.setString(1, query.getTargetSet()); + getConnection().prepareStatement("call initDisjunctionFilter(?)"); + callInitEmptyTarget.setBoolean(1, query.isVersioned()); final ResultSet initEmptyTargetResultSet = callInitEmptyTarget.executeQuery(); if (initEmptyTargetResultSet.next()) { this.targetSet = bytes2UTF8(initEmptyTargetResultSet.getBytes("newTableName")); @@ -100,19 +92,22 @@ public class Negation implements EntityFilterInterface, QueryInterface { this.filter.apply(this); - if (query.getTargetSet() != null && !this.targetSet.equalsIgnoreCase(query.getTargetSet())) { - // merge temporary targetSet with query.getTargetSet() + if (query.getTargetSet() == null) { + // intersect temporary targetSet with query.getSourceSet() final CallableStatement callFinishConjunctionFilter = - query.getConnection().prepareCall("call calcUnion(?,?)"); - callFinishConjunctionFilter.setString(1, query.getTargetSet()); + query.getConnection().prepareCall("call calcDifference(?,?,?)"); + callFinishConjunctionFilter.setString(1, query.getSourceSet()); callFinishConjunctionFilter.setString(2, this.targetSet); + callFinishConjunctionFilter.setBoolean(3, this.isVersioned()); callFinishConjunctionFilter.execute(); - } else if (query.getTargetSet() == null) { - // intersect temporary targetSet with query.getSourceSet() + } else { + // merge temporary targetSet with query.getTargetSet() final CallableStatement callFinishConjunctionFilter = - query.getConnection().prepareCall("call calcDifference(?,?)"); - callFinishConjunctionFilter.setString(1, query.getSourceSet()); + query.getConnection().prepareCall("call calcComplementUnion(?,?,?,?)"); + callFinishConjunctionFilter.setString(1, query.getTargetSet()); callFinishConjunctionFilter.setString(2, this.targetSet); + callFinishConjunctionFilter.setString(3, query.getSourceSet()); + callFinishConjunctionFilter.setBoolean(4, this.isVersioned()); callFinishConjunctionFilter.execute(); } } catch (final SQLException e) { @@ -167,4 +162,18 @@ public class Negation implements EntityFilterInterface, QueryInterface { public void addBenchmark(final String str, final long time) { this.query.addBenchmark(this.getClass().getSimpleName() + "." + str, time); } + + @Override + public boolean isVersioned() { + return this.query.isVersioned(); + } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("NEG("); + if (this.filter != null) sb.append(this.filter.getCacheKey()); + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/POV.java b/src/main/java/org/caosdb/server/query/POV.java index 03b603d0df70d1159c1d78f1626e23faab40e033..a1008bd6a67a8712baf2c9b52ab164f619236263 100644 --- a/src/main/java/org/caosdb/server/query/POV.java +++ b/src/main/java/org/caosdb/server/query/POV.java @@ -155,7 +155,7 @@ public class POV implements EntityFilterInterface { this.unit = getUnit(unitStr); } catch (final ParserException e) { e.printStackTrace(); - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("Could not parse the unit."); } this.stdUnitSig = this.unit.normalize().getSignature(); @@ -214,6 +214,10 @@ public class POV implements EntityFilterInterface { @Override public void apply(final QueryInterface query) throws QueryException { + if (query.isVersioned() && hasSubProperty()) { + throw new UnsupportedOperationException( + "Versioned queries are not supported for subqueries yet. Please file a feature request."); + } final long t1 = System.currentTimeMillis(); try { this.connection = query.getConnection(); @@ -226,9 +230,9 @@ public class POV implements EntityFilterInterface { // applyPOV(sourceSet, targetSet, propertiesTable, refIdsTable, o, // vText, vInt, // vDouble, - // vDatetime, vDateTimeDotNotation, agg, pname) + // vDatetime, vDateTimeDotNotation, agg, pname, versioned) final CallableStatement callPOV = - this.connection.prepareCall("call applyPOV(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + this.connection.prepareCall("call applyPOV(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); callPOV.setString(1, query.getSourceSet()); // sourceSet this.statistics.put("sourceSet", query.getSourceSet()); this.statistics.put( @@ -313,6 +317,10 @@ public class POV implements EntityFilterInterface { } if (getAggregate() != null) { // agg + if (query.isVersioned()) { + throw new UnsupportedOperationException( + "Versioned queries are not supported for aggregate functions like GREATES or SMALLEST in the filters."); + } callPOV.setString(14, getAggregate()); } else { callPOV.setNull(14, VARCHAR); @@ -323,6 +331,7 @@ public class POV implements EntityFilterInterface { } else { callPOV.setNull(15, VARCHAR); } + callPOV.setBoolean(16, query.isVersioned()); prefix.add("#executeStmt"); executeStmt(callPOV, query); prefix.pop(); @@ -420,7 +429,9 @@ public class POV implements EntityFilterInterface { if (hasSubProperty() && this.targetSet != null) { try (PreparedStatement stmt = - query.getConnection().prepareStatement("call initEmptyTargetSet(NULL)")) { + query.getConnection().prepareStatement("call initEmptyTargetSet(?, ?)")) { + stmt.setNull(1, VARCHAR); + stmt.setBoolean(2, query.isVersioned()); // generate new targetSet final ResultSet rs = stmt.executeQuery(); if (rs.next()) { @@ -501,4 +512,15 @@ public class POV implements EntityFilterInterface { private String measurement(String m) { return String.join("", prefix) + m; } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + if (this.getAggregate() != null) sb.append(this.aggregate); + sb.append(toString()); + if (this.hasSubProperty()) { + sb.append(getSubProperty().getCacheKey()); + } + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java index c3873cf3b4ba2322373dde13f4dfd72e6a94c7e4..cd323bd972b21a261c84bfa6c16c08ea54558fd0 100644 --- a/src/main/java/org/caosdb/server/query/Query.java +++ b/src/main/java/org/caosdb/server/query/Query.java @@ -24,6 +24,7 @@ package org.caosdb.server.query; import static org.caosdb.server.database.DatabaseUtils.bytes2UTF8; +import java.io.Serializable; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; @@ -38,11 +39,14 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.UUID; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.commons.jcs.access.behavior.ICacheAccess; import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; +import org.caosdb.server.caching.Cache; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; import org.caosdb.server.database.backend.implementation.MySQL.MySQLHelper; @@ -63,6 +67,7 @@ import org.caosdb.server.query.CQLParser.CqContext; import org.caosdb.server.query.CQLParsingErrorListener.ParsingError; import org.caosdb.server.transaction.TransactionInterface; import org.jdom2.Element; +import org.slf4j.Logger; public class Query implements QueryInterface, ToElementable, TransactionInterface { @@ -169,12 +174,45 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } + public static class IdVersionPair { + public IdVersionPair(Integer id, String version) { + this.id = id; + this.version = version; + } + + public Integer id; + public String version; + + @Override + public String toString() { + if (version == null) { + return Integer.toString(id); + } + return Integer.toString(id) + "@" + version; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof IdVersionPair) { + IdVersionPair that = (IdVersionPair) obj; + return this.id == that.id && this.version == that.version; + } + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + } + private static boolean filterEntitiesWithoutRetrievePermisions = !CaosDBServer.getServerProperty( ServerProperties.KEY_QUERY_FILTER_ENTITIES_WITHOUT_RETRIEVE_PERMISSIONS) .equalsIgnoreCase("FALSE"); - List<Integer> resultSet = null; + private Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + List<IdVersionPair> resultSet = null; private final String query; private Pattern entity = null; private Role role = null; @@ -189,6 +227,25 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac private Type type = null; private final ArrayList<ToElementable> messages = new ArrayList<>(); private Access access; + private boolean versioned = false; + private static ICacheAccess<String, Serializable> cache = + Cache.getCache("HIGH_LEVEL_QUERY_CACHE"); + /** Cached=true means that the results of this query have actually been pulled from the cache. */ + private boolean cached = false; + /** + * Cachable=false means that the results of this query cannot be used for the cache, because the + * query evaluation contains complex permission checking which could only be cached on a per-user + * basis (maybe in the future). + */ + private boolean cachable = true; + + /** + * Tags the query cache and is renewed each time the cache is being cleared, i.e. each time the + * database is being updated. + * + * <p>As the name suggests, the idea is similar to the ETag header of the HTTP protocol. + */ + private static String cacheETag = UUID.randomUUID().toString(); public Type getType() { return this.type; @@ -216,7 +273,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac */ private void initResultSetWithNameIDAndChildren() throws SQLException { final CallableStatement callInitEntity = - getConnection().prepareCall("call initEntity(?,?,?,?,?)"); + getConnection().prepareCall("call initEntity(?,?,?,?,?,?)"); try { callInitEntity.setInt(1, Integer.parseInt(this.entity.toString())); @@ -243,6 +300,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac break; } callInitEntity.setString(5, this.sourceSet); + callInitEntity.setBoolean(6, this.versioned); callInitEntity.execute(); callInitEntity.close(); } @@ -260,8 +318,8 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac applyQueryTemplates(this, getSourceSet()); } - if (this.role != null) { - final RoleFilter roleFilter = new RoleFilter(this.role, "="); + if (this.role != null && this.role != Role.ENTITY) { + final RoleFilter roleFilter = new RoleFilter(this.role, "=", this.versioned); roleFilter.apply(this); } @@ -312,7 +370,9 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac final Query subQuery = new Query(q.getValue(), query.getUser()); subQuery.setAccess(query.getAccess()); subQuery.parse(); - final String subResultSet = subQuery.executeStrategy(); + + // versioning for QueryTemplates is not supported and probably never will. + final String subResultSet = subQuery.executeStrategy(false); // ... and merge the resultSets. union(query, resultSet, subResultSet); @@ -384,7 +444,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac // filter by role if (this.role != null && this.role != Role.ENTITY) { - final RoleFilter roleFilter = new RoleFilter(this.role, "="); + final RoleFilter roleFilter = new RoleFilter(this.role, "=", this.versioned); roleFilter.apply(this); } @@ -402,8 +462,9 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } - private String initQuery() throws QueryException { - try (final CallableStatement callInitQuery = getConnection().prepareCall("call initQuery()")) { + private String initQuery(boolean versioned) throws QueryException { + String sql = "call initQuery(" + versioned + ")"; + try (final CallableStatement callInitQuery = getConnection().prepareCall(sql)) { ResultSet initQueryResult = null; initQueryResult = callInitQuery.executeQuery(); if (!initQueryResult.next()) { @@ -415,7 +476,13 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } - public void parse() throws ParsingException { + /** + * Parse the query and run optimize() if the parameter `optimize` is true. + * + * @param optimize whether to run optimize() immediately. + * @throws ParsingException + */ + public void parse(boolean optimize) throws ParsingException { final long t1 = System.currentTimeMillis(); CQLLexer lexer; lexer = new CQLLexer(CharStreams.fromString(this.query)); @@ -431,6 +498,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } this.entity = cq.e; + this.versioned = cq.v != VersionFilter.UNVERSIONED; this.role = cq.r; this.parseTree = cq.toStringTree(parser); this.type = cq.t; @@ -440,25 +508,102 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac if (t2 - t1 > 1000) { addBenchmark("LONG_PARSING: " + this.query, t2 - t1); } + + if (optimize) { + optimize(); + } } - private String executeStrategy() throws QueryException { + /** + * Parse the query and run optimize() immediately. + * + * @throws ParsingException + */ + public void parse() throws ParsingException { + parse(true); + } + + /** + * Optimize the query after parsing. The optimization is purely based on formal rules. + * + * <p>Implemented rules: + * + * <ol> + * <li>FIND * -> FIND ENTITY (which basically prevents to copy the complete entity table just to + * read out the ids immediately). + * </ol> + */ + public void optimize() { + // basic optimization + if (this.entity != null + && this.entity.type == Pattern.TYPE_LIKE + && this.entity.str.equals("*")) { + this.entity = null; + if (this.role == null) { + this.role = Role.ENTITY; + } + } + } + + private String executeStrategy(boolean versioned) throws QueryException { if (this.entity != null) { - return sourceStrategy(initQuery()); + return sourceStrategy(initQuery(versioned)); + } else if (this.role == Role.ENTITY && this.filter == null) { + return "entities"; } else { - return targetStrategy(initQuery()); + return targetStrategy(initQuery(versioned)); } } - private LinkedList<Integer> getResultSet(final String resultSetTableName) throws QueryException { + /** + * Generate a SQL statement which reads out the resulting ids (and version ids if `versioned` is + * true). + * + * <p>If the parameter `resultSetTableName` is "entities" actually the entity_version table is + * used to fetch all ids. + * + * @param resultSetTableName name of the table with all the resulting entities + * @param versioned whether the query was versioned + * @return an SQL statement + * @throws QueryException + */ + private String generateSelectStatementForResultSet( + final String resultSetTableName, boolean versioned) { + if (resultSetTableName.equals("entities")) { + return "SELECT entity_id AS id" + + (versioned ? ", version AS version" : "") + + " FROM entity_version" + + (versioned ? "" : " WHERE `_iversion` = 1"); + } + return "SELECT results.id AS id" + + (versioned ? ", ev.version AS version" : "") + + " FROM `" + + resultSetTableName + + "` AS results" + + (versioned + ? " JOIN entity_version AS ev ON (results.id = ev.entity_id AND results._iversion = ev._iversion)" + : ""); + } + + /** + * Return a list of all resulting entities (versions of entities if `versioned` is true). + * + * @param resultSetTableName name of the table with all the resulting entities + * @param versioned whether the query was versioned + * @return list of results of this query. + * @throws QueryException + */ + private List<IdVersionPair> getResultSet(final String resultSetTableName, boolean versioned) + throws QueryException { ResultSet finishResultSet = null; try { - final PreparedStatement finish = - getConnection().prepareStatement("Select id from `" + resultSetTableName + "`"); + final String sql = generateSelectStatementForResultSet(resultSetTableName, versioned); + final PreparedStatement finish = getConnection().prepareStatement(sql); finishResultSet = finish.executeQuery(); - final LinkedList<Integer> rs = new LinkedList<Integer>(); + final List<IdVersionPair> rs = new LinkedList<>(); while (finishResultSet.next()) { - rs.add(finishResultSet.getInt("id")); + final String version = versioned ? finishResultSet.getString("version") : null; + rs.add(new IdVersionPair(finishResultSet.getInt("id"), version)); } return rs; } catch (final SQLException e) { @@ -474,31 +619,98 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } + /** + * Whether the transaction allows this query instance to use the query cache. This is controlled + * by the "cache" flag. + * + * @see {@link NoCache} + * @return true if caching is encouraged. + */ + private boolean useCache() { + return getAccess().useCache(); + } + + /** + * Execute the query. + * + * <p>First try the cache and only then use the back-end. + * + * @param access + * @return + * @throws ParsingException + */ public Query execute(final Access access) throws ParsingException { - setAccess(access); parse(); + setAccess(access); + if (useCache()) { + this.resultSet = getCached(getCacheKey()); + } - try { - - this.resultSet = getResultSet(executeStrategy()); + if (this.resultSet == null) { + executeNoCache(access); + if (this.cachable) { + setCache(getCacheKey(), this.resultSet); + } + this.logger.debug("Uncached query {}", this.query); + } else { + this.logger.debug("Using cached result for {}", this.query); + this.cached = true; + } - filterEntitiesWithoutRetrievePermission(this.resultSet); + this.resultSet = filterEntitiesWithoutRetrievePermission(this.resultSet); - // Fill resulting entities into container - if (this.container != null && this.type == Type.FIND) { - for (final int id : this.resultSet) { + // Fill resulting entities into container + if (this.container != null && this.type == Type.FIND) { + for (final IdVersionPair p : this.resultSet) { - final Entity e = new RetrieveEntity(id); + final Entity e = new RetrieveEntity(p.id, p.version); - // if query has select-clause: - if (this.selections != null && !this.selections.isEmpty()) { - e.addSelections(this.selections); - } - this.container.add(e); + // if query has select-clause: + if (this.selections != null && !this.selections.isEmpty()) { + e.addSelections(this.selections); } + this.container.add(e); } - return this; + } + return this; + } + + /** Remove all cached queries from the cache. */ + public static void clearCache() { + cacheETag = UUID.randomUUID().toString(); + cache.clear(); + } + + /** + * Cache a query result. + * + * @param key + * @param resultSet + */ + private void setCache(String key, List<IdVersionPair> resultSet) { + synchronized (cache) { + if (resultSet instanceof Serializable) { + cache.put(key, (Serializable) resultSet); + } else { + cache.put(key, new ArrayList<>(resultSet)); + } + } + } + + /** + * Retrieve a result set of entity ids (and the version) from the cache. + * + * @param key + * @return + */ + @SuppressWarnings("unchecked") + private List<IdVersionPair> getCached(String key) { + return (List<IdVersionPair>) cache.get(key); + } + protected void executeNoCache(Access access) { + try { + this.resultSet = getResultSet(executeStrategy(this.versioned), this.versioned); } finally { cleanUp(); } @@ -538,31 +750,31 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * Filter out all entities which may not be retrieved by this user due to a missing RETRIEVE * permission. This one is also designed for filtering of intermediate results. * - * @param query * @param resultSet * @throws SQLException * @throws TransactionException */ - public void filterEntitiesWithoutRetrievePermission( - final QueryInterface query, final String resultSet) + public void filterEntitiesWithoutRetrievePermission(final String resultSet) throws SQLException, TransactionException { if (!filterEntitiesWithoutRetrievePermisions) { return; } - try (final Statement stmt = query.getConnection().createStatement()) { + cachable = false; + try (final Statement stmt = this.getConnection().createStatement()) { final ResultSet rs = stmt.executeQuery("SELECT id from `" + resultSet + "`"); final List<Integer> toBeDeleted = new LinkedList<Integer>(); while (rs.next()) { final long t1 = System.currentTimeMillis(); final Integer id = rs.getInt("id"); - if (!execute(new RetrieveSparseEntity(id, null), query.getAccess()) - .getEntity() - .getEntityACL() - .isPermitted(query.getUser(), EntityPermission.RETRIEVE_ENTITY)) { + if (id > 99 + && !execute(new RetrieveSparseEntity(id, null), this.getAccess()) + .getEntity() + .getEntityACL() + .isPermitted(this.getUser(), EntityPermission.RETRIEVE_ENTITY)) { toBeDeleted.add(id); } final long t2 = System.currentTimeMillis(); - query.addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); + this.addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); } rs.close(); for (final Integer id : toBeDeleted) { @@ -578,25 +790,30 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * * @param entities * @throws TransactionException + * @return the filtered list. */ - private void filterEntitiesWithoutRetrievePermission(final List<Integer> entities) - throws TransactionException { + private List<IdVersionPair> filterEntitiesWithoutRetrievePermission( + final List<IdVersionPair> entities) throws TransactionException { if (!filterEntitiesWithoutRetrievePermisions) { - return; + return entities; } - final Iterator<Integer> iterator = entities.iterator(); + + List<IdVersionPair> result = new ArrayList<>(); + final Iterator<IdVersionPair> iterator = entities.iterator(); while (iterator.hasNext()) { final long t1 = System.currentTimeMillis(); - final Integer id = iterator.next(); - if (!execute(new RetrieveSparseEntity(id, null), getAccess()) - .getEntity() - .getEntityACL() - .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) { - iterator.remove(); + final IdVersionPair next = iterator.next(); + if (next.id > 99 + && execute(new RetrieveSparseEntity(next.id, next.version), getAccess()) + .getEntity() + .getEntityACL() + .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) { + result.add(next); } final long t2 = System.currentTimeMillis(); addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); } + return result; } @Override @@ -604,10 +821,6 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac return this.query; } - public List<Integer> getResultSet() { - return this.resultSet; - } - @Override public String getSourceSet() { return this.sourceSet; @@ -657,6 +870,8 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } else { ret.setAttribute("results", "0"); } + ret.setAttribute("cached", Boolean.toString(this.cached)); + ret.setAttribute("etag", cacheETag); final Element parseTreeElem = new Element("ParseTree"); if (this.el.hasErrors()) { @@ -742,4 +957,44 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } return benchmark; } + + @Override + public boolean isVersioned() { + return this.versioned; + } + + /** + * Return a key for the query cache. The key should describe the query with all the filters but + * without the FIND, COUNT and SELECT ... FROM parts. + * + * @return A Cache key. + */ + String getCacheKey() { + StringBuilder sb = new StringBuilder(); + if (this.versioned) sb.append("versioned"); + if (this.role != null) sb.append(this.role.toString()); + if (this.entity != null) sb.append(this.entity.toString()); + if (this.filter != null) sb.append(this.filter.getCacheKey()); + return sb.toString(); + } + + public Pattern getEntity() { + return this.entity; + } + + public Role getRole() { + return this.role; + } + + /** + * Return the ETag. + * + * <p>The ETag tags the query cache and is renewed each time the cache is being cleared, i.e. each + * time the database is being updated. + * + * @return The ETag + */ + public static String getETag() { + return cacheETag; + } } diff --git a/src/main/java/org/caosdb/server/query/QueryInterface.java b/src/main/java/org/caosdb/server/query/QueryInterface.java index 32cc1c7d8d98b56f80b159fc3f0004a491773563..0f2db9a97094ddc26062a9c9922907c183a26413 100644 --- a/src/main/java/org/caosdb/server/query/QueryInterface.java +++ b/src/main/java/org/caosdb/server/query/QueryInterface.java @@ -43,4 +43,6 @@ public interface QueryInterface { public Subject getUser(); public void addBenchmark(final String str, final long time); + + public boolean isVersioned(); } diff --git a/src/main/java/org/caosdb/server/query/RoleFilter.java b/src/main/java/org/caosdb/server/query/RoleFilter.java index 9055dccb358dd94ec17dee8b26bac97d2170b016..97d1b1eab3bcc9e21adab4fa5e2a5204411e58fb 100644 --- a/src/main/java/org/caosdb/server/query/RoleFilter.java +++ b/src/main/java/org/caosdb/server/query/RoleFilter.java @@ -33,6 +33,7 @@ public class RoleFilter implements EntityFilterInterface { private final Role role; private final String operator; + private boolean versioned; /** * Guarantees that all entities in the result set do have ("=") or do not have ("!=") the role in @@ -43,7 +44,8 @@ public class RoleFilter implements EntityFilterInterface { * @throws NullPointerException If role or operator is null. * @throws IllegalArgumentException If operator is not "=" or "!=". */ - public RoleFilter(final Role role, final String operator) { + public RoleFilter(final Role role, final String operator, final boolean versioned) { + this.versioned = versioned; if (role == null) { throw new NullPointerException("The role must not be null."); } @@ -72,7 +74,8 @@ public class RoleFilter implements EntityFilterInterface { getOperator(), getRole(), query.getSourceSet(), - query.getTargetSet()); + query.getTargetSet(), + this.versioned); } } catch (final SQLException e) { throw new QueryException(e); @@ -85,20 +88,28 @@ public class RoleFilter implements EntityFilterInterface { final String operator, final String role, final String sourceSet, - final String targetSet) + final String targetSet, + final boolean versioned) throws SQLException { - final PreparedStatement filterRoleStmt = - connection.prepareCall( - "INSERT IGNORE INTO `" - + targetSet - + "` (id) SELECT id FROM `" - + sourceSet - + (sourceSet.equals("entities") - ? "` AS s WHERE EXISTS (SELECT * FROM entities AS e WHERE e.id=s.id AND e.role" - : "` AS e WHERE NOT (e.role") - + operator - + "?);"); - filterRoleStmt.setString(1, role); + if (!sourceSet.equals("entities")) { + throw new UnsupportedOperationException("SourceSet is supposed to be the `entities` table."); + } + final String sql = + ("INSERT IGNORE INTO `" + + targetSet + + (versioned + ? "` (id, _iversion) SELECT e.id, _get_head_iversion(e.id) FROM `entities` AS e WHERE e.role " + + operator + + " ? UNION SELECT a.id, a._iversion FROM `archive_entities` AS a WHERE a.role" + + operator + + "?" + : "` (id) SELECT e.id FROM `entities` AS e WHERE e.role" + operator + "?")); + final PreparedStatement filterRoleStmt = connection.prepareCall(sql); + int params = (versioned ? 2 : 1); + while (params > 0) { + filterRoleStmt.setString(params, role); + params--; + } filterRoleStmt.execute(); } @@ -109,7 +120,7 @@ public class RoleFilter implements EntityFilterInterface { connection.prepareCall( "DELETE FROM `" + sourceSet - + "` WHERE EXISTS (SELECT * FROM entities AS e WHERE e.id=`" + + "` WHERE EXISTS (SELECT 1 FROM entities AS e WHERE e.id=`" + sourceSet + "`.id AND NOT e.role" + operator @@ -137,4 +148,10 @@ public class RoleFilter implements EntityFilterInterface { private String getOperator() { return this.operator; } + + @Override + public String getCacheKey() { + // unused + return null; + } } diff --git a/src/main/java/org/caosdb/server/query/StoredAt.java b/src/main/java/org/caosdb/server/query/StoredAt.java index 84a12d2c6b5b7ca3c3b0644eef4ea72a200cc328..90adc188b61e375a30721b605cb91b93fe76a88b 100644 --- a/src/main/java/org/caosdb/server/query/StoredAt.java +++ b/src/main/java/org/caosdb/server/query/StoredAt.java @@ -201,4 +201,9 @@ public class StoredAt implements EntityFilterInterface { public String toString() { return "SAT(" + (this.pattern_matching ? this.likeLocation : this.location) + ")"; } + + @Override + public String getCacheKey() { + return toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/SubProperty.java b/src/main/java/org/caosdb/server/query/SubProperty.java index f8c22367c70f623903a3ee2f14a0af3cbbefd128..f3be825ccf28b7df2ba646306a61f006664ae5a6 100644 --- a/src/main/java/org/caosdb/server/query/SubProperty.java +++ b/src/main/java/org/caosdb/server/query/SubProperty.java @@ -87,21 +87,18 @@ public class SubProperty implements QueryInterface, EntityFilterInterface { this.filter.apply(this); - getQuery().filterEntitiesWithoutRetrievePermission(this, this.sourceSet); + getQuery().filterEntitiesWithoutRetrievePermission(this.sourceSet); final CallableStatement callFinishSubProperty = - getConnection().prepareCall("call finishSubProperty(?,?,?)"); - callFinishSubProperty.setString(1, query.getSourceSet()); // sourceSet - // of - // parent - // query + getConnection().prepareCall("call finishSubProperty(?,?,?,?)"); + callFinishSubProperty.setString(1, query.getSourceSet()); // sourceSet of parent query if (query.getTargetSet() != null) { // targetSet callFinishSubProperty.setString(2, query.getTargetSet()); } else { callFinishSubProperty.setNull(2, VARCHAR); } - callFinishSubProperty.setString(3, this.sourceSet); // sub query - // sourceSet + callFinishSubProperty.setString(3, this.sourceSet); // sub query sourceSet + callFinishSubProperty.setBoolean(4, this.isVersioned()); callFinishSubProperty.execute(); callFinishSubProperty.close(); } else { @@ -165,4 +162,17 @@ public class SubProperty implements QueryInterface, EntityFilterInterface { public void addBenchmark(final String str, final long time) { this.query.addBenchmark(this.getClass().getSimpleName() + "." + str, time); } + + @Override + public boolean isVersioned() { + return this.query.isVersioned(); + } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("SUB("); + if (this.filter != null) sb.append(this.filter.getCacheKey()); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/TransactionFilter.java b/src/main/java/org/caosdb/server/query/TransactionFilter.java index 55176594f5d17fdb726b82f37e5d3eb83944e611..fed4a7156048f1bd43f7e4000df80f29a38187a0 100644 --- a/src/main/java/org/caosdb/server/query/TransactionFilter.java +++ b/src/main/java/org/caosdb/server/query/TransactionFilter.java @@ -256,4 +256,9 @@ public class TransactionFilter implements EntityFilterInterface { + this.transactor + ")"; } + + @Override + public String getCacheKey() { + return toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/VersionFilter.java b/src/main/java/org/caosdb/server/query/VersionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..aa391778ce20c089a9fa6dae88caf77e0f797703 --- /dev/null +++ b/src/main/java/org/caosdb/server/query/VersionFilter.java @@ -0,0 +1,7 @@ +package org.caosdb.server.query; + +public class VersionFilter { + + public static final VersionFilter ANY_VERSION = new VersionFilter(); + public static final VersionFilter UNVERSIONED = new VersionFilter(); +} diff --git a/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java index c7ecd4deb76117474adc188c3f8a7d0f6064a3b8..876b43a160f673b96a7220a746c9a7dcd4951388 100644 --- a/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java +++ b/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java @@ -117,7 +117,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { getRequest().setEntity(r); } - this.utils = WebinterfaceUtils.getInstance(getHostRef()); + this.utils = WebinterfaceUtils.getInstance(getRequest()); this.timestamp = getRequest().getDate().getTime(); @@ -177,7 +177,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { retRoot.setAttribute("crid", this.getCRID()); } retRoot.setAttribute("timestamp", getTimestamp().toString()); - retRoot.setAttribute("baseuri", getRootRef().toString()); + retRoot.setAttribute("baseuri", getUtils().getServerRootURI()); return retRoot; } diff --git a/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java b/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java index f20a4ab042bd996ef7e7d004e9246935cf034b64..41f92a5631d9cabb04ee5498373848b516885424 100644 --- a/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java +++ b/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,20 +24,26 @@ */ package org.caosdb.server.resource; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -import org.caosdb.server.resource.transaction.handlers.RetriveOwnerRequestHandler; +import org.caosdb.server.permissions.EntityPermission; +import org.caosdb.server.resource.transaction.RetrieveEntityResource; -public class EntityOwnerResource extends EntityResource { - - public EntityOwnerResource() { - // only retrieval is allowed - super(true, false, false, false); - } +/** + * Resource which returns the entity with the owner information attached to it. + * + * <p>Note: 'Owners' are all roles with the {@link EntityPermission#EDIT_ACL} + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityOwnerResource extends RetrieveEntityResource { @Override - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new RetriveOwnerRequestHandler(); + protected void handleRetrieveContainer(RetrieveContainer container) { + super.handleRetrieveContainer(container); + for (final EntityInterface e : container) { + // with this flag, the owner info will be loaded as well. + e.setFlag("owner", null); + } } } diff --git a/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java b/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java index ffce6fe18378ec2586276cd69124cfaeaf8f29fc..1f5ee5f7d8a28a235de02bbc33f826da32af0615 100644 --- a/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java +++ b/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,19 +25,20 @@ package org.caosdb.server.resource; import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -import org.caosdb.server.resource.transaction.handlers.RetrieveGlobalEntityPermissionsHandler; +import org.caosdb.server.permissions.EntityPermission; +import org.caosdb.server.resource.transaction.RetrieveEntityResource; -public class EntityPermissionsResource extends EntityResource { - - public EntityPermissionsResource() { - // only retrieval is allowed - super(true, false, false, false); - } +/** + * Resource which appends the entity permissions (esp. the global ones) to the normal entity + * response. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityPermissionsResource extends RetrieveEntityResource { @Override - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new RetrieveGlobalEntityPermissionsHandler(); + protected void handleRetrieveContainer(RetrieveContainer container) { + super.handleRetrieveContainer(container); + container.addMessage(EntityPermission.getAllEntityPermissions()); } } diff --git a/src/main/java/org/caosdb/server/resource/UserResource.java b/src/main/java/org/caosdb/server/resource/UserResource.java index 8ac0df1d8f8f018a07251e35a118ae944224dcc3..2c463e0e4bf7a640adc2a8368f74d798f69d0fd5 100644 --- a/src/main/java/org/caosdb/server/resource/UserResource.java +++ b/src/main/java/org/caosdb/server/resource/UserResource.java @@ -99,13 +99,11 @@ public class UserResource extends AbstractCaosDBServerResource { : UserSources.guessRealm(username)); final String password = form.getFirstValue("password"); final String email = form.getFirstValue("email"); + final UserStatus status = - UserStatus.valueOf( - form.getFirstValue( - "status", - CaosDBServer.getServerProperty( - ServerProperties.KEY_NEW_USER_DEFAULT_ACTIVITY)) - .toUpperCase()); + form.getFirstValue("status") != null + ? UserStatus.valueOf(form.getFirstValue("status").toUpperCase()) + : null; Integer userEntity = null; if (form.getFirst("entity") != null) { if (form.getFirstValue("entity").isEmpty()) { diff --git a/src/main/java/org/caosdb/server/resource/Webinterface.java b/src/main/java/org/caosdb/server/resource/Webinterface.java index bba1fb09c7f2c0a14b95b7314832b6ff174cef6b..adba714a96e5bbb4bc3ea13b8d72ec69a88cb33b 100644 --- a/src/main/java/org/caosdb/server/resource/Webinterface.java +++ b/src/main/java/org/caosdb/server/resource/Webinterface.java @@ -47,7 +47,7 @@ public class Webinterface extends ServerResource { @Override protected void doInit() throws ResourceException { - this.utils = WebinterfaceUtils.getInstance(getHostRef()); + this.utils = WebinterfaceUtils.getInstance(getRequest()); super.doInit(); } diff --git a/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java b/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java index 0fe186a0426002071e0b6c6a5ebe0b3f234e9b2a..22f1a46e4fd314b0dcaebde7abdcc1d41a9f50f0 100644 --- a/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java +++ b/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java @@ -39,7 +39,7 @@ public class WebinterfaceBuildNumber extends ServerResource { @Override protected void doInit() throws ResourceException { super.doInit(); - this.utils = WebinterfaceUtils.getInstance(getHostRef()); + this.utils = WebinterfaceUtils.getInstance(getRequest()); } /** diff --git a/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java b/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java index 0b3f8b7aa4f4a7724b9b6646e7ecb72330293980..0c66485ea3d62286d633d9580c1362ba33435095 100644 --- a/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java +++ b/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java @@ -1,18 +1,41 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ package org.caosdb.server.resource.transaction; import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.handlers.GetNamesRequestHandler; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -public class EntityNamesResource extends EntityResource { - - public EntityNamesResource() { - // only get is allowed - super(true, false, false, false); - } +/** + * Resource which returns only the names of entities. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityNamesResource extends RetrieveEntityResource { @Override - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new GetNamesRequestHandler(); + protected void handleRetrieveContainer(RetrieveContainer container) { + super.handleRetrieveContainer(container); + container.getFlags().put("names", null); } } diff --git a/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java b/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java index 12659362e0a9a82e8cbb95e73e4d6ac6c2d27c6c..704fcfb9aa92e56ad963365d763f70fa7feb470e 100644 --- a/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java +++ b/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -24,182 +26,167 @@ package org.caosdb.server.resource.transaction; import java.io.IOException; import java.security.NoSuchAlgorithmException; -import java.sql.SQLException; +import java.util.List; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileItemStream; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.caosdb.server.CaosDBException; +import org.caosdb.server.FileSystem; import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; -import org.caosdb.server.entity.container.DeleteContainer; -import org.caosdb.server.entity.container.InsertContainer; -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.entity.container.UpdateContainer; -import org.caosdb.server.resource.AbstractCaosDBServerResource; -import org.caosdb.server.resource.transaction.handlers.FileUploadHandler; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -import org.caosdb.server.resource.transaction.handlers.SimpleDeleteRequestHandler; -import org.caosdb.server.resource.transaction.handlers.SimpleGetRequestHandler; -import org.caosdb.server.resource.transaction.handlers.SimpleWriteHandler; -import org.caosdb.server.transaction.Delete; -import org.caosdb.server.transaction.Insert; -import org.caosdb.server.transaction.Retrieve; -import org.caosdb.server.transaction.Update; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.FileProperties; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.UpdateEntity; +import org.caosdb.server.entity.container.WritableContainer; +import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.transaction.WriteTransactionInterface; +import org.caosdb.server.utils.ServerMessages; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.restlet.data.MediaType; -import org.restlet.data.Status; +import org.restlet.ext.fileupload.RestletFileUpload; import org.restlet.representation.Representation; -public class EntityResource extends AbstractCaosDBServerResource { - - private final boolean get; - private final boolean post; - private final boolean put; - private final boolean delete; - - public EntityResource( - final boolean get, final boolean post, final boolean put, final boolean delete) { - this.get = get; - this.post = post; - this.put = put; - this.delete = delete; - } - - public EntityResource() { - this(true, true, true, true); - } - - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new SimpleGetRequestHandler(); - } - - protected RequestHandler<DeleteContainer> getDeleteRequestHandler() { - return new SimpleDeleteRequestHandler(); - } - - protected RequestHandler<InsertContainer> getPostRequestHandler() { - if (getRequest().getEntity().getMediaType() != null - && getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { - return new FileUploadHandler<InsertContainer>(); - } else { - return new SimpleWriteHandler<InsertContainer>(); - } - } - - protected RequestHandler<UpdateContainer> getPutRequestHandler() { - if (getRequest().getEntity().getMediaType() != null - && getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { - return new FileUploadHandler<UpdateContainer>(); - } else { - return new SimpleWriteHandler<UpdateContainer>(); - } - } - - @Override - protected final Representation httpGetInChildClass() - throws ConnectionException, IOException, SQLException, CaosDBException, - NoSuchAlgorithmException, Exception { - - final long t1 = System.currentTimeMillis(); - if (!this.get) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - final RetrieveContainer entityContainer = - new RetrieveContainer(getUser(), getTimestamp(), getSRID(), getFlags()); - final Document doc = new Document(); - - getGetRequestHandler().handle(this, entityContainer); - - final long t2 = System.currentTimeMillis(); - entityContainer - .getTransactionBenchmark() - .addMeasurement(getClass().getSimpleName() + ".httpGetInChildClass#handle", t2 - t1); - - final Retrieve retrieve = new Retrieve(entityContainer); - retrieve.execute(); - - final long t3 = System.currentTimeMillis(); - entityContainer - .getTransactionBenchmark() - .addMeasurement( - getClass().getSimpleName() + ".httpGetInChildClass#retrieve.execute", t3 - t2); - - final Element rootElem = generateRootElement(); - entityContainer.addToElement(rootElem); - doc.setRootElement(rootElem); - - final long t4 = System.currentTimeMillis(); - entityContainer - .getTransactionBenchmark() - .addMeasurement( - getClass().getSimpleName() + ".httpGetInChildClass#element_handling", t4 - t3); - - return ok(doc); - } +/** + * Handles POST, PUT, and DELETE requests for Entities. + * + * <p>The GET requests (Retrieval) is handled in the superclass {@link RetrieveEntityResource}. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityResource extends RetrieveEntityResource { + /** Handle entity deletions (DELETE requests). */ @Override protected final Representation httpDeleteInChildClass() throws Exception { - if (!this.delete) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - final DeleteContainer entityContainer = - new DeleteContainer(getUser(), getTimestamp(), getSRID(), getFlags()); + final WritableContainer container = + new WritableContainer(getUser(), getTimestamp(), getSRID(), getFlags()); final Document doc = new Document(); - getDeleteRequestHandler().handle(this, entityContainer); + for (final String item : getRequestedItems()) { + String[] elem = item.split("@", 1); + Integer id = null; + String version = null; + try { + id = Integer.parseInt(elem[0]); + } catch (NumberFormatException e) { + // pass + } + if (elem.length > 1) { + version = elem[1]; + } + + if (id != null) { + container.add(new DeleteEntity(id, version)); + } + } - final Delete delete = new Delete(entityContainer); + final WriteTransactionInterface delete = new WriteTransaction(container); delete.execute(); final Element rootElem = generateRootElement(); - entityContainer.addToElement(rootElem); + container.addToElement(rootElem); doc.setRootElement(rootElem); return ok(doc); } + /** Handle entity insertions (POST requests). */ @Override protected final Representation httpPostInChildClass(final Representation entity) throws xmlNotWellFormedException, Exception { - if (!this.post) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } + final WritableContainer entityContainer = + new WritableContainer(getUser(), getTimestamp(), getSRID(), getFlags()); - final InsertContainer entityContainer = - new InsertContainer(getUser(), getTimestamp(), getSRID(), getFlags()); - final Document doc = new Document(); + List<Element> insertEntities; + if (entity.getMediaType() != null + && entity.getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { + insertEntities = parseMultipartEntity(entityContainer).getChildren(); + } else { + insertEntities = parseEntity(entity).getRootElement().getChildren(); + } - getPostRequestHandler().handle(this, entityContainer); + for (final Element e : insertEntities) { + entityContainer.add(new InsertEntity(e)); + } - final Insert insert = new Insert(entityContainer); + final WriteTransactionInterface insert = new WriteTransaction(entityContainer); insert.execute(); final Element rootElem = generateRootElement(); entityContainer.addToElement(rootElem); + final Document doc = new Document(); doc.setRootElement(rootElem); return ok(doc); } + /** + * Parse the body of requests with content type "multipart/form-data". This is specific for + * requests which also contain file blobs. + */ + private Element parseMultipartEntity(WritableContainer container) + throws FileUploadException, IOException, Message, xmlNotWellFormedException, + NoSuchAlgorithmException, CaosDBException { + final DiskFileItemFactory factory = new DiskFileItemFactory(); + factory.setSizeThreshold(1000240); + final RestletFileUpload upload = new RestletFileUpload(factory); + final FileItemIterator iter = upload.getItemIterator(getRequest().getEntity()); + + FileItemStream item = iter.next(); + + final Element element; + // First part of the multi-part form data entity must be the xml + // representation of the files. + // Name of the form field: FileRepresentation + if (item.isFormField()) { + if (item.getFieldName().equals("FileRepresentation")) { + element = parseEntity(item.openStream()).getRootElement(); + } else { + throw ServerMessages.NO_FILE_REPRESENTATION_SUBMITTED; + } + } else { + throw ServerMessages.FORM_CONTAINS_UNSUPPORTED_CONTENT; + } + + // Get files, store to tmp dir. + while (iter.hasNext()) { + item = iter.next(); + final FileProperties file = FileSystem.upload(item, getSRID()); + container.addFile(item.getName(), file); + } + + // transform xml elements to entities and add them to the container. + return element; + } + + /** Handle entity updates (PUT requests). */ @Override protected final Representation httpPutInChildClass(final Representation entity) throws ConnectionException, JDOMException, Exception, xmlNotWellFormedException { - if (!this.put) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - - final UpdateContainer entityContainer = - new UpdateContainer(getUser(), getTimestamp(), getSRID(), getFlags()); + final WritableContainer entityContainer = + new WritableContainer(getUser(), getTimestamp(), getSRID(), getFlags()); final Document doc = new Document(); - getPutRequestHandler().handle(this, entityContainer); + List<Element> updateEntities; + if (getRequest().getEntity().getMediaType() != null + && getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { + updateEntities = parseMultipartEntity(entityContainer).getChildren(); + } else { + updateEntities = parseEntity(getRequest().getEntity()).getRootElement().getChildren(); + } + + for (final Element e : updateEntities) { + entityContainer.add(new UpdateEntity(e)); + } - final Update update = new Update(entityContainer); + final WriteTransactionInterface update = new WriteTransaction(entityContainer); update.execute(); final Element rootElem = generateRootElement(); diff --git a/src/main/java/org/caosdb/server/resource/transaction/RetrieveEntityResource.java b/src/main/java/org/caosdb/server/resource/transaction/RetrieveEntityResource.java new file mode 100644 index 0000000000000000000000000000000000000000..5eb9db72f69f8f054dbb6f02b970d09e09ff366c --- /dev/null +++ b/src/main/java/org/caosdb/server/resource/transaction/RetrieveEntityResource.java @@ -0,0 +1,110 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.resource.transaction; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.sql.SQLException; +import org.caosdb.server.CaosDBException; +import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; +import org.caosdb.server.entity.container.RetrieveContainer; +import org.caosdb.server.resource.AbstractCaosDBServerResource; +import org.caosdb.server.transaction.Retrieve; +import org.jdom2.Document; +import org.jdom2.Element; +import org.restlet.representation.Representation; + +/** + * Handles GET requests for different subclasses which all have in common that they retrieve + * Entities (plus other information in some cases). + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public abstract class RetrieveEntityResource extends AbstractCaosDBServerResource { + + /** + * Parse the segment which specifies the entities which are to be retrieved + * + * @param container + */ + protected void handleRetrieveContainer(RetrieveContainer container) { + + for (final String item : getRequestedItems()) { + String[] elem = item.split("@", 2); + Integer id = null; + String name = null; + String version = null; + try { + id = Integer.parseInt(elem[0]); + } catch (NumberFormatException e) { + name = elem[0]; + } + if (elem.length > 1) { + version = elem[1]; + } + + if (id != null) { + container.add(id, version); + } else { + container.add(name); + } + } + } + + /** Handle the GET request. */ + @Override + protected final Representation httpGetInChildClass() + throws ConnectionException, IOException, SQLException, CaosDBException, + NoSuchAlgorithmException, Exception { + + final RetrieveContainer entityContainer = + new RetrieveContainer(getUser(), getTimestamp(), getSRID(), getFlags()); + + handleRetrieveContainer(entityContainer); + + final long t2 = System.currentTimeMillis(); + final Retrieve retrieve = new Retrieve(entityContainer); + retrieve.execute(); + + final long t3 = System.currentTimeMillis(); + entityContainer + .getTransactionBenchmark() + .addMeasurement( + getClass().getSimpleName() + ".httpGetInChildClass#retrieve.execute", t3 - t2); + + final Element rootElem = generateRootElement(); + entityContainer.addToElement(rootElem); + final Document doc = new Document(); + doc.setRootElement(rootElem); + + final long t4 = System.currentTimeMillis(); + entityContainer + .getTransactionBenchmark() + .addMeasurement( + getClass().getSimpleName() + ".httpGetInChildClass#element_handling", t4 - t3); + + return ok(doc); + } +} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/FileUploadHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/FileUploadHandler.java deleted file mode 100644 index ec5c5bc4d144af9ce99f7b27915a52420c331bb5..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/FileUploadHandler.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import java.io.IOException; -import org.apache.commons.fileupload.FileItemIterator; -import org.apache.commons.fileupload.FileItemStream; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.caosdb.server.FileSystem; -import org.caosdb.server.entity.FileProperties; -import org.caosdb.server.entity.container.WritableContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.utils.ServerMessages; -import org.jdom2.Element; -import org.restlet.data.MediaType; -import org.restlet.ext.fileupload.RestletFileUpload; - -public class FileUploadHandler<T extends WritableContainer> extends SimpleWriteHandler<T> { - - @Override - public void handle(final EntityResource t, final T container) throws Exception { - if (!t.getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { - throw new Exception("Wrong MEDIATYPE"); - } - final DiskFileItemFactory factory = new DiskFileItemFactory(); - factory.setSizeThreshold(1000240); - final RestletFileUpload upload = new RestletFileUpload(factory); - final FileItemIterator iter = upload.getItemIterator(t.getRequest().getEntity()); - - FileItemStream item = iter.next(); - - final Element element; - // First part of the multi-part form data entity must be the xml - // representation of the files. - // Name of the form field: FileRepresentation - if (item.isFormField()) { - if (item.getFieldName().equals("FileRepresentation")) { - element = t.parseEntity(item.openStream()).getRootElement(); - } else { - throw ServerMessages.NO_FILE_REPRESENTATION_SUBMITTED; - } - } else { - throw ServerMessages.FORM_CONTAINS_UNSUPPORTED_CONTENT; - } - - // Get files, store to tmp dir. - while (iter.hasNext()) { - item = iter.next(); - try { - final FileProperties file = FileSystem.upload(item, t.getSRID()); - container.addFile(item.getName(), file); - } catch (final IOException e) { - throw new FileUploadException(); - } - } - - // transform xml elements to entities and add them to the container. - processEntities(container, element); - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/GetNamesRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/GetNamesRequestHandler.java deleted file mode 100644 index c327c6833d281c07148c59be689ddfac25b33f4d..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/GetNamesRequestHandler.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class GetNamesRequestHandler extends RequestHandler<RetrieveContainer> { - - /** Adds the `names` flag to the RetrieveContainer which triggers the RetrieveAllNames job. */ - @Override - public void handle(EntityResource t, RetrieveContainer container) throws Exception { - container.getFlags().put("names", null); - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/RequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/RequestHandler.java deleted file mode 100644 index fc49cc518db12e1f750716372f94b808dcb3874b..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/RequestHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public abstract class RequestHandler<T extends TransactionContainer> { - - public abstract void handle(final EntityResource t, T container) throws Exception; -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetrieveGlobalEntityPermissionsHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/RetrieveGlobalEntityPermissionsHandler.java deleted file mode 100644 index 6801e804a8bcb721e2c7170ad857126579cbfb46..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetrieveGlobalEntityPermissionsHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.permissions.EntityPermission; -import org.caosdb.server.resource.transaction.EntityResource; - -public class RetrieveGlobalEntityPermissionsHandler extends SimpleGetRequestHandler { - - @Override - public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - super.handle(t, container); - container.addMessage(EntityPermission.getAllEntityPermissions()); - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetriveOwnerRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/RetriveOwnerRequestHandler.java deleted file mode 100644 index 7b2d698cb4673ba7e4ed30fc81fc249e89632734..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetriveOwnerRequestHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class RetriveOwnerRequestHandler extends SimpleGetRequestHandler { - - @Override - public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - super.handle(t, container); - for (final EntityInterface e : container) { - e.setFlag("owner", null); - } - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java deleted file mode 100644 index 34c1a3836290f4e90fde5a1ec10a2818b7b63fae..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.DeleteContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class SimpleDeleteRequestHandler extends RequestHandler<DeleteContainer> { - - @Override - public void handle(final EntityResource t, final DeleteContainer container) throws Exception { - // TODO a lot of code duplication, see SimpleGetRequestHandle#handle. - // However, this is about to be changed again when string ids are - // introduced, anyways. So we just leave it. - for (final String item : t.getRequestedItems()) { - String[] elem = item.split("@", 1); - Integer id = null; - String version = null; - try { - id = Integer.parseInt(elem[0]); - } catch (NumberFormatException e) { - // pass - } - if (elem.length > 1) { - version = elem[1]; - } - - if (id != null) { - container.add(id, version); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java deleted file mode 100644 index b80d9c219e2cbda1e012c9c73993b5ea427c6ccf..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class SimpleGetRequestHandler extends RequestHandler<RetrieveContainer> { - - @Override - public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - for (final String item : t.getRequestedItems()) { - String[] elem = item.split("@", 2); - Integer id = null; - String name = null; - String version = null; - try { - id = Integer.parseInt(elem[0]); - } catch (NumberFormatException e) { - name = elem[0]; - } - if (elem.length > 1) { - version = elem[1]; - } - - if (id != null) { - container.add(id, version); - } else { - container.add(name); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleWriteHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleWriteHandler.java deleted file mode 100644 index feb784da2f1ab9dcc09942a108bcce6bd073e128..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleWriteHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import java.util.List; -import org.caosdb.server.entity.container.WritableContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.jdom2.Element; - -public class SimpleWriteHandler<T extends WritableContainer> extends RequestHandler<T> { - - /** - * Transform child elements of parameter 'element' to entities and add them to the container. Iff - * parameter 'tag' is not null, only elements with that tag name will be transformed into - * entities. The others will be ignored. - * - * @param container - * @param element - * @param tag - * @throws Exception - */ - protected void processEntities(final WritableContainer container, final Element element) - throws Exception { - - final List<Element> insertEntities; - insertEntities = element.getChildren(); - - for (final Element e : insertEntities) { - container.add(e); - } - } - - @Override - public void handle(final EntityResource t, final T container) throws Exception { - final Element element = t.parseEntity(t.getRequest().getEntity()).getRootElement(); - processEntities(container, element); - } -} diff --git a/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java b/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java index 5bb9498944f02c3cc2bd79429dab10868299200f..c9a0b522d949a4ba3c36ae4f867661263a0185d3 100644 --- a/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java +++ b/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java @@ -23,7 +23,8 @@ package org.caosdb.server.scripting; import java.io.File; -import java.nio.file.Path; +import java.io.IOException; +import java.util.ArrayList; import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.caosdb.server.entity.Message; @@ -33,25 +34,48 @@ import org.caosdb.server.utils.Utils; public class ScriptingUtils { - private File bin; + private File[] bin_dirs; private File working; public ScriptingUtils() { - this.bin = - new File( - CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR)); + ArrayList<File> new_bin_dirs = new ArrayList<>(); + String bin_dirs_str = + CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS); + if (bin_dirs_str == null) { + // fall-back for old server property + bin_dirs_str = + CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR); + } + + // split and process + if (bin_dirs_str != null) { + for (String dir : bin_dirs_str.split("\\s+|\\s*,\\s*")) { + + File bin; + try { + bin = new File(dir).getCanonicalFile(); + } catch (IOException e) { + throw new ConfigurationException( + "Scripting bin dir `" + dir + "` cannot be resolved to a real path."); + } + if (!bin.exists()) { + bin.mkdirs(); + } + if (!bin.isDirectory()) { + throw new ConfigurationException( + "The ServerProperty `" + + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS + + "` must point to directories"); + } + new_bin_dirs.add(bin); + } + } + + bin_dirs = new_bin_dirs.toArray(new File[new_bin_dirs.size()]); + this.working = new File( CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR)); - if (!bin.exists()) { - bin.mkdirs(); - } - if (!bin.isDirectory()) { - throw new ConfigurationException( - "The ServerProperty `" - + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR - + "` must point to a directory"); - } if (!working.exists()) { working.mkdirs(); @@ -64,21 +88,46 @@ public class ScriptingUtils { } } - public File getScriptFile(final String command) { - final Path script = bin.toPath().resolve(command); - return script.toFile(); - } + /** + * Get the script file by the relative path. + * + * <p>Run through all registered bin_dirs and try to resolve the command relative to them. The + * first matching file is used. When it is not executable throw a + * SERVER_SIDE_SCRIPT_NOT_EXECUTABLE message. When no matching file exists throw a + * SERVER_SIDE_SCRIPT_DOES_NOT_EXIST message. + * + * @param command The relative path + * @return The script File object. + * @throws Message + */ + public File getScriptFile(final String command) throws Message { + for (File bin_dir : bin_dirs) { + File script = bin_dir.toPath().resolve(command).toFile(); - public void checkScriptExists(final String command) throws Message { - if (!getScriptFile(command).exists()) { - throw ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST; - } - } + try { + script = script.getCanonicalFile(); + if (!script.toPath().startsWith(bin_dir.toPath())) { + // not below the allowed directory tree + continue; + } + } catch (IOException e) { + // cannot be resolved to canonical file - we treat it as non-existing. + continue; + } + + if (!script.exists()) { + // doesn't exist. + continue; + } + + if (!script.canExecute()) { + throw ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE; + } - public void checkScriptExecutable(final String command) throws Message { - if (!getScriptFile(command).canExecute()) { - throw ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE; + // we found it! + return script; } + throw ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST; } public File getTmpWorkingDir() { diff --git a/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java index 2ec19a426fdd5a7f0cb6438b86f73a3dc93f55cd..4f5f81e248e17f6344d8523286828ce7142c529a 100644 --- a/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java +++ b/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java @@ -54,6 +54,7 @@ public class ServerSideScriptingCaller { public static final Integer STARTED = -1; private final String[] commandLine; private final int timeoutMs; + private String absoluteScriptPath = null; private ScriptingUtils utils; private List<FileProperties> files; private File workingDir; @@ -122,7 +123,7 @@ public class ServerSideScriptingCaller { /** Does some final preparation, then calls the script and cleans up. */ public int invoke() throws Message { try { - checkCommandLine(commandLine); + this.absoluteScriptPath = getAbsoluteScriptPath(commandLine); try { createWorkingDir(); putFilesInWorkingDir(files); @@ -135,7 +136,8 @@ public class ServerSideScriptingCaller { } try { - return callScript(); + code = callScript(this.absoluteScriptPath, this.commandLine, this.authToken, this.env); + return code; } catch (TimeoutException e) { throw ServerMessages.SERVER_SIDE_SCRIPT_TIMEOUT; } catch (final Throwable e) { @@ -147,9 +149,17 @@ public class ServerSideScriptingCaller { } } - void checkCommandLine(String[] commandLine) throws Message { - utils.checkScriptExists(commandLine[0]); - utils.checkScriptExecutable(commandLine[0]); + /** + * Returns the absolute script path. + * + * <p>Throws Message when the script does not exist or when the script is not executable. + * + * @param commandLine + * @return The absolute script path. + * @throws Message + */ + String getAbsoluteScriptPath(String[] commandLine) throws Message { + return utils.getScriptFile(commandLine[0]).getAbsolutePath(); } void putFilesInWorkingDir(final Collection<FileProperties> files) @@ -243,10 +253,6 @@ public class ServerSideScriptingCaller { if (pwd.exists()) FileUtils.forceDelete(pwd); } - String makeCallAbsolute(String call) { - return utils.getScriptFile(call).getAbsolutePath(); - } - /** @fixme Should be injected into environment instead. Will be changed in v0.4 of SSS-API */ String[] injectAuthToken(String[] commandLine) { String[] newCommandLine = new String[commandLine.length + 1]; @@ -258,14 +264,31 @@ public class ServerSideScriptingCaller { return newCommandLine; } - int callScript() throws IOException, InterruptedException, TimeoutException { + /** + * Call the script. + * + * <p>The absoluteScriptPath is called with all remaining parameters from the commandLine arrays, + * an optional additional authToken and environment variables. + * + * @param absoluteScriptPath + * @param commandLine + * @param authToken + * @param env - environment variables + * @return the exit code of the script call + * @throws IOException + * @throws InterruptedException + * @throws TimeoutException + */ + int callScript( + String absoluteScriptPath, String[] commandLine, Object authToken, Map<String, String> env) + throws IOException, InterruptedException, TimeoutException { String[] effectiveCommandLine; if (authToken != null) { - effectiveCommandLine = injectAuthToken(getCommandLine()); + effectiveCommandLine = injectAuthToken(commandLine); } else { - effectiveCommandLine = Arrays.copyOf(getCommandLine(), getCommandLine().length); + effectiveCommandLine = Arrays.copyOf(commandLine, commandLine.length); } - effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]); + effectiveCommandLine[0] = absoluteScriptPath; final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine); // inject environment variables @@ -278,7 +301,7 @@ public class ServerSideScriptingCaller { pb.redirectError(Redirect.to(getStdErrFile())); pb.directory(getTmpWorkingDir()); - code = STARTED; + int code = STARTED; final TimeoutProcess process = new TimeoutProcess(pb.start(), getTimeoutMs()); code = process.waitFor(); diff --git a/src/main/java/org/caosdb/server/terminal/CaosDBTerminal.java b/src/main/java/org/caosdb/server/terminal/CaosDBTerminal.java deleted file mode 100644 index 96bd6315b7e32c03273a8b65bd489bfc061e0ef1..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/CaosDBTerminal.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.TerminalFacade; -import com.googlecode.lanterna.gui.GUIScreen; -import com.googlecode.lanterna.gui.GUIScreen.Position; -import com.googlecode.lanterna.screen.Screen; -import com.googlecode.lanterna.terminal.Terminal; -import com.googlecode.lanterna.terminal.text.UnixTerminal; -import java.nio.charset.Charset; - -/** - * @deprecated Soon to be removed - * @author Timm Fitschen (t.fitschen@indiscale.com) - */ -@Deprecated -public class CaosDBTerminal extends Thread { - - public CaosDBTerminal() { - if ((this.terminal = - TerminalFacade.createTerminal(System.in, System.out, Charset.forName("UTF8"))) - instanceof UnixTerminal) { - this.terminal = - new UnixTerminal( - System.in, - System.out, - Charset.forName("UTF8"), - null, - UnixTerminal.Behaviour.CTRL_C_KILLS_APPLICATION); - } - this.screen = new Screen(this.terminal); - this.guiScreen = new GUIScreen(this.screen); - } - - private Terminal terminal = null; - private Screen screen = null; - private GUIScreen guiScreen = null; - - @Override - public void run() { - this.guiScreen.getScreen().startScreen(); - final MainWindow m = new MainWindow(); - - this.guiScreen.showWindow(m, Position.FULL_SCREEN); - this.guiScreen.getScreen().stopScreen(); - } - - public void shutDown() { - this.guiScreen.getActiveWindow().close(); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/DatabaseAccessPanel.java b/src/main/java/org/caosdb/server/terminal/DatabaseAccessPanel.java deleted file mode 100644 index 6877049605888682bd003f8382a931661e79602c..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/DatabaseAccessPanel.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.component.Table; -import org.caosdb.server.database.DatabaseMonitor; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; - -public final class DatabaseAccessPanel extends Panel implements Observer { - - private static final DatabaseAccessPanel instance = new DatabaseAccessPanel(); - private final Table table = new Table(2); - private final Label label1 = new Label(); - private final Label label2 = new Label(); - private final Label label3 = new Label(); - private final Label label4 = new Label(); - private final Label label5 = new Label(); - - public static final DatabaseAccessPanel getInstance() { - return instance; - } - - private DatabaseAccessPanel() { - super(); - DatabaseMonitor.acceptWeakAccessObserver(this); - DatabaseMonitor.acceptStrongAccessObserver(this); - - this.table.addRow(new Label("Weak access, acquired:"), this.label1); - this.table.addRow(new Label("Weak access, waiting:"), this.label2); - this.table.addRow(new Label("Strong access, allocated:"), this.label3); - this.table.addRow(new Label("Strong access, acquired:"), this.label4); - this.table.addRow(new Label("Strong access, waiting:"), this.label5); - addComponent(this.table); - - notifyObserver(null, null); - } - - @Override - public boolean notifyObserver(final String e, final Observable o) { - try { - synchronized (this) { - if (getParent() != null) { - this.label1.setText(Integer.toString(DatabaseMonitor.acquiredWeakAccess())); - this.label2.setText(Integer.toString(DatabaseMonitor.waitingAcquireWeakAccess())); - this.label3.setText( - DatabaseMonitor.whoHasAllocatedStrongAccess() != null - ? DatabaseMonitor.whoHasAllocatedStrongAccess().getName() - : "None"); - this.label4.setText( - DatabaseMonitor.whoHasAcquiredStrongAccess() != null - ? DatabaseMonitor.whoHasAcquiredStrongAccess().getName() - : "None"); - this.label5.setText(Integer.toString(DatabaseMonitor.waitingAllocateStrongAccess())); - } - } - } catch (final Exception exc) { - exc.printStackTrace(); - } - return true; - } -} diff --git a/src/main/java/org/caosdb/server/terminal/EntitiesPanel.java b/src/main/java/org/caosdb/server/terminal/EntitiesPanel.java deleted file mode 100644 index b2f7489de66eeae9771e911b45e7d4546cb4f5b9..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/EntitiesPanel.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.component.Table; -import org.caosdb.server.utils.Info; - -public final class EntitiesPanel extends Panel implements StatComponent { - - private static final EntitiesPanel instance = new EntitiesPanel(); - private boolean active = false; - private final Table table = new Table(2); - private final Label label1 = new Label(); - private final Label label2 = new Label(); - private final Label label3 = new Label(); - private final Label label4 = new Label(); - - public static final EntitiesPanel getInstance() { - return instance; - } - - private EntitiesPanel() { - super(); - - this.table.addRow(new Label("RecordTypes:"), this.label2); - this.table.addRow(new Label("Properties:"), this.label3); - this.table.addRow(new Label("Records:"), this.label4); - this.table.addRow(new Label("Files:"), this.label1); - addComponent(this.table); - - // init INFO - Info.getInstance().notifyObserver(null, null); - - // adds itself to StatsPanel - StatsPanel.addStat("Entities", this); - start(); - } - - @Override - public void start() { - this.active = true; - update(); - } - - @Override - public void stop() { - this.active = false; - } - - @Override - public void update() { - if (this.active) { - try { - this.label1.setText(Info.getFilesCount().toString()); - this.label2.setText(Info.getRecordTypesCount().toString()); - this.label3.setText(Info.getPropertiesCount().toString()); - this.label4.setText(Info.getRecordsCount().toString()); - } catch (final Exception e) { - e.printStackTrace(); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/MainWindow.java b/src/main/java/org/caosdb/server/terminal/MainWindow.java deleted file mode 100644 index aa22f12b23bed00dfb46db8c7cd140fb05551eec..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/MainWindow.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Action; -import com.googlecode.lanterna.gui.Border; -import com.googlecode.lanterna.gui.Window; -import com.googlecode.lanterna.gui.component.Button; -import com.googlecode.lanterna.gui.component.Label; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.dialog.DialogButtons; -import com.googlecode.lanterna.gui.dialog.DialogResult; -import com.googlecode.lanterna.gui.dialog.MessageBox; -import com.googlecode.lanterna.gui.layout.LinearLayout; -import com.googlecode.lanterna.input.Key; -import com.googlecode.lanterna.input.Key.Kind; -import org.caosdb.server.utils.Observer; - -public class MainWindow extends Window { - - // A panel at the top of the window which contains buttons to switch - // between several data panels which are to be shown in the dataPanel. - final Panel dataSelectorPanel = new Panel(new Border.Invisible(), Panel.Orientation.HORISONTAL); - - // A panel right below the dataSelectorPanel. Here the interesting - // data is to be show. The contained panels can be made visible or - // invisible via the buttons in the dataSelectorPanel. - final Panel dataPanel = new Panel(new Border.Bevel(true), Panel.Orientation.HORISONTAL); - - public final synchronized void setVisibleDataPanel(final Panel visibleDataPanel) { - this.dataPanel.removeAllComponents(); - this.dataPanel.addComponent(visibleDataPanel); - } - - public MainWindow() { - super("Caos DB"); - addComponent(this.dataSelectorPanel, LinearLayout.MAXIMIZES_HORIZONTALLY); - addComponent( - this.dataPanel, LinearLayout.MAXIMIZES_HORIZONTALLY, LinearLayout.MAXIMIZES_VERTICALLY); - - // initializing stream output panels. These pipe the output of - // System.out and System.err to a TextArea. - final SystemOutPanel systemOutPanel = SystemOutPanel.getInstance(); - final Thread systemOutThread = new Thread(systemOutPanel); - systemOutThread.setName("systemOutPanel"); - - final SystemErrPanel systemErrPanel = SystemErrPanel.getInstance(); - final Thread systemErrThread = new Thread(systemErrPanel); - systemErrThread.setName("systemErrPanel"); - - systemOutThread.start(); - systemErrThread.start(); - - // initializing DatabaseAccessPanel that shows which thread is accessing - // the database and how many threads are waiting for access. - final DatabaseAccessPanel databaseAccessPanel = DatabaseAccessPanel.getInstance(); - - // init StatsPanel - StatsPanel.getPanel(); - - // init entities stats - EntitiesPanel.getInstance(); - - // add all initialized panels to the main window - addDataPanel("System.out", systemOutPanel); - addDataPanel("System.err", systemErrPanel); - addDataPanel("DB Access", databaseAccessPanel); - addDataPanel("Misc", StatsPanel.getPanel()); - - // add a welcome panel - this.dataPanel.addComponent( - new Label("Welcome. This is CaosDB - An Open Scientific DataBase.")); - - // add shutdown button - this.dataSelectorPanel.addComponent(new ShutdownButton()); - - // set focus on the dataSelectorPanel - setFocus(this.dataSelectorPanel.nextFocus(null)); - } - - /** - * Add a panel to the dataPanel. A button is created within the dataSelectorPanel which shows the - * name of the panel - * - * @param name - * @param dataPanel - */ - private void addDataPanel(final String name, final Panel dataPanel) { - this.dataSelectorPanel.addComponent(new DataPanelSelectorButton(name, dataPanel)); - } - - /** - * A Button that causes the dataPanel to show a certain panel. - * - * @author tf - */ - class DataPanelSelectorButton extends Button { - public DataPanelSelectorButton(final String name, final Panel panel) { - super( - name, - new Action() { - - @Override - public void doAction() { - // show panel in dataPanel - setVisibleDataPanel(panel); - - // update panel if necessary - if (panel instanceof Observer) { - ((Observer) panel).notifyObserver(null, null); - } - - // move focus to panel - setFocus(panel.nextFocus(null)); - } - }); - } - } - - @Override - public void onKeyPressed(final Key key) { - if (key.getKind() == Kind.Escape) { - setFocus(this.dataSelectorPanel.nextFocus(null)); - } else { - super.onKeyPressed(key); - } - } - - /** - * A button that causes the server to shut down when pressed. - * - * @author tf - */ - class ShutdownButton extends Button { - public ShutdownButton() { - super( - "Shutdown Server", - new Action() { - @Override - public void doAction() { - final DialogResult result = - MessageBox.showMessageBox( - getOwner(), - "Server shutdown", - "Select [OK] to shut down the server now.", - DialogButtons.OK_CANCEL); - if (result == DialogResult.OK) { - System.exit(0); - } - } - }); - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/OutputStreamPanel.java b/src/main/java/org/caosdb/server/terminal/OutputStreamPanel.java deleted file mode 100644 index 50284f8a88e7d30596cc901be24108f3ea201d0a..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/OutputStreamPanel.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Action; -import com.googlecode.lanterna.gui.component.Button; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.component.TextArea; -import com.googlecode.lanterna.gui.layout.LinearLayout; -import com.googlecode.lanterna.input.Key; -import com.googlecode.lanterna.input.Key.Kind; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.io.PrintStream; - -class PipedPrintStream extends PrintStream { - private final TextArea out; - int lines = 0; - - public PipedPrintStream(final TextArea out) { - super(new NullOutputStream()); - this.out = out; - } - - @Override - public void println(final String x) { - if (open) { - synchronized (this) { - out.appendLine(x); - if (lines >= 4000) { - out.removeLine(0); - } else { - lines++; - } - } - } - } - - @Override - public void print(final String x) {} - - private final boolean open = false; -} - -class NullOutputStream extends OutputStream { - - @Override - public void write(final int b) throws IOException {} -} - -public abstract class OutputStreamPanel extends Panel implements Runnable { - - private final TextArea logArea = new TextArea(); - private BufferedReader reader = null; - protected final PipedOutputStream panelOutputStream = new PipedOutputStream(); - - protected final PipedOutputStream getPanelOutputStream() { - return panelOutputStream; - } - - Button toogle = - new Button( - "on/off", - new Action() { - - @Override - public void doAction() { - toogle(); - } - }); - - private boolean on = true; - - void toogle() { - on = !on; - } - - public OutputStreamPanel() { - super(); - // addComponent(toogle); - addComponent(logArea, LinearLayout.MAXIMIZES_HORIZONTALLY, LinearLayout.MAXIMIZES_VERTICALLY); - } - - protected void init() throws IOException { - final PipedInputStream pIn = new PipedInputStream(panelOutputStream); - reader = new BufferedReader(new InputStreamReader(pIn)); - } - - @Override - public final void run() { - int lines = 0; - while (true) { - while (on) { - try { - if (reader.ready()) { - logArea.appendLine(reader.readLine()); - if (lines >= 4000) { - logArea.removeLine(0); - } else { - lines++; - } - try { - logArea.keyboardInteraction(new Key(Kind.End)); - } catch (final NullPointerException e) { - // this usually happens when the logArea is updated - // but - // not painted (while not being an active panel) - } - } else { - Thread.sleep(1000); - } - - } catch (final Exception e2) { - e2.printStackTrace(); - } - } - } - } - - @Override - protected void finalize() throws Throwable { - if (reader != null) { - reader.close(); - } - super.finalize(); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/StatComponent.java b/src/main/java/org/caosdb/server/terminal/StatComponent.java deleted file mode 100644 index 90a9ac1125425bd23415abf3e4186f00695b9b2f..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatComponent.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Component; - -public interface StatComponent extends Component { - - public void start(); - - public void stop(); - - public void update(); -} diff --git a/src/main/java/org/caosdb/server/terminal/StatLabel.java b/src/main/java/org/caosdb/server/terminal/StatLabel.java deleted file mode 100644 index 73a03ac31ed8874fc936807393701995130eaf7f..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatLabel.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; - -public class StatLabel extends Label implements Observer, StatComponent { - - boolean active = false; - Object obj; - String name; - - @Override - public boolean notifyObserver(final String e, final Observable o) { - update(); - return true; - } - - public StatLabel(final Object obj) { - this(null, obj); - } - - public StatLabel(final String name, final Object obj) { - this.name = name; - this.obj = obj; - if (obj instanceof Observable) { - ((Observable) obj).acceptObserver(this); - } - start(); - update(); - } - - @Override - public void stop() { - this.active = false; - } - - @Override - public void start() { - this.active = true; - } - - @Override - public void update() { - if (this.active) { - setText((this.name == null ? "" : this.name + ": ") + this.obj.toString()); - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/StatTable.java b/src/main/java/org/caosdb/server/terminal/StatTable.java deleted file mode 100644 index e7234cdb8000a7c4dcc98463c3e9ca6cc8c9f19c..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatTable.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Component; -import com.googlecode.lanterna.gui.component.Panel; - -public class StatTable extends Panel implements StatComponent { - - @Override - public void start() { - for (final Component c : components()) { - if (c instanceof StatComponent) { - ((StatComponent) c).start(); - } - } - } - - @Override - public void stop() { - for (final Component c : components()) { - if (c instanceof StatComponent) { - ((StatComponent) c).stop(); - } - } - } - - @Override - public void update() { - for (final Component c : components()) { - if (c instanceof StatComponent) { - ((StatComponent) c).update(); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/StatsPanel.java b/src/main/java/org/caosdb/server/terminal/StatsPanel.java deleted file mode 100644 index 3b41ac6a9c4ae7366e85cffedbd71f83b30c6438..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatsPanel.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Action; -import com.googlecode.lanterna.gui.Border; -import com.googlecode.lanterna.gui.component.Button; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.layout.LinearLayout; -import java.util.HashMap; - -public class StatsPanel extends Panel { - - private static StatsPanel instance = null; - - HashMap<String, StatTable> stats = new HashMap<String, StatTable>(); - Panel dataPanel = new Panel("Data", new Border.Bevel(true), Panel.Orientation.VERTICAL); - Panel selectorPanel = new Panel("Selector", new Border.Bevel(true), Panel.Orientation.VERTICAL); - StatComponent current = null; - - private StatsPanel() { - super(Panel.Orientation.HORISONTAL); - addComponent(this.selectorPanel, LinearLayout.MAXIMIZES_VERTICALLY); - addComponent( - this.dataPanel, LinearLayout.MAXIMIZES_VERTICALLY, LinearLayout.MAXIMIZES_HORIZONTALLY); - } - - public static void addStat(final String name, final Object obj) { - if (instance != null) { - instance.addStatComponent(name, new StatLabel(obj)); - } - } - - public static void addStat(final String name, final StatComponent statComponent) { - if (instance != null) { - instance.addStatComponent(name, statComponent); - } - } - - private void addStatComponent(final String name, final StatComponent statComponent) { - final StatTable c = this.stats.get(name); - if (c != null) { - c.addComponent(statComponent); - } else { - final StatTable table = new StatTable(); - table.addComponent(statComponent); - this.selectorPanel.addComponent(new SelectorButton(name, table)); - this.stats.put(name, table); - } - } - - private void setVisibleDataPanel(final StatComponent panel) { - this.dataPanel.removeAllComponents(); - - if (this.current != null) { - this.current.stop(); - } - this.current = panel; - this.current.start(); - this.current.update(); - - this.dataPanel.addComponent(this.current); - } - - class SelectorButton extends Button { - public SelectorButton(final String name, final StatComponent panel) { - super( - name, - new Action() { - - @Override - public void doAction() { - // show panel in dataPanel - setVisibleDataPanel(panel); - } - }); - } - } - - public static Panel getPanel() { - if (instance == null) { - instance = new StatsPanel(); - } - return instance; - } -} diff --git a/src/main/java/org/caosdb/server/terminal/SystemErrPanel.java b/src/main/java/org/caosdb/server/terminal/SystemErrPanel.java deleted file mode 100644 index 9bf800fdb3c755cd8ea2b2a55119cf4e093fde09..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/SystemErrPanel.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import java.io.IOException; -import java.io.PrintStream; - -public final class SystemErrPanel extends OutputStreamPanel { - - private static final SystemErrPanel instance = new SystemErrPanel(); - private PrintStream err; - - public static final SystemErrPanel getInstance() { - return instance; - } - - private SystemErrPanel() { - super(); - try { - init(); - this.err = System.err; - final PrintStream newErr = new PrintStream(getPanelOutputStream()); - System.setErr(newErr); - } catch (final IOException e) { - addComponent(new Label("Sorry, could not initialize this SystemErrPanel")); - } - } - - public static void close() { - System.setErr(instance.err); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/SystemOutPanel.java b/src/main/java/org/caosdb/server/terminal/SystemOutPanel.java deleted file mode 100644 index b34cdaf97d8088a97ea66932d9abc96d6e1589be..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/SystemOutPanel.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import java.io.IOException; -import java.io.PrintStream; - -public final class SystemOutPanel extends OutputStreamPanel { - - private static final SystemOutPanel instance = new SystemOutPanel(); - - public static final SystemOutPanel getInstance() { - return instance; - } - - private SystemOutPanel() { - super(); - try { - init(); - System.setOut(new PrintStream(getPanelOutputStream())); - } catch (final IOException e) { - addComponent(new Label("Sorry, could not initialize this SystemOutPanel")); - } - } -} diff --git a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java index 0a2d1363427d9e21504b421c88e7605af59634ac..4897cd65620577e36a98f2db5e5530a42b3f43d0 100644 --- a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java @@ -22,7 +22,7 @@ */ package org.caosdb.server.transaction; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.misc.RollBackHandler; import org.caosdb.server.entity.Message; @@ -33,7 +33,7 @@ public abstract class AccessControlTransaction implements TransactionInterface { @Override public final void execute() throws Exception { - this.access = DatabaseMonitor.getAccountAccess(this); + this.access = DatabaseAccessManager.getAccountAccess(this); try { transaction(); diff --git a/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java b/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java index b9d350719f13b5bd26e44f7d2b959a428adf5793..56fc07da22d9fac5bfbe713d1a9cd36b2d25a9ba 100644 --- a/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java +++ b/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java @@ -25,7 +25,7 @@ package org.caosdb.server.transaction; import java.io.IOException; import java.security.NoSuchAlgorithmException; import org.caosdb.server.CaosDBException; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.GetUpdateableChecksums; import org.caosdb.server.database.backend.transaction.RetrieveSparseEntity; @@ -34,7 +34,7 @@ import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.FileProperties; import org.caosdb.server.entity.Message; -import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.container.WritableContainer; import org.caosdb.server.utils.FileUtils; /** @@ -43,14 +43,15 @@ import org.caosdb.server.utils.FileUtils; * * @author tf */ -public class ChecksumUpdater extends WriteTransaction<TransactionContainer> implements Runnable { +public class ChecksumUpdater extends WriteTransaction + implements Runnable, WriteTransactionInterface { private Boolean running = false; private static final ChecksumUpdater instance = new ChecksumUpdater(); private ChecksumUpdater() { - super(new TransactionContainer()); + super(new WritableContainer()); } /** Retrieves all file without a checksum, calculates one and stores it to the database */ @@ -76,14 +77,14 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl Access strongAccess = null; try { - strongAccess = DatabaseMonitor.getInstance().allocateStrongAccess(this); + strongAccess = DatabaseAccessManager.getInstance().reserveWriteAccess(this); if (wasChangedInTheMeantime(fileEntity, strongAccess, lastModified)) { continue; } fileEntity.getFileProperties().setChecksum(checksum); - DatabaseMonitor.getInstance().acquireStrongAccess(this); + DatabaseAccessManager.getInstance().acquireWriteAccess(this); // update execute(new SetFileChecksum(fileEntity), strongAccess); @@ -130,8 +131,8 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl return null; } - private EntityInterface getNextUpdateableFileEntity() { - final Access weakAccess = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + private EntityInterface getNextUpdateableFileEntity() throws InterruptedException { + final Access weakAccess = DatabaseAccessManager.getInstance().acquireReadAccess(this); try { synchronized (instance.running) { diff --git a/src/main/java/org/caosdb/server/transaction/Delete.java b/src/main/java/org/caosdb/server/transaction/Delete.java deleted file mode 100644 index 44e68b3afe6225f286ab76329b699f0cf0f10670..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/transaction/Delete.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.transaction; - -import org.apache.shiro.authz.AuthorizationException; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.DeleteEntity; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.container.DeleteContainer; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.jobs.JobExecutionTime; -import org.caosdb.server.permissions.EntityPermission; -import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.ServerMessages; - -public class Delete extends WriteTransaction<DeleteContainer> { - - public Delete(final DeleteContainer container) { - super(container); - } - - @Override - protected void init() throws Exception {} - - @Override - protected void preCheck() throws InterruptedException, Exception { - // allocate strong (write) access. Only one thread can do this - // at a time. But weak access can still be acquired by other - // thread until the allocated strong access is actually - // acquired. - setAccess(getMonitor().allocateStrongAccess(this)); - - // retrieve all entities which are to be deleted. - execute(new RetrieveFullEntity(getContainer()), getAccess()); - - for (final EntityInterface e : getContainer()) { - if (e.getEntityStatus() == EntityStatus.NONEXISTENT) { - e.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); - continue; - } - - // check permissions - try { - e.checkPermission(EntityPermission.DELETE); - } catch (final AuthorizationException exc) { - e.setEntityStatus(EntityStatus.UNQUALIFIED); - e.addError(ServerMessages.AUTHORIZATION_ERROR); - e.addInfo(exc.getMessage()); - } - - // no standard entities are to be deleted. - if (e.hasId() && e.getId() < 100) { - e.setEntityStatus(EntityStatus.UNQUALIFIED); - e.addInfo("This entity cannot be deleted"); - } - } - - // make schedule of existing entities which are to be deleted. - makeSchedule(); - getSchedule().runJobs(JobExecutionTime.INIT); - } - - @Override - protected void postCheck() {} - - @Override - protected void postTransaction() { - // set entityStatus to DELETED and add deletion info message - if (getContainer().getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - for (final EntityInterface entity : getContainer()) { - if (entity.getEntityStatus() == EntityStatus.VALID) { - entity.setEntityStatus(EntityStatus.DELETED); - entity.addInfo(ServerMessages.ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY); - } - } - } - } - - @Override - public void transaction() throws Exception { - // delete entities from database - delete(getContainer(), getAccess()); - } - - private void delete(final TransactionContainer container, final Access access) throws Exception { - if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - execute(new DeleteEntity(container), access); - } - } - - @Override - public boolean logHistory() { - return true; - } -} diff --git a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java index 5d2a04b1e9f45360f47a93889f638eab23fbd512..ed93812d22eaad6bf448fa1dede2d92cea1d50a6 100644 --- a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java +++ b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.TimeZone; import org.caosdb.datetime.UTCDateTime; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.FileConsistencyCheck; import org.caosdb.server.database.backend.transaction.GetFileIterator; @@ -64,8 +64,8 @@ public class FileStorageConsistencyCheck extends Thread @Override public void run() { - this.access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); try { + this.access = DatabaseAccessManager.getInstance().acquireReadAccess(this); // test all files in file system. final Iterator<String> iterator = @@ -73,12 +73,12 @@ public class FileStorageConsistencyCheck extends Thread this.ts = System.currentTimeMillis(); while (iterator != null && iterator.hasNext()) { - if (DatabaseMonitor.whoHasAllocatedStrongAccess() != null) { + if (DatabaseAccessManager.whoHasReservedWriteAccess() != null) { // there is a thread waiting to write. pause this one and // apply for a new weak access which will be granted when // the write thread is ready. this.access.release(); - this.access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + this.access = DatabaseAccessManager.getInstance().acquireReadAccess(this); } final String path = iterator.next(); diff --git a/src/main/java/org/caosdb/server/transaction/Insert.java b/src/main/java/org/caosdb/server/transaction/Insert.java deleted file mode 100644 index f60fc1286ae909eb7fd999aa3af1f8379dd331ff..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/transaction/Insert.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.transaction; - -import org.apache.shiro.SecurityUtils; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.InsertEntity; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.FileProperties; -import org.caosdb.server.entity.Version; -import org.caosdb.server.entity.container.InsertContainer; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.permissions.EntityACL; -import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.ServerMessages; - -public class Insert extends WriteTransaction<InsertContainer> { - - public Insert(final InsertContainer container) { - super(container); - } - - @Override - protected void init() throws Exception { - // allocate strong (write) access. Only one thread can do this - // at a time. But weak access can still be acquired by other - // thread until the allocated strong access is actually - // acquired. - setAccess(getMonitor().allocateStrongAccess(this)); - - // make schedule for all parsed entities; - makeSchedule(); - - // dereference files (file upload only) - for (final EntityInterface entity : getContainer()) { - if (entity.hasFileProperties() && !entity.getFileProperties().isPickupable()) { - if (entity.getFileProperties().getTmpIdentifyer() != null) { - final FileProperties f = - getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer()); - if (f != null) { - entity.getFileProperties().setFile(f.getFile()); - if (f.getThumbnail() != null) { - entity.getFileProperties().setThumbnail(f.getThumbnail()); - } else { - final FileProperties thumbnail = - getContainer() - .getFiles() - .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); - if (thumbnail != null) { - entity.getFileProperties().setThumbnail(thumbnail.getFile()); - } - } - } else { - entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); - entity.setEntityStatus(EntityStatus.UNQUALIFIED); - } - } - } - } - } - - @Override - protected void preCheck() throws InterruptedException { - for (final EntityInterface entity : getContainer()) { - // set default EntityACL if none present - if (entity.getEntityACL() == null) { - entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject())); - } - } - } - - @Override - protected void postCheck() {} - - @Override - protected void postTransaction() {} - - @Override - public void transaction() throws Exception { - // write new entities to database - insert(getContainer(), getAccess()); - } - - public void insert(final TransactionContainer container, final Access access) throws Exception { - if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - execute(new InsertEntity(container), access); - for (EntityInterface e : container) { - // TODO move to InsertEntity transaction - e.setVersion(new Version(e.getVersion().getId(), this.getTimestamp())); - } - } - } - - @Override - public boolean logHistory() { - return true; - } -} diff --git a/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java index be01d93086e65694313c0d97c6b1603aef6a6bc7..c1128135b8620ebfe08328cd4901e9c0e6062053 100644 --- a/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java @@ -24,7 +24,7 @@ package org.caosdb.server.transaction; import java.util.List; import java.util.logging.LogRecord; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.InsertLogRecord; @@ -38,7 +38,7 @@ public class InsertLogRecordTransaction implements TransactionInterface { @Override public void execute() throws Exception { - final Access access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + final Access access = DatabaseAccessManager.getInstance().acquireReadAccess(this); try { execute(new InsertLogRecord(this.toBeFlushed), access); } finally { diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java index 5b01657a9c0df55f2b93fe83532b074543debff9..fd7bae8a82e95554d527135f76124d397c23f0c4 100644 --- a/src/main/java/org/caosdb/server/transaction/Retrieve.java +++ b/src/main/java/org/caosdb/server/transaction/Retrieve.java @@ -24,7 +24,7 @@ package org.caosdb.server.transaction; import org.apache.shiro.authz.AuthorizationException; import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.RetrieveContainer; import org.caosdb.server.entity.xml.SetFieldStrategy; @@ -47,7 +47,7 @@ public class Retrieve extends Transaction<RetrieveContainer> { @Override protected void init() throws Exception { // acquire weak access - setAccess(getMonitor().acquiredWeakAccess(this)); + setAccess(getAccessManager().acquireReadAccess(this)); // resolve names final ResolveNames r = new ResolveNames(); @@ -137,7 +137,7 @@ public class Retrieve extends Transaction<RetrieveContainer> { private void retrieveFullEntities(final RetrieveContainer container, final Access access) throws Exception { - execute(new RetrieveFullEntity(container), access); + execute(new RetrieveFullEntityTransaction(container), access); } @Override diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java index 49d850dbcb672767da33a7c855063b04e1099f29..7e6c527865566795c397377409b1fc63020ca1dd 100644 --- a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java @@ -27,7 +27,7 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import org.apache.shiro.SecurityUtils; import org.caosdb.server.accessControl.ACMPermissions; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.RetrieveLogRecord; @@ -60,7 +60,7 @@ public class RetrieveLogRecordTransaction implements TransactionInterface { @Override public void execute() throws Exception { SecurityUtils.getSubject().checkPermission(ACMPermissions.PERMISSION_RETRIEVE_SERVERLOGS); - final Access access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + final Access access = DatabaseAccessManager.getInstance().acquireReadAccess(this); try { this.logRecords = execute(new RetrieveLogRecord(this.logger, this.level, this.message), access) diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java b/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java index d82985fe8060b3919d48c17a6bbd2994fc92c5bf..520787105388d8f1408c2d3b05090c761c8f687c 100644 --- a/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java +++ b/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java @@ -41,7 +41,7 @@ public class RetrieveSparseEntityByPath extends Transaction<TransactionContainer @Override protected void init() throws Exception { // acquire weak access - setAccess(getMonitor().acquiredWeakAccess(this)); + setAccess(getAccessManager().acquireReadAccess(this)); } @Override diff --git a/src/main/java/org/caosdb/server/transaction/Transaction.java b/src/main/java/org/caosdb/server/transaction/Transaction.java index 0235b8a25cd1d7df9f923023b1be2d5597038b0e..c2bd7e0feb62fa31dadd5580990350b829ff4591 100644 --- a/src/main/java/org/caosdb/server/transaction/Transaction.java +++ b/src/main/java/org/caosdb/server/transaction/Transaction.java @@ -29,7 +29,7 @@ import java.util.List; import org.apache.shiro.subject.Subject; import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.accessControl.Principal; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.InsertTransactionHistory; import org.caosdb.server.database.exceptions.TransactionException; @@ -61,7 +61,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra return getContainer().getTransactionBenchmark(); } - private static final DatabaseMonitor monitor = DatabaseMonitor.getInstance(); + private static final DatabaseAccessManager monitor = DatabaseAccessManager.getInstance(); public static final String CLEAN_UP = "TransactionCleanUp"; private final C container; @@ -77,7 +77,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra if (o != null) acceptObserver(o); } - public static DatabaseMonitor getMonitor() { + public static DatabaseAccessManager getAccessManager() { return monitor; } @@ -231,8 +231,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra String realm = ((Principal) getTransactor().getPrincipal()).getRealm(); String username = ((Principal) getTransactor().getPrincipal()).getUsername(); execute( - new InsertTransactionHistory( - getContainer(), this.getClass().getSimpleName(), realm, username, getTimestamp()), + new InsertTransactionHistory(getContainer(), realm, username, getTimestamp()), getAccess()); } } diff --git a/src/main/java/org/caosdb/server/transaction/Update.java b/src/main/java/org/caosdb/server/transaction/Update.java deleted file mode 100644 index f5a9a7ffd8b32e4b8d220ea6bba44731b51047ef..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/transaction/Update.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.transaction; - -import com.google.common.base.Objects; -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.util.HashSet; -import java.util.Set; -import org.apache.shiro.authz.AuthorizationException; -import org.caosdb.server.CaosDBException; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; -import org.caosdb.server.database.backend.transaction.UpdateEntity; -import org.caosdb.server.entity.Entity; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.FileProperties; -import org.caosdb.server.entity.RetrieveEntity; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.entity.container.UpdateContainer; -import org.caosdb.server.entity.wrapper.Parent; -import org.caosdb.server.entity.wrapper.Property; -import org.caosdb.server.permissions.EntityPermission; -import org.caosdb.server.permissions.Permission; -import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.ServerMessages; - -public class Update extends WriteTransaction<UpdateContainer> { - - public Update(final UpdateContainer container) { - super(container); - } - - @Override - protected void preCheck() throws Exception {} - - @Override - protected void init() throws Exception { - // collect all ids of the entities which are to be updated. - final TransactionContainer oldContainer = new TransactionContainer(); - for (final EntityInterface entity : getContainer()) { - // entity has no id -> it cannot be updated. - if (!entity.hasId()) { - entity.addError(ServerMessages.ENTITY_HAS_NO_ID); - entity.setEntityStatus(EntityStatus.UNQUALIFIED); - continue; - } - - // add entity with this id to the updateContainer - if (entity.getEntityStatus() == EntityStatus.QUALIFIED) { - final Entity oldEntity = new RetrieveEntity(entity.getId()); - oldContainer.add(oldEntity); - } - } - - // allocate strong (write) access. Only one thread can do this - // at a time. But weak access can still be acquired by other - // thread until the allocated strong access is actually - // acquired. - setAccess(getMonitor().allocateStrongAccess(this)); - - // retrieve a container which contains all id of those entities - // which are to be updated. - execute(new RetrieveFullEntity(oldContainer), getAccess()); - - // Check if any updates are to be processed. - for (final EntityInterface newEntity : getContainer()) { - if (newEntity.getEntityStatus() == EntityStatus.QUALIFIED) { - innerLoop: - for (final EntityInterface oldEntity : oldContainer) { - if (oldEntity.getId().equals(newEntity.getId())) { - if (oldEntity.getEntityStatus() == EntityStatus.NONEXISTENT) { - newEntity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - } else { - // dereference files (upload only) - if (newEntity.hasFileProperties() && !newEntity.getFileProperties().isPickupable()) { - - if (newEntity.getFileProperties().getTmpIdentifyer() != null) { - // get file by tmpIdentifier - final FileProperties f = - getContainer() - .getFiles() - .get(newEntity.getFileProperties().getTmpIdentifyer()); - - // is it there? - if (f != null) { - newEntity.getFileProperties().setFile(f.getFile()); - - // has it a thumbnail? - if (f.getThumbnail() != null) { - newEntity.getFileProperties().setThumbnail(f.getThumbnail()); - } else { - final FileProperties thumbnail = - getContainer() - .getFiles() - .get(newEntity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); - if (thumbnail != null) { - newEntity.getFileProperties().setThumbnail(thumbnail.getFile()); - } else { - newEntity.addWarning(ServerMessages.THUMBNAIL_HAS_NOT_BEEN_UPLOAED); - } - } - } else { - newEntity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - } - - } else { - // in case no file has been uploaded, - // the file is expected to stay - // unchanged. Therefore we let the file - // object point to the original file in - // the file system. - newEntity - .getFileProperties() - .setFile(oldEntity.getFileProperties().retrieveFromFileSystem()); - } - } - - try { - checkPermissions(newEntity, deriveUpdate(newEntity, oldEntity)); - } catch (final AuthorizationException exc) { - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - newEntity.addError(ServerMessages.AUTHORIZATION_ERROR); - newEntity.addInfo(exc.getMessage()); - } - } - break innerLoop; - } - } - } - } - - // make schedule of those entities which are to be updated. - makeSchedule(); - } - - @Override - protected void postCheck() {} - - @Override - protected void postTransaction() {} - - @Override - public void transaction() throws Exception { - // write new entities to database - update(getContainer(), getAccess()); - } - - private void update(final TransactionContainer container, final Access access) throws Exception { - if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - execute(new UpdateEntity(container), access); - } - } - - /** Check if the user has all permissions */ - public void checkPermissions(final EntityInterface entity, final Set<Permission> permissions) { - for (final Permission p : permissions) { - entity.checkPermission(p); - } - } - - /** - * The entity is set to VALID iff there are no updates to be processed. The entity is set to - * QUALIFIED otherwise. - * - * @param newEntity - * @param oldEntity - * @throws CaosDBException - * @throws IOException - * @throws NoSuchAlgorithmException - */ - public static HashSet<Permission> deriveUpdate( - final EntityInterface newEntity, final EntityInterface oldEntity) - throws NoSuchAlgorithmException, IOException, CaosDBException { - final HashSet<Permission> needPermissions = new HashSet<>(); - boolean updatetable = false; - - // new acl? - if (newEntity.hasEntityACL() && !newEntity.getEntityACL().equals(oldEntity.getEntityACL())) { - oldEntity.checkPermission(EntityPermission.EDIT_ACL); - if (!newEntity - .getEntityACL() - .getPriorityEntityACL() - .equals(oldEntity.getEntityACL().getPriorityEntityACL())) { - // priority acl is to be changed? - oldEntity.checkPermission(Permission.EDIT_PRIORITY_ACL); - } - updatetable = true; - } else if (!newEntity.hasEntityACL()) { - newEntity.setEntityACL(oldEntity.getEntityACL()); - } - - // new query template definition? - if (!Objects.equal( - newEntity.getQueryTemplateDefinition(), oldEntity.getQueryTemplateDefinition())) { - needPermissions.add(EntityPermission.UPDATE_QUERY_TEMPLATE_DEFINITION); - updatetable = true; - } - - // new datatype? - if (newEntity.hasDatatype() - && oldEntity.hasDatatype() - && !newEntity.getDatatype().equals(oldEntity.getDatatype()) - || newEntity.hasDatatype() ^ oldEntity.hasDatatype()) { - needPermissions.add(EntityPermission.UPDATE_DATA_TYPE); - updatetable = true; - } - - // entity role - if (newEntity.hasRole() - && oldEntity.hasRole() - && !newEntity.getRole().equals(oldEntity.getRole()) - || newEntity.hasRole() ^ oldEntity.hasRole()) { - needPermissions.add(EntityPermission.UPDATE_ROLE); - updatetable = true; - } - - // entity value - if (newEntity.hasValue() - && oldEntity.hasValue() - && !newEntity.getValue().equals(oldEntity.getValue()) - || newEntity.hasValue() ^ oldEntity.hasValue()) { - needPermissions.add(EntityPermission.UPDATE_VALUE); - updatetable = true; - } - - // entity name - if (newEntity.hasName() - && oldEntity.hasName() - && !newEntity.getName().equals(oldEntity.getName()) - || newEntity.hasName() ^ oldEntity.hasName()) { - needPermissions.add(EntityPermission.UPDATE_NAME); - updatetable = true; - } - - // entity description - if (newEntity.hasDescription() - && oldEntity.hasDescription() - && !newEntity.getDescription().equals(oldEntity.getDescription()) - || newEntity.hasDescription() ^ oldEntity.hasDescription()) { - needPermissions.add(EntityPermission.UPDATE_DESCRIPTION); - updatetable = true; - } - - // file properties - if (newEntity.hasFileProperties() || oldEntity.hasFileProperties()) { - if (newEntity.hasFileProperties() && !oldEntity.hasFileProperties()) { - // add a file - needPermissions.add(EntityPermission.UPDATE_ADD_FILE); - updatetable = true; - } else if (!newEntity.hasFileProperties() && oldEntity.hasFileProperties()) { - // remove a file - needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); - updatetable = true; - } else { - // change file - final FileProperties newFile = newEntity.getFileProperties(); - final FileProperties oldFile = oldEntity.getFileProperties(); - - // file path - if (newFile.hasPath() && oldFile.hasPath() && !newFile.getPath().equals(oldFile.getPath()) - || newFile.hasPath() ^ oldFile.hasPath()) { - // this means, the location of the file is to be changed - needPermissions.add(EntityPermission.UPDATE_MOVE_FILE); - updatetable = true; - } - - // change actual file (different byte code!) - if (!oldFile.retrieveFromFileSystem().equals(newFile.getFile())) { - // CHANGE A FILE is like REMOVE AND ADD - needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); - needPermissions.add(EntityPermission.UPDATE_ADD_FILE); - updatetable = true; - } - } - } - - // properties - outerLoop: - for (final EntityInterface newProperty : newEntity.getProperties()) { - - // find corresponding oldProperty for this new property and make a - // diff. - if (newProperty.hasId()) { - for (final EntityInterface oldProperty : oldEntity.getProperties()) { - if (newProperty.getId().equals(oldProperty.getId())) { - // do not check again. - oldEntity.getProperties().remove(oldProperty); - - if (((Property) oldProperty).getPIdx() != ((Property) newProperty).getPIdx()) { - // change order of properties - needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); - needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); - updatetable = true; - } - - deriveUpdate(newProperty, oldProperty); - if (newProperty.getEntityStatus() == EntityStatus.QUALIFIED) { - needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); - needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); - updatetable = true; - } - - continue outerLoop; - } - } - } else { - newProperty.setEntityStatus(EntityStatus.UNQUALIFIED); - newProperty.addError(ServerMessages.ENTITY_HAS_NO_ID); - newProperty.addInfo("On updates, allways specify the id not just the name."); - newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - return needPermissions; - } - - // no corresponding property found -> this property is new. - needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); - updatetable = true; - } - - // some old properties left (and not matched with new ones) -> there are - // properties to be deleted. - if (!oldEntity.getProperties().isEmpty()) { - needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); - updatetable = true; - } - - // update parents - outerLoop: - for (final Parent newParent : newEntity.getParents()) { - - // find corresponding oldParent - if (newParent.hasId()) { - for (final Parent oldParent : oldEntity.getParents()) { - if (oldParent.getId().equals(newParent.getId())) { - // still there! do not check this one again - oldEntity.getParents().remove(oldParent); - continue outerLoop; - } - } - } else { - newParent.setEntityStatus(EntityStatus.UNQUALIFIED); - newParent.addError(ServerMessages.ENTITY_HAS_NO_ID); - newParent.addInfo("On updates, allways specify the id not just the name."); - newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - return needPermissions; - } - - // no corresponding parent found -> this parent is new. - needPermissions.add(EntityPermission.UPDATE_ADD_PARENT); - updatetable = true; - } - - // some old parents left (and not matched with new ones) -> there are - // parents to be deleted. - if (!oldEntity.getParents().isEmpty()) { - needPermissions.add(EntityPermission.UPDATE_REMOVE_PARENT); - updatetable = true; - } - - // nothing to be updated - if (!updatetable) { - newEntity.setEntityStatus(EntityStatus.VALID); - newEntity.addInfo("Nothing to be updated."); - } - - return needPermissions; - } - - @Override - public boolean logHistory() { - return true; - } -} diff --git a/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java b/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java index dc93549a4e5a65d474043f109d136048c37d6146..330ce5907f601b4b4345c26b1d483029f7d049b1 100644 --- a/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java @@ -78,7 +78,7 @@ public class UpdateUserTransaction extends AccessControlTransaction { + this.user.realm + "' cannot be updated. Only the users from the realm '" + UserSources.getInternalRealm().getName() - + "' can change their passwords from withing caosdb."); + + "' can change their passwords from within caosdb."); } SecurityUtils.getSubject() diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java index 04c3834caad3264d6c1b8e5c99cf5b17a513e403..abda78b6feeb0de7104de5365877b28d474058fe 100644 --- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,13 +24,53 @@ */ package org.caosdb.server.transaction; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.AuthorizationException; +import org.caosdb.server.CaosDBException; +import org.caosdb.server.database.BackendTransaction; +import org.caosdb.server.database.access.Access; +import org.caosdb.server.database.backend.transaction.DeleteEntityTransaction; +import org.caosdb.server.database.backend.transaction.InsertEntityTransaction; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; +import org.caosdb.server.database.backend.transaction.UpdateEntityTransaction; import org.caosdb.server.database.misc.RollBackHandler; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.FileProperties; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.RetrieveEntity; +import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.container.WritableContainer; +import org.caosdb.server.entity.wrapper.Parent; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.jobs.Schedule; +import org.caosdb.server.permissions.EntityACL; +import org.caosdb.server.permissions.EntityPermission; +import org.caosdb.server.permissions.Permission; +import org.caosdb.server.query.Query; +import org.caosdb.server.utils.EntityStatus; +import org.caosdb.server.utils.ServerMessages; -public abstract class WriteTransaction<C extends TransactionContainer> extends Transaction<C> { +/** + * This class is responsible for inserting, updating and deleting entities which are held in the + * {@link TransactionContainer}. + * + * <p>This class initializes and runs the {@link Schedule} of {@link Job}s, calls the {@link + * BackendTransaction}s for each entity, and handles exceptions, roll-back (if necessary) and clean + * up afterwards. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class WriteTransaction extends Transaction<WritableContainer> + implements WriteTransactionInterface { - protected WriteTransaction(final C container) { + public WriteTransaction(final WritableContainer container) { super(container); } @@ -36,12 +78,13 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T protected final void preTransaction() throws InterruptedException { // acquire strong access. No other thread can have access until // it this strong access is released. - setAccess(getMonitor().acquireStrongAccess(this)); + setAccess(getAccessManager().acquireWriteAccess(this)); } @Override protected void commit() throws Exception { getAccess().commit(); + Query.clearCache(); } @Override @@ -51,6 +94,18 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T super.rollBack(); } + private void update(final TransactionContainer container, final Access access) throws Exception { + execute(new UpdateEntityTransaction(container), access); + } + + private void insert(final TransactionContainer container, final Access access) throws Exception { + execute(new InsertEntityTransaction(container), access); + } + + private void delete(final TransactionContainer container, final Access access) throws Exception { + execute(new DeleteEntityTransaction(container), access); + } + @Override protected final void cleanUp() { // release strong access. Other threads can acquire or allocate @@ -65,6 +120,437 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T } } + @Override + public boolean logHistory() { + return true; + } + + @Override + protected void init() throws Exception { + // collect all ids of the entities which are to be updated. + final TransactionContainer oldContainer = new TransactionContainer(); + // collect all ids of the entities which are to be deleted. + final TransactionContainer deleteContainer = new TransactionContainer(); + for (final EntityInterface entity : getContainer()) { + if (entity instanceof UpdateEntity) { + // entity has no id -> it cannot be updated. + if (!entity.hasId()) { + entity.addError(ServerMessages.ENTITY_HAS_NO_ID); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + continue; + } + + // add entity with this id to the updateContainer + if (entity.getEntityStatus() == EntityStatus.QUALIFIED) { + final EntityInterface oldEntity = new RetrieveEntity(entity.getId()); + oldContainer.add(oldEntity); + } + } else if (entity instanceof DeleteEntity) { + deleteContainer.add(entity); + } + } + + // Reserve write access. Only one thread can do this at a time. But read access can still be + // acquired by other threads until the reserved write access is actually acquired. + setAccess(getAccessManager().reserveWriteAccess(this)); + + // Retrieve a container which contains all IDs of those entities + // which are to be updated. + execute(new RetrieveFullEntityTransaction(oldContainer), getAccess()); + + // Retrieve all entities which are to be deleted. + execute(new RetrieveFullEntityTransaction(deleteContainer), getAccess()); + + // Check if any updates are to be processed. + for (final EntityInterface entity : getContainer()) { + if (entity instanceof UpdateEntity && entity.getEntityStatus() == EntityStatus.QUALIFIED) { + innerLoop: + for (final EntityInterface oldEntity : oldContainer) { + if (oldEntity.getId().equals(entity.getId())) { + if (oldEntity.getEntityStatus() == EntityStatus.NONEXISTENT) { + entity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + } else { + // dereference files (upload only) + if (entity.hasFileProperties() + && entity.getFileProperties().hasTmpIdentifier() + && !entity.getFileProperties().isPickupable()) { + + // get file by tmpIdentifier + final FileProperties f = + getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer()); + + // is it there? + if (f != null) { + entity.getFileProperties().setFile(f.getFile()); + + // has it a thumbnail? + if (f.getThumbnail() != null) { + entity.getFileProperties().setThumbnail(f.getThumbnail()); + } else { + final FileProperties thumbnail = + getContainer() + .getFiles() + .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); + if (thumbnail != null) { + entity.getFileProperties().setThumbnail(thumbnail.getFile()); + } else { + entity.addWarning(ServerMessages.THUMBNAIL_HAS_NOT_BEEN_UPLOAED); + } + } + } else { + entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + } + + } else if (entity.hasFileProperties() + && !entity.getFileProperties().hasTmpIdentifier()) { + // in case no file has been uploaded, + // the file is expected to stay + // unchanged. Therefore we let the file + // object point to the original file in + // the file system. + entity + .getFileProperties() + .setFile(oldEntity.getFileProperties().retrieveFromFileSystem()); + } + + try { + checkPermissions(entity, deriveUpdate(entity, oldEntity)); + } catch (final AuthorizationException exc) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addError(ServerMessages.AUTHORIZATION_ERROR); + entity.addInfo(exc.getMessage()); + } + } + break innerLoop; + } + } + } else if (entity instanceof DeleteEntity) { + if (entity.getEntityStatus() == EntityStatus.NONEXISTENT) { + entity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); + continue; + } + + // check permissions + try { + entity.checkPermission(EntityPermission.DELETE); + } catch (final AuthorizationException exc) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addError(ServerMessages.AUTHORIZATION_ERROR); + entity.addInfo(exc.getMessage()); + } + + // no standard entities are to be deleted. + if (entity.hasId() && entity.getId() < 100) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addInfo("This entity cannot be deleted"); + } + + } else if (entity instanceof InsertEntity + && entity.hasFileProperties() + && entity.getFileProperties().hasTmpIdentifier() + && !entity.getFileProperties().isPickupable()) { + // dereference files (file upload only) + final FileProperties f = + getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer()); + if (f != null) { + entity.getFileProperties().setFile(f.getFile()); + if (f.getThumbnail() != null) { + entity.getFileProperties().setThumbnail(f.getThumbnail()); + } else { + final FileProperties thumbnail = + getContainer() + .getFiles() + .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); + if (thumbnail != null) { + entity.getFileProperties().setThumbnail(thumbnail.getFile()); + } else { + entity.addWarning(ServerMessages.THUMBNAIL_HAS_NOT_BEEN_UPLOAED); + } + } + } else { + entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + } + } + } + + makeSchedule(); + } + + /** Check if the user has all permissions */ + public static void checkPermissions( + final EntityInterface entity, final Set<Permission> permissions) { + for (final Permission p : permissions) { + entity.checkPermission(p); + } + } + + @Override + protected void preCheck() throws InterruptedException, Exception { + for (final EntityInterface entity : getContainer()) { + // set default EntityACL if none present + if (entity.getEntityACL() == null) { + entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject())); + } + } + } + + @Override + protected void postCheck() {} + + @Override + protected void transaction() throws Exception { + if (getContainer().getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { + + // split up the TransactionContainer into three containers, one for each + // type of writing transaction. + TransactionContainer inserts = new TransactionContainer(); + TransactionContainer updates = new TransactionContainer(); + TransactionContainer deletes = new TransactionContainer(); + for (EntityInterface entity : getContainer()) { + if (entity instanceof InsertEntity) { + inserts.add(entity); + } else if (entity instanceof UpdateEntity) { + updates.add(entity); + } else if (entity instanceof DeleteEntity) { + deletes.add(entity); + } + } + + // The order is crucial: 1) Update-entities may reference new entities which + // have to be inserted first. 2) Update-Entities which (originally) + // reference old entities which are to be deleted, have be updated before + // the others can be deleted. + insert(inserts, getAccess()); + update(updates, getAccess()); + delete(deletes, getAccess()); + } + } + + @Override + protected void postTransaction() throws Exception { + // set entityStatus to DELETED and add deletion info message for deleted entities. + if (getContainer().getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { + for (final EntityInterface entity : getContainer()) { + if (entity instanceof DeleteEntity && entity.getEntityStatus() == EntityStatus.VALID) { + entity.setEntityStatus(EntityStatus.DELETED); + entity.addInfo(ServerMessages.ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY); + } + } + } + } + + /** + * The entity is set to VALID iff there are no updates to be processed. The entity is set to + * QUALIFIED otherwise. + * + * @param newEntity + * @param oldEntity + * @throws CaosDBException + * @throws IOException + * @throws NoSuchAlgorithmException + */ + public static HashSet<Permission> deriveUpdate( + final EntityInterface newEntity, final EntityInterface oldEntity) + throws NoSuchAlgorithmException, IOException, CaosDBException { + final HashSet<Permission> needPermissions = new HashSet<>(); + boolean updatetable = false; + + // new acl? + if (newEntity.hasEntityACL() && !newEntity.getEntityACL().equals(oldEntity.getEntityACL())) { + oldEntity.checkPermission(EntityPermission.EDIT_ACL); + if (!newEntity + .getEntityACL() + .getPriorityEntityACL() + .equals(oldEntity.getEntityACL().getPriorityEntityACL())) { + // priority acl is to be changed? + oldEntity.checkPermission(Permission.EDIT_PRIORITY_ACL); + } + updatetable = true; + } else if (!newEntity.hasEntityACL()) { + newEntity.setEntityACL(oldEntity.getEntityACL()); + } + + // new query template definition? + if (!Objects.equals( + newEntity.getQueryTemplateDefinition(), oldEntity.getQueryTemplateDefinition())) { + needPermissions.add(EntityPermission.UPDATE_QUERY_TEMPLATE_DEFINITION); + updatetable = true; + } + + // new datatype? + if (newEntity.hasDatatype() + && oldEntity.hasDatatype() + && !newEntity.getDatatype().equals(oldEntity.getDatatype()) + || newEntity.hasDatatype() ^ oldEntity.hasDatatype()) { + needPermissions.add(EntityPermission.UPDATE_DATA_TYPE); + updatetable = true; + } + + // entity role + if (newEntity.hasRole() + && oldEntity.hasRole() + && !newEntity.getRole().equals(oldEntity.getRole()) + || newEntity.hasRole() ^ oldEntity.hasRole()) { + needPermissions.add(EntityPermission.UPDATE_ROLE); + updatetable = true; + } + + // entity value + if (newEntity.hasValue() + && oldEntity.hasValue() + && !newEntity.getValue().equals(oldEntity.getValue()) + || newEntity.hasValue() ^ oldEntity.hasValue()) { + needPermissions.add(EntityPermission.UPDATE_VALUE); + updatetable = true; + } + + // entity name + if (newEntity.hasName() + && oldEntity.hasName() + && !newEntity.getName().equals(oldEntity.getName()) + || newEntity.hasName() ^ oldEntity.hasName()) { + needPermissions.add(EntityPermission.UPDATE_NAME); + updatetable = true; + } + + // entity description + if (newEntity.hasDescription() + && oldEntity.hasDescription() + && !newEntity.getDescription().equals(oldEntity.getDescription()) + || newEntity.hasDescription() ^ oldEntity.hasDescription()) { + needPermissions.add(EntityPermission.UPDATE_DESCRIPTION); + updatetable = true; + } + + // file properties + if (newEntity.hasFileProperties() || oldEntity.hasFileProperties()) { + if (newEntity.hasFileProperties() && !oldEntity.hasFileProperties()) { + // add a file + needPermissions.add(EntityPermission.UPDATE_ADD_FILE); + updatetable = true; + } else if (!newEntity.hasFileProperties() && oldEntity.hasFileProperties()) { + // remove a file + needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); + updatetable = true; + } else { + // change file + final FileProperties newFile = newEntity.getFileProperties(); + final FileProperties oldFile = oldEntity.getFileProperties(); + + // file path + if (newFile.hasPath() && oldFile.hasPath() && !newFile.getPath().equals(oldFile.getPath()) + || newFile.hasPath() ^ oldFile.hasPath()) { + // this means, the location of the file is to be changed + needPermissions.add(EntityPermission.UPDATE_MOVE_FILE); + updatetable = true; + } + + // change actual file (different byte code!) + if (!oldFile.retrieveFromFileSystem().equals(newFile.getFile())) { + // CHANGE A FILE is like REMOVE AND ADD + needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); + needPermissions.add(EntityPermission.UPDATE_ADD_FILE); + updatetable = true; + } + } + } + + // properties + outerLoop: + for (final EntityInterface newProperty : newEntity.getProperties()) { + + // find corresponding oldProperty for this new property and make a + // diff. + if (newProperty.hasId()) { + for (final EntityInterface oldProperty : oldEntity.getProperties()) { + if (newProperty.getId().equals(oldProperty.getId())) { + // do not check again. + oldEntity.getProperties().remove(oldProperty); + + if (((Property) oldProperty).getPIdx() != ((Property) newProperty).getPIdx()) { + // change order of properties + needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); + needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); + updatetable = true; + } + + deriveUpdate(newProperty, oldProperty); + if (newProperty.getEntityStatus() == EntityStatus.QUALIFIED) { + needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); + needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); + updatetable = true; + } + + continue outerLoop; + } + } + } else { + newProperty.setEntityStatus(EntityStatus.UNQUALIFIED); + newProperty.addError(ServerMessages.ENTITY_HAS_NO_ID); + newProperty.addInfo("On updates, allways specify the id not just the name."); + newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); + newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); + return needPermissions; + } + + // no corresponding property found -> this property is new. + needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); + updatetable = true; + } + + // some old properties left (and not matched with new ones) -> there are + // properties to be deleted. + if (!oldEntity.getProperties().isEmpty()) { + needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); + updatetable = true; + } + + // update parents + outerLoop: + for (final Parent newParent : newEntity.getParents()) { + + // find corresponding oldParent + if (newParent.hasId()) { + for (final Parent oldParent : oldEntity.getParents()) { + if (oldParent.getId().equals(newParent.getId())) { + // still there! do not check this one again + oldEntity.getParents().remove(oldParent); + continue outerLoop; + } + } + } else { + newParent.setEntityStatus(EntityStatus.UNQUALIFIED); + newParent.addError(ServerMessages.ENTITY_HAS_NO_ID); + newParent.addInfo("On updates, allways specify the id not just the name."); + newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); + newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); + return needPermissions; + } + + // no corresponding parent found -> this parent is new. + needPermissions.add(EntityPermission.UPDATE_ADD_PARENT); + updatetable = true; + } + + // some old parents left (and not matched with new ones) -> there are + // parents to be deleted. + if (!oldEntity.getParents().isEmpty()) { + needPermissions.add(EntityPermission.UPDATE_REMOVE_PARENT); + updatetable = true; + } + + // nothing to be updated + if (!updatetable) { + newEntity.setEntityStatus(EntityStatus.VALID); + newEntity.addInfo("Nothing to be updated."); + } + + return needPermissions; + } + public String getSRID() { return getContainer().getRequestId(); } diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java b/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..165acb408776a720c6887d31b550cf57e3d6fa0c --- /dev/null +++ b/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java @@ -0,0 +1,8 @@ +package org.caosdb.server.transaction; + +import org.caosdb.server.database.access.Access; + +public interface WriteTransactionInterface extends TransactionInterface { + + public Access getAccess(); +} diff --git a/src/main/java/org/caosdb/server/utils/EntityStatus.java b/src/main/java/org/caosdb/server/utils/EntityStatus.java index ef8bdc101c61e216c9aa448d667b59dfa5107672..d6658ef12535bca15a6af7b1d16a4d3d8c0b5e58 100644 --- a/src/main/java/org/caosdb/server/utils/EntityStatus.java +++ b/src/main/java/org/caosdb/server/utils/EntityStatus.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,17 +25,24 @@ package org.caosdb.server.utils; /** - * IGNORE - This instance must be ignored during further processing. <br> - * VALID - This instance has a final ID and has been synchronized with the database. <br> - * WRITTEN - This instance has been written successfully to the database. It has a final ID. <br> - * QUALIFIED - This instance has a provisional or final ID, represents a well-formed entity, and any - * referenced entities (by a reference property) have status QUALIFIED or VALID. QUALIFIED - This - * instance has been checked and is not qualified to be processed further (i.e. to be inserted, - * updated, deleted) DELETED - This instance has been deleted recently. CORRUPT - This instance has - * been retrieved from the database, but though something turned out to be wrong with it. - * NONEXISTENT - This instance has been called (via id or something) but it doesn't exist. - * - * @author Timm Fitschen + * The order of the EntityStatus values matters, in that everything later than "QUALIFIED" also + * counts as qualified. + * + * <p> + * + * <ul> + * <li>IGNORE - This instance must be ignored during further processing. + * <li>UNQUALIFIED - This instance has been checked and is not qualified to be processed further + * (i.e. to be inserted, updated, deleted) + * <li>DELETED - This instance has been deleted recently. + * <li>NONEXISTENT - This instance has been called (via id or something) but it doesn't exist. + * <li>QUALIFIED - This instance has a provisional or final ID, represents a well-formed entity, + * and any referenced entities (by a reference property) have status QUALIFIED or VALID. Every + * status after this one also counts as QUALIFIED. + * <li>VALID - This instance has a final ID and has been synchronized with the database. + * </ul> + * + * @author Timm Fitschen <t.fitschen@indiscale.com> */ public enum EntityStatus { IGNORE, diff --git a/src/main/java/org/caosdb/server/utils/Info.java b/src/main/java/org/caosdb/server/utils/Info.java index 19f792cf625765924f02a7b734f2e57849584ffc..18ee09828006c0394dec315ca77483f8298ba86b 100644 --- a/src/main/java/org/caosdb/server/utils/Info.java +++ b/src/main/java/org/caosdb/server/utils/Info.java @@ -30,7 +30,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.caosdb.server.CaosDBServer; import org.caosdb.server.FileSystem; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.GetInfo; import org.caosdb.server.database.backend.transaction.SyncStats; @@ -58,7 +58,7 @@ public class Info extends AbstractObservable implements Observer, TransactionInt } private Info() { - this.access = DatabaseMonitor.getInfoAccess(this); + this.access = DatabaseAccessManager.getInfoAccess(this); try { syncDatabase(); } catch (final Exception exc) { diff --git a/src/main/java/org/caosdb/server/utils/Initialization.java b/src/main/java/org/caosdb/server/utils/Initialization.java index baae53804f57433aa58da1ae1b9c58060c3db160..cb1a36307928a72b9da95b48737196e569c43346 100644 --- a/src/main/java/org/caosdb/server/utils/Initialization.java +++ b/src/main/java/org/caosdb/server/utils/Initialization.java @@ -24,7 +24,7 @@ */ package org.caosdb.server.utils; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.transaction.TransactionInterface; @@ -34,7 +34,7 @@ public final class Initialization implements TransactionInterface, AutoCloseable private static final Initialization instance = new Initialization(); private Initialization() { - this.access = DatabaseMonitor.getInitAccess(this); + this.access = DatabaseAccessManager.getInitAccess(this); } public static final Initialization setUp() { diff --git a/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java b/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java index 1aff4de329bf3f32ba599a449c101b7fbe30423f..375d6b5e7bbced84cec5bdcdee26a9e436f0215a 100644 --- a/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java +++ b/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java @@ -33,6 +33,7 @@ import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.caosdb.server.resource.AbstractCaosDBServerResource; import org.caosdb.server.resource.Webinterface; +import org.restlet.Request; import org.restlet.data.Reference; /** @@ -61,14 +62,19 @@ public class WebinterfaceUtils { private static final Map<String, WebinterfaceUtils> instances = new HashMap<>(); /** - * Retrieve an instance of {@link WebinterfaceUtils} for the host. The instance can be shared with - * other callers. + * Retrieve an instance of {@link WebinterfaceUtils} for the request. The instance can be shared + * with other callers. * - * @param host + * @param request * @return a shared instance of {@link WebinterfaceUtils}. */ - public static WebinterfaceUtils getInstance(Reference host) { - return getInstance(host.getHostIdentifier()); + public static WebinterfaceUtils getInstance(Request request) { + String hostStr = request.getHostRef().getHostIdentifier(); + String scheme = request.getHeaders().getFirstValue("X-Forwarded-Proto", true); + if (scheme != null) { + hostStr = hostStr.replaceFirst("^" + request.getHostRef().getScheme(), scheme); + } + return getInstance(hostStr); } /** diff --git a/src/test/docker/Dockerfile b/src/test/docker/Dockerfile index ecc6b332b2d88e0d587fdb3df0fc13cdfd0c159a..08a2a0884d1f154bb941388075b2bdd915125f99 100644 --- a/src/test/docker/Dockerfile +++ b/src/test/docker/Dockerfile @@ -1,5 +1,16 @@ FROM debian:buster RUN apt-get update && \ - apt-get install \ - git make mariadb-server maven openjdk-11-jdk-headless \ - python3-pip screen libpam0g-dev unzip curl shunit2 -y + apt-get install -y \ + git make mariadb-server maven openjdk-11-jdk-headless \ + python3-pip screen libpam0g-dev unzip curl shunit2 \ + python3-sphinx \ + && \ + pip3 install javasphinx recommonmark sphinx-rtd-theme + +# Alternative, if javasphinx fails because python3-sphinx is too recent: +# (`_l` not found): +# +# git clone git@github.com:simgrid/javasphinx.git +# cd javasphinx +# git checkout 659209069603a +# pip3 install . diff --git a/src/test/java/org/caosdb/server/database/DatabaseAccessManagerTest.java b/src/test/java/org/caosdb/server/database/DatabaseAccessManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0ba085b1fd050578b50e443613f82167890ea61f --- /dev/null +++ b/src/test/java/org/caosdb/server/database/DatabaseAccessManagerTest.java @@ -0,0 +1,319 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.database; + +import static org.junit.Assert.assertFalse; + +import java.util.LinkedList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class DatabaseAccessManagerTest { + Thread createReadThread(long wait, String name, ReadAccessSemaphore readAccess) { + return new Thread( + new Runnable() { + + @Override + public void run() { + try { + readAccess.acquire(); + Thread.sleep(wait); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + readAccess.release(); + } + } + }, + name); + } + + Thread createWriteThread(long wait, String name, WriteAccessLock writeAccess) { + return new Thread( + new Runnable() { + + @Override + public void run() { + try { + writeAccess.reserve(); + Thread.sleep(wait); + writeAccess.lockInterruptibly(); + Thread.sleep(wait); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + writeAccess.release(); + } + } + }, + name); + } + + @Test + public void testDeadLock() throws InterruptedException { + final ReadAccessSemaphore readAccess = new ReadAccessSemaphore(); + final WriteAccessLock writeAccess = new WriteAccessLock(readAccess); + List<Thread> ts = new LinkedList<>(); + for (int i = 0; i < 1000; i++) { + Thread t1 = createReadThread(1, "Ra" + i, readAccess); + Thread t2 = createReadThread(2, "Rb" + i, readAccess); + Thread t3 = createReadThread(3, "Rc" + i, readAccess); + Thread t5 = createReadThread(5, "Rd" + i, readAccess); + Thread t7 = createReadThread(7, "Re" + i, readAccess); + Thread t11 = createReadThread(11, "Rf" + i, readAccess); + Thread w5 = createWriteThread(2, "W" + i, writeAccess); + + t1.start(); + t2.start(); + w5.start(); + t3.start(); + t5.start(); + t7.start(); + t11.start(); + + ts.add(t1); + ts.add(t2); + ts.add(t3); + ts.add(t5); + ts.add(t7); + ts.add(t11); + ts.add(w5); + } + + for (Thread t : ts) { + t.join(10000); + assertFalse(t.isAlive()); + } + } + + public static final ReadAccessSemaphore readAccess = new ReadAccessSemaphore(); + public static final WriteAccessLock writeAccess = new WriteAccessLock(readAccess); + + /** + * Two read-, two write-threads. The read-threads request read access, the write-threads request + * write access.<br> + * The read-thread rt1 is started and gets read access. Then the write-thread wt1 starts and + * reserves write access.<br> + * A second read-thread rt2 starts and also gets read access. A second write thread wt2 starts and + * requests allocation of write access. It has to wait until wt2 releases it. <br> + * wt1 acquires write access as soon as both read-threads released their read-accesss.<br> + * + * @throws InterruptedException + */ + @Test + public void test1() throws InterruptedException { + // first invoke a read thread + final ReadThread rt1 = new ReadThread("rt1"); + rt1.start(); // paused, has acquired read access now + + // invoke a write thread + final WriteThread wt1 = new WriteThread("wt1"); + wt1.start(); // paused, has reserved write access now + synchronized (this) { + this.wait(500); + } + // waiting means any processing + Assert.assertEquals(wt1.getState(), Thread.State.WAITING); + + final ReadThread rt2 = new ReadThread("rt2"); + rt2.start(); // paused, has acquired a second read access now + synchronized (this) { + this.wait(500); + } + synchronized (rt2) { + rt2.notify(); + } + synchronized (this) { + this.wait(500); + } + + // rt2 was processed while wt1 has reserved but not yet acquired write + // access. rt2 terminated after releasing its read access. + Assert.assertEquals(rt2.getState(), Thread.State.TERMINATED); + + final WriteThread wt2 = new WriteThread("wt2"); + wt2.start(); // wt2 immediatelly reserves write access and is block + // since wt1 already yields it. + synchronized (this) { + this.wait(500); + } + Assert.assertEquals(wt2.getState(), Thread.State.WAITING); + + // wt1 request write access. + synchronized (wt1) { + wt1.notify(); + } + // it is blocked as rt1 yet yields it. + Assert.assertEquals(wt1.getState(), Thread.State.BLOCKED); + + // rt1 is waiting (due to pause()). + Assert.assertEquals(rt1.getState(), Thread.State.WAITING); + synchronized (rt1) { + rt1.notify(); + } + synchronized (this) { + this.wait(500); + } + // rt1 was notified an terminated, releasing the read acccess. + // so wt1 acquires write access and pauses the second time. + Assert.assertEquals(rt1.getState(), Thread.State.TERMINATED); + + synchronized (wt1) { + wt1.notify(); + } + synchronized (this) { + this.wait(1000); + } + // wt2 reserves write access as wt1 released it now. + Assert.assertEquals(wt1.getState(), Thread.State.TERMINATED); + Assert.assertEquals(wt2.getState(), Thread.State.WAITING); + + // while wt2 has not yet acquired write access, rt3 acquires read + // access + final ReadThread rt3 = new ReadThread("rt3"); + rt3.start(); + synchronized (this) { + this.wait(500); + } + Assert.assertEquals(rt3.getState(), Thread.State.WAITING); + + synchronized (wt2) { + wt2.notify(); + } + synchronized (this) { + this.wait(500); + } + Assert.assertEquals(wt2.getState(), Thread.State.WAITING); + + synchronized (rt3) { + rt3.notify(); + } + synchronized (this) { + this.wait(500); + } + Assert.assertEquals(rt3.getState(), Thread.State.TERMINATED); + + synchronized (wt2) { + wt2.notify(); + } + synchronized (this) { + this.wait(500); + } + Assert.assertEquals(wt2.getState(), Thread.State.TERMINATED); + } + + @Test + public void test2() throws InterruptedException { + + // start a write-thread + final WriteThread wt1 = new WriteThread("wt1"); + wt1.start(); + // start another write-thread. It is blocked until wt1 releases the + // write access. + final WriteThread wt2 = new WriteThread("wt2"); + wt2.start(); + synchronized (this) { + this.wait(500); + } + + // and interrupt wt1 after allocating, but before acquiring the write + // access. + wt1.interrupt(); + + synchronized (this) { + this.wait(500); + } + synchronized (wt2) { + wt2.notify(); + } + + // read access should still be blocked. + final ReadThread rt1 = new ReadThread("rt1"); + rt1.start(); + + synchronized (this) { + this.wait(500); + } + Assert.assertEquals(rt1.getState(), Thread.State.WAITING); + synchronized (wt2) { + wt2.notify(); + } + } + + class WriteThread extends Thread { + @Override + public void run() { + try { + System.out.println(currentThread().getName() + " request to reserve write access"); + writeAccess.reserve(); + System.out.println(currentThread().getName() + " has reserved write access "); + process("reserved write access"); + writeAccess.lockInterruptibly(); + System.out.println(currentThread().getName() + " acquires write access"); + process("acquired write access"); + writeAccess.unlock(); + System.out.println(currentThread().getName() + " releases write access"); + } catch (final InterruptedException e) { + System.out.println(currentThread().getName() + " was interrupted"); + writeAccess.unlock(); + } + } + + private synchronized void process(String access) throws InterruptedException { + System.out.println(currentThread().getName() + " processes with " + access); + this.wait(); + System.out.println(currentThread().getName() + " is ready with " + access); + } + + public WriteThread(String name) { + super(name); + } + } + + class ReadThread extends Thread { + @Override + public void run() { + try { + System.out.println(currentThread().getName() + " requests read access"); + readAccess.acquire(); + System.out.println(currentThread().getName() + " acquires read access"); + pause(); + readAccess.release(); + System.out.println(currentThread().getName() + " releases read access"); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + } + + private synchronized void pause() throws InterruptedException { + System.out.println(currentThread().getName() + " waits "); + this.wait(); + System.out.println(currentThread().getName() + " goes on"); + } + + public ReadThread(String name) { + super(name); + } + } +} diff --git a/src/test/java/org/caosdb/server/database/DatabaseMonitorTest.java b/src/test/java/org/caosdb/server/database/DatabaseMonitorTest.java deleted file mode 100644 index 554baec079ed17d332dff47f0afb57405913dc33..0000000000000000000000000000000000000000 --- a/src/test/java/org/caosdb/server/database/DatabaseMonitorTest.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database; - -import org.junit.Assert; -import org.junit.Test; - -public class DatabaseMonitorTest { - public static final WeakAccessSemaphore wa = new WeakAccessSemaphore(); - public static final StrongAccessLock sa = new StrongAccessLock(wa); - - /** - * Two read-, two write-threads. The read-threads request weak access, the write-threads request - * strong access.<br> - * The read-thread rt1 is started and gets weak access. Then the write-thread wt1 starts and - * allocates strong access.<br> - * A second read-thread rt2 starts and also gets weak access. A second write thread wt2 starts and - * requests allocation of strong access. It has to wait until wt2 releases it. <br> - * wt1 acquires strong access as soon as both read-threads released their weak-accesss.<br> - * - * @throws InterruptedException - */ - @Test - public void test1() throws InterruptedException { - // first invoke a read thread - final ReadThread rt1 = new ReadThread(); - rt1.start(); // paused, has acquired weak access now - - // invoke a write thread - final WriteThread wt1 = new WriteThread(); - wt1.start(); // paused, has allocated strong access now - synchronized (this) { - this.wait(1000); - } - // waiting means any processing - Assert.assertEquals(wt1.getState(), Thread.State.WAITING); - - final ReadThread rt2 = new ReadThread(); - rt2.start(); // paused, has acquired a second weak access now - synchronized (this) { - this.wait(1000); - } - synchronized (rt2) { - rt2.notify(); - } - synchronized (this) { - this.wait(1000); - } - - // rt2 was processed while wt1 has allocated but not yet acquired strong - // access. rt2 terminated after releasing its weak access. - Assert.assertEquals(rt2.getState(), Thread.State.TERMINATED); - - final WriteThread wt2 = new WriteThread(); - wt2.start(); // wt2 immediatelly allocates strong access and is block - // since wt1 already yields it. - synchronized (this) { - this.wait(1000); - } - // Assert.assertEquals(wt2.getState(), Thread.State.BLOCKED); - - // wt1 request strong access. - synchronized (wt1) { - wt1.notify(); - } - // it is blocked as rt1 yet yields it. - Assert.assertEquals(wt1.getState(), Thread.State.BLOCKED); - - // rt1 is waiting (due to pause()). - Assert.assertEquals(rt1.getState(), Thread.State.WAITING); - synchronized (rt1) { - rt1.notify(); - } - synchronized (this) { - this.wait(1000); - } - // rt1 was notified an terminated, releasing the weak acccess. - // so wt1 acquires strong access and pauses the second time. - Assert.assertEquals(rt1.getState(), Thread.State.TERMINATED); - - synchronized (wt1) { - wt1.notify(); - } - synchronized (this) { - this.wait(1000); - } - // wt2 allocates strong access as wt1 released it now. - Assert.assertEquals(wt1.getState(), Thread.State.TERMINATED); - Assert.assertEquals(wt2.getState(), Thread.State.WAITING); - - // while wt2 has not yet acquired strong access, rt3 acquires weak - // access - final ReadThread rt3 = new ReadThread(); - rt3.start(); - synchronized (this) { - this.wait(1000); - } - Assert.assertEquals(rt3.getState(), Thread.State.WAITING); - - synchronized (wt2) { - wt2.notify(); - } - synchronized (this) { - this.wait(1000); - } - // Assert.assertEquals(wt2.getState(), Thread.State.BLOCKED); - - synchronized (rt3) { - rt3.notify(); - } - synchronized (this) { - this.wait(1000); - } - Assert.assertEquals(rt3.getState(), Thread.State.TERMINATED); - Assert.assertEquals(wt2.getState(), Thread.State.WAITING); - - synchronized (wt2) { - wt2.notify(); - } - synchronized (this) { - this.wait(1000); - } - Assert.assertEquals(wt2.getState(), Thread.State.TERMINATED); - } - - @Test - public void test2() throws InterruptedException { - - // start a write-thread - final WriteThread wt1 = new WriteThread(); - wt1.start(); - // start another write-thread. It is blocked until wt1 releases the - // strong access. - final WriteThread wt2 = new WriteThread(); - wt2.start(); - synchronized (this) { - this.wait(1000); - } - - // and interrupt wt1 after allocating, but before acquiring the strong - // access. - wt1.interrupt(); - - synchronized (this) { - this.wait(1000); - } - synchronized (wt2) { - wt2.notify(); - } - - // read access should still be blocked. - final ReadThread rt1 = new ReadThread(); - rt1.start(); - - synchronized (this) { - this.wait(1000); - } - synchronized (wt2) { - wt2.notify(); - } - } - - class WriteThread extends Thread { - @Override - public void run() { - try { - System.out.println("T" + currentThread().getId() + " request allocation sa"); - sa.allocate(); - System.out.println("T" + currentThread().getId() + " allocates sa"); - pause(); - sa.lockInterruptibly(); - System.out.println("T" + currentThread().getId() + " acquires sa"); - pause(); - sa.unlock(); - System.out.println("T" + currentThread().getId() + " releases sa"); - } catch (final InterruptedException e) { - System.out.println("T" + currentThread().getId() + " was interrupted"); - sa.unlock(); - } - } - - private synchronized void pause() throws InterruptedException { - this.wait(); - } - } - - class ReadThread extends Thread { - @Override - public void run() { - try { - System.out.println("T" + currentThread().getId() + " requests wa"); - wa.acquire(); - System.out.println("T" + currentThread().getId() + " acquires wa"); - pause(); - wa.release(); - System.out.println("T" + currentThread().getId() + " releases wa"); - } catch (final InterruptedException e) { - e.printStackTrace(); - } - } - - private synchronized void pause() throws InterruptedException { - this.wait(); - } - } -} diff --git a/src/test/java/org/caosdb/server/database/backend/transaction/BackendTransactionTest.java b/src/test/java/org/caosdb/server/database/backend/transaction/BackendTransactionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..93402bf0d06c487451d9219b4487f301eb855f49 --- /dev/null +++ b/src/test/java/org/caosdb/server/database/backend/transaction/BackendTransactionTest.java @@ -0,0 +1,24 @@ +package org.caosdb.server.database.backend.transaction; + +import static org.junit.Assert.assertEquals; + +import org.caosdb.server.database.BackendTransaction; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.RetrieveEntity; +import org.caosdb.server.entity.Role; +import org.caosdb.server.entity.UpdateEntity; +import org.jdom2.Element; +import org.junit.Test; + +public class BackendTransactionTest { + + @Test + public void testGetTransactionType() { + BackendTransaction transaction = new InsertTransactionHistory(null, null, null, null); + assertEquals("Retrieve", transaction.getTransactionType(new RetrieveEntity("test"))); + assertEquals("Insert", transaction.getTransactionType(new InsertEntity("test", Role.Record))); + assertEquals("Delete", transaction.getTransactionType(new DeleteEntity(1234))); + assertEquals("Update", transaction.getTransactionType(new UpdateEntity(new Element("Record")))); + } +} diff --git a/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java b/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java index 52b4c951d939f097258ec40ed16b6fd99784f9bd..4b6429b73946110b0fd173de9c53a3b30fd83e18 100644 --- a/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java +++ b/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java @@ -40,8 +40,8 @@ public class RetrieveFullEntityTest { @Test public void testRetrieveSubEntities() { - RetrieveFullEntity r = - new RetrieveFullEntity(0) { + RetrieveFullEntityTransaction r = + new RetrieveFullEntityTransaction(0) { /** Mock-up */ @Override diff --git a/src/test/java/org/caosdb/server/entity/EntityTest.java b/src/test/java/org/caosdb/server/entity/EntityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3bc85dfbf81ba865e2ebb543a28b72c5e468a485 --- /dev/null +++ b/src/test/java/org/caosdb/server/entity/EntityTest.java @@ -0,0 +1,59 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.entity; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.caosdb.server.datatype.IntegerDatatype; +import org.junit.Test; + +public class EntityTest { + + @Test + public void testIsReference() { + EntityInterface entity = new RetrieveEntity("test"); + assertFalse(entity.isReference()); + + entity.setDatatype(new IntegerDatatype()); + assertFalse(entity.isReference()); + + entity.setDatatype("Person"); + assertTrue(entity.isReference()); + } + + @Test + public void testIsReferenceList() { + EntityInterface entity = new RetrieveEntity("test"); + assertFalse(entity.isReferenceList()); + + entity.setDatatype(new IntegerDatatype()); + assertFalse(entity.isReferenceList()); + + entity.setDatatype("Person"); + assertFalse(entity.isReferenceList()); + + entity.setDatatype("LIST<Person>"); + assertTrue(entity.isReferenceList()); + } +} diff --git a/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java b/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java index 99cf2706937dce10b8db3a1305053356587e139a..436278ad640663ba42f71b824924b2f1293a5132 100644 --- a/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java +++ b/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java @@ -77,6 +77,7 @@ public class PropertyToElementStrategyTest { house.addProperty(houseHeight); windowProperty = new Property(2345); windowProperty.setName("window"); + windowProperty.setDatatype("window"); windowProperty.setValue(new ReferenceValue(window.getId())); house.addProperty(windowProperty); diff --git a/src/test/java/org/caosdb/server/query/QueryTest.java b/src/test/java/org/caosdb/server/query/QueryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..26e43ec6c65414808c72b9e04b95c1d67011fc9a --- /dev/null +++ b/src/test/java/org/caosdb/server/query/QueryTest.java @@ -0,0 +1,105 @@ +package org.caosdb.server.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.io.IOException; +import org.caosdb.server.CaosDBServer; +import org.caosdb.server.database.access.InitAccess; +import org.caosdb.server.transaction.WriteTransaction; +import org.junit.BeforeClass; +import org.junit.Test; + +public class QueryTest { + + @BeforeClass + public static void initServerProperties() throws IOException { + CaosDBServer.initServerProperties(); + } + + String getCacheKey(String query) { + Query q = new Query(query); + q.parse(); + return q.getCacheKey(); + } + + @Test + public void testGetKey() { + assertEquals("enamePOV(pname,=,val1)", getCacheKey("FIND ename WITH pname = val1")); + assertEquals("enamePOV(pname,=,val1)", getCacheKey("COUNT ename WITH pname = val1")); + assertEquals("enamePOV(pname,=,val1)", getCacheKey("SELECT bla FROM ename WITH pname = val1")); + assertEquals("enamePOV(pname,null,null)", getCacheKey("SELECT bla FROM ename WITH pname")); + assertEquals( + "enamemaxPOV(pname,null,null)", + getCacheKey("SELECT bla FROM ename WITH THE GREATEST pname")); + + assertEquals( + "RECORDenamePOV(pname,=,val1)", getCacheKey("FIND RECORD ename WITH pname = val1")); + assertEquals("ENTITYPOV(pname,=,val1)", getCacheKey("COUNT ENTITY WITH pname = val1")); + assertEquals( + "enameConj(POV(pname,=,val1)POV(ename2,=,val2))", + getCacheKey("SELECT bla FROM ename WITH pname = val1 AND ename2 = val2")); + + assertEquals("versionedENTITYID(,>,2)", getCacheKey("FIND ANY VERSION OF ENTITY WITH ID > 2")); + assertEquals("ENTITYID(min,,)", getCacheKey("FIND ENTITY WITH THE SMALLEST ID")); + assertEquals("ENTITYSAT(asdf/%%)", getCacheKey("FIND ENTITY WHICH IS STORED AT /asdf/*")); + assertEquals("ENTITYSAT(asdf/asdf)", getCacheKey("FIND ENTITY WHICH IS STORED AT asdf/asdf")); + assertEquals( + "enamePOV(ref1,null,null)SUB(POV(pname,>,val1)", + getCacheKey("FIND ename WITH ref1 WITH pname > val1 ")); + assertEquals( + "ename@(ref1,null)SUB(POV(pname,>,val1)", + getCacheKey("FIND ename WHICH IS REFERENCED BY ref1 WITH pname > val1 ")); + } + + @Test + public void testOptimizationOfFindStar() { + Query q = new Query("FIND *"); + q.parse(false); + assertEquals("*", q.getEntity().str); + assertNull(q.getRole()); + + q.optimize(); + assertNull(q.getEntity()); + assertEquals(Query.Role.ENTITY, q.getRole()); + } + + /** + * Assure that {@link WriteTransaction#commit()} calls {@link Query#clearCache()}. + * + * <p>Since currently the cache shall be cleared whenever there is a commit. + */ + @Test + public void testEtagChangesAfterWrite() { + String old = Query.getETag(); + assertNotNull(old); + + WriteTransaction w = + new WriteTransaction(null) { + + @Override + public boolean useCache() { + // this function is being overriden purely for the purpose of calling + // commit() (which is protected) + try { + // otherwise the test fails because getAccess() return null; + setAccess(new InitAccess(null)); + + commit(); + } catch (Exception e) { + fail("this should not happen"); + } + return false; + } + }; + + // trigger commit(); + w.useCache(); + + String neu = Query.getETag(); + assertNotEquals(old, neu, "old and new tag should not be equal"); + } +} diff --git a/src/test/java/org/caosdb/server/query/TestCQL.java b/src/test/java/org/caosdb/server/query/TestCQL.java index 2264c9f02233c5462339615e142c38e2df4eb033..2c28d7f59c018f90ce77e9f7691a362027655466 100644 --- a/src/test/java/org/caosdb/server/query/TestCQL.java +++ b/src/test/java/org/caosdb/server/query/TestCQL.java @@ -65,9 +65,8 @@ public class TestCQL { String query1e = "FIND ename . pname1"; String query2 = "FIND ename.pname1=val1 OR pname2=val2"; String query3 = "FIND ename.pname1=val1 AND pname2=val2 AND pname3=val3"; - // String query4 = - // "FIND ename WHICH HAS A PROPERTY pname1=val1 AND pname1 HAS A PROPERTY - // pname2=val2"; + String query4 = + "FIND ename WHICH HAS A PROPERTY pname1=val1 AND pname1 HAS A PROPERTY pname2=val2"; String query5 = "FIND ename WHICH HAS A PROPERTY pname1=val1 AND (pname2=val2 OR pname3=val3)"; String query6 = "FIND ename1 WHICH HAS A pname REFERENCE TO ename2"; String query6a = "FIND ename1 WHICH REFERENCES ename2 AS A pname"; @@ -148,8 +147,13 @@ public class TestCQL { String query27b = "FIND 'Some name with spaces and 1234 numbers and \"'"; String query27c = "FIND 'Some name with spaces and 1234 numbers and \\*'"; String query27d = "FIND 'Some name with spaces and 1234 numbers and *'"; - String query28 = "FIND ename . pname=2.0"; + String query28 = "FIND ename . pname=2.02"; String query28a = "FIND ename . pname=2.0prop=test"; + String query28b = "FIND ename . pname = 1.02m"; + String query28c = "FIND ename . pname = .02"; + String query28d = "FIND ename . pname =.02m"; + String query28e = "FIND ename . pname =.02 1/m^2"; + String ticket148 = "FIND RECORD FrequencyMeasurement WHICH HAS A PROPERTY ( BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016') AND A PROPERTY MinPeakHeight = '0.5'"; String ticket148a = @@ -213,13 +217,26 @@ public class TestCQL { String query54b = "SELECT field with spaces FROM RECORD ename"; String query54c = "SELECT field1, field2, field3 FROM RECORD ename"; String query54d = "SELECT field1.subfield1, field1.subfield2, field2.*, field3 FROM RECORD ename"; + String query54e = "SELECT id FROM ename"; String query55a = "FIND FILE WHICH IS STORED AT /dir/with/date/2016-05-15"; String query55b = "FIND FILE WHICH IS STORED AT /dir/with/date/2016-05-15/**"; String query56a = "FIND RECORD WHICH REFERENCES anna"; String query56b = "FIND RECORD WHICH REFERENCES AN ename2"; String query56c = "FIND RECORD WHICH REFERENCES atom"; String query56d = "FIND RECORD WHICH REFERENCES A tom"; + String query57a = "FIND ENTITY"; + String query57b = "FIND ENTITY WITH ID"; + String query57c = "FIND ENTITY WITH ID = 123"; + + // strange names and values + String query58a = "FIND ENTITY WITH endswith"; + String query58b = "FIND ENTITY WITH endswith = val1"; + String query58c = "FIND ENTITY WITH 0with = val1"; + String query58d = "FIND ENTITY.withdrawn=TRUE"; + String query58e = "FIND ENTITY WITH pname=with"; + String queryIssue31 = "FIND FILE WHICH IS STORED AT /data/in0.foo"; + String queryIssue116 = "FIND *"; // File paths /////////////////////////////////////////////////////////////// String filepath_verb01 = "/foo/"; @@ -241,6 +258,9 @@ public class TestCQL { String referenceByLikePattern = "FIND ENTITY WHICH IS REFERENCED BY *name*"; String emptyTextValue = "FIND ENTITY WITH prop=''"; + String queryMR56 = "FIND ENTITY WITH ((p0 = v0 OR p1=v1) AND p2=v2)"; + + String versionedQuery1 = "FIND ANY VERSION OF ENTITY e1"; @Test public void testQuery1() @@ -256,7 +276,7 @@ public class TestCQL { // 4 children: FIND, EMPTY_SPACE, entity, entity_filter assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); @@ -303,19 +323,19 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals("WITHpname1=val1", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals("WITH pname1=val1", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WITH", entity_filter.getChild(0).getText()); + assertEquals("WITH ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname1=val1", entity_filter.getChild(1).getText()); @@ -350,19 +370,19 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals("WHICHHAS APROPERTYpname1=val1", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals("WHICH HAS A PROPERTY pname1=val1", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP,conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", entity_filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname1=val1", entity_filter.getChild(1).getText()); @@ -398,19 +418,19 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals("WHICHHAS Apname1=val1", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals("WHICH HAS A pname1=val1", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname1=val1", entity_filter.getChild(1).getText()); @@ -451,21 +471,21 @@ public class TestCQL { } System.out.println("query1d: " + sfq.toStringTree(parser)); - // 5 children: FIND, role, entity, filter, EOF + // 6 children: FIND, role, entity, WHITE_SPACE, filter, EOF // entity_filter - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("ename", sfq.getChild(2).getText()); - assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals("WHICHHAS Apname1=val1", sfq.getChild(3).getText()); - final ParseTree entity_filter = sfq.getChild(3); + assertEquals("WHICH HAS A pname1=val1", sfq.getChild(4).getText()); + assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); + final ParseTree entity_filter = sfq.getChild(4); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname1=val1", entity_filter.getChild(1).getText()); @@ -509,20 +529,19 @@ public class TestCQL { } System.out.println("query1e: " + sfq.toStringTree(parser)); - // 4 children: FIND, role, entity, EOF - // entity_filter - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals(".pname1", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals(". pname1", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals(".", entity_filter.getChild(0).getText()); + assertEquals(". ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname1", entity_filter.getChild(1).getText()); @@ -560,12 +579,12 @@ public class TestCQL { // 4 children: FIND, entity, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals(".pname1=val1ORpname2=val2", sfq.getChild(2).getText()); + assertEquals(".pname1=val1 OR pname2=val2", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, disjunction @@ -573,13 +592,13 @@ public class TestCQL { assertEquals(".", entity_filter.getChild(0).getText()); // disjunction - assertEquals("pname1=val1ORpname2=val2", entity_filter.getChild(1).getText()); + assertEquals("pname1=val1 OR pname2=val2", entity_filter.getChild(1).getText()); final ParseTree disjunction = entity_filter.getChild(1); // 3 children: pov, OR, pov assertEquals(3, disjunction.getChildCount()); - assertEquals("pname1=val1", disjunction.getChild(0).getText()); - assertEquals("OR", disjunction.getChild(1).getText()); + assertEquals("pname1=val1 ", disjunction.getChild(0).getText()); + assertEquals("OR ", disjunction.getChild(1).getText()); assertEquals("pname2=val2", disjunction.getChild(2).getText()); // pov @@ -597,7 +616,7 @@ public class TestCQL { assertEquals("=", pov1.getChild(1).getText()); assertEquals("=", pov2.getChild(1).getText()); - assertEquals("val1", pov1.getChild(2).getText()); + assertEquals("val1 ", pov1.getChild(2).getText()); assertEquals("val2", pov2.getChild(2).getText()); assertEquals("ename", sfq.e.toString()); @@ -625,14 +644,14 @@ public class TestCQL { // 4 children: FIND, entity, entity_filter EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); // entity assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity filter - assertEquals(".pname1=val1ANDpname2=val2ANDpname3=val3", sfq.getChild(2).getText()); + assertEquals(".pname1=val1 AND pname2=val2 AND pname3=val3", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction @@ -640,15 +659,16 @@ public class TestCQL { assertEquals(".", entity_filter.getChild(0).getText()); // conjunction - assertEquals("pname1=val1ANDpname2=val2ANDpname3=val3", entity_filter.getChild(1).getText()); + assertEquals( + "pname1=val1 AND pname2=val2 AND pname3=val3", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 9 children: pov, AND, pov, AND, pov assertEquals(5, conjunction.getChildCount()); - assertEquals("pname1=val1", conjunction.getChild(0).getText()); - assertEquals("AND", conjunction.getChild(1).getText()); - assertEquals("pname2=val2", conjunction.getChild(2).getText()); - assertEquals("AND", conjunction.getChild(3).getText()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(1).getText()); + assertEquals("pname2=val2 ", conjunction.getChild(2).getText()); + assertEquals("AND ", conjunction.getChild(3).getText()); assertEquals("pname3=val3", conjunction.getChild(4).getText()); // pov @@ -671,8 +691,8 @@ public class TestCQL { assertEquals("=", pov2.getChild(1).getText()); assertEquals("=", pov3.getChild(1).getText()); - assertEquals("val1", pov1.getChild(2).getText()); - assertEquals("val2", pov2.getChild(2).getText()); + assertEquals("val1 ", pov1.getChild(2).getText()); + assertEquals("val2 ", pov2.getChild(2).getText()); assertEquals("val3", pov3.getChild(2).getText()); assertEquals("ename", sfq.e.toString()); @@ -689,124 +709,82 @@ public class TestCQL { } } - // @Test - // public void testQuery4() { - // CQLLexer lexer; - // lexer = new CQLLexer(CharStreams.fromString(query4)); - // final CommonTokenStream tokens = new CommonTokenStream(lexer); - // - // final CQLParser parser = new CQLParser(tokens); - // final CqContext sfq = parser.cq(); - // - // System.out.println("query4: " + sfq.toStringTree(parser)); - // - // // 5 children: FIND, EMPTY_SPACE, entity, EMPTY_SPACE, entity_filter - // assertEquals(5, sfq.getChildCount()); - // assertEquals("FIND", sfq.getChild(0).getText()); - // assertEquals(" ", sfq.getChild(1).getText()); - // assertEquals("ename", sfq.getChild(2).getText()); - // assertEquals(" ", sfq.getChild(3).getText()); - // - // // entity_filter - // assertEquals("WHICH HAS A PROPERTY pname1=val1 AND pname1 HAS A PROPERTY - // pname2=val2", - // sfq - // .getChild(4).getText()); - // final ParseTree entity_filter1 = sfq.getChild(4); - // - // // 3 children: WHICH_EXP, conjunction - // assertEquals(3, entity_filter1.getChildCount()); - // assertEquals("WHICH HAS A PROPERTY", - // entity_filter1.getChild(0).getText()); - // assertEquals(" ", entity_filter1.getChild(1).getText()); - // - // // conjunction - // assertEquals("pname1=val1 AND pname1 HAS A PROPERTY pname2=val2", - // entity_filter1 - // .getChild(2).getText()); - // final ParseTree conjunction = entity_filter1.getChild(2); - // - // // 5 children: pov, AND, subproperty - // assertEquals(5, conjunction.getChildCount()); - // assertEquals("pname1=val1", conjunction.getChild(0).getText()); - // assertEquals(" ", conjunction.getChild(1).getText()); - // assertEquals("AND", conjunction.getChild(2).getText()); - // assertEquals(" ", conjunction.getChild(3).getText()); - // assertEquals("pname1 HAS A PROPERTY pname2=val2", - // conjunction.getChild(4).getText()); - // - // // pov - // final ParseTree pov1 = conjunction.getChild(0); - // - // // 3 children: property, operator, value - // assertEquals(3, pov1.getChildCount()); - // assertEquals("pname1", pov1.getChild(0).getText()); - // assertEquals("=", pov1.getChild(1).getText()); - // assertEquals("val1", pov1.getChild(2).getText()); - // - // // subproperty - // final ParseTree subproperty = conjunction.getChild(4); - // - // // 3 children: property, entity_filter - // assertEquals(3, subproperty.getChildCount()); - // assertEquals("pname1", subproperty.getChild(0).getText()); - // assertEquals(" ", subproperty.getChild(1).getText()); - // assertEquals("HAS A PROPERTY pname2=val2", - // subproperty.getChild(2).getText()); - // - // // entity_filter - // final ParseTree entity_filter2 = subproperty.getChild(2); - // - // // 3 children: WHICH_EXP pov - // assertEquals(3, entity_filter2.getChildCount()); - // assertEquals("HAS A PROPERTY", entity_filter2.getChild(0).getText()); - // assertEquals(" ", entity_filter2.getChild(1).getText()); - // assertEquals("pname2=val2", entity_filter2.getChild(2).getText()); - // - // // conjunction2 - // final ParseTree conjunction2 = entity_filter2.getChild(2); - // assertEquals(1, conjunction2.getChildCount()); - // assertEquals("pname2=val2", conjunction2.getChild(0).getText()); - // - // // pov2 - // final ParseTree pov2 = conjunction2.getChild(0); - // - // // 3 children: property, operator, value - // assertEquals(3, pov2.getChildCount()); - // assertEquals("pname2", pov2.getChild(0).getText()); - // assertEquals("=", pov2.getChild(1).getText()); - // assertEquals("val2", pov2.getChild(2).getText()); - // - // assertEquals("ename", sfq.e.toString()); - // assertNotNull(sfq.filter); - // assertEquals(Conjunction.class.getName(), - // sfq.filter.getClass().getName()); - // assertNotNull(sfq.filter.getFilters()); - // assertFalse(sfq.filter.getFilters().isEmpty()); - // - // Integer i = 0; - // EntityFilterInterface f = sfq.filter.getFilters().get(i); - // assertNotNull(f); - // assertEquals(POV.class.getName(), f.getClass().getName()); - // assertEquals("POV(pname1,=,val1)", f.toString()); - // - // i++; - // f = sfq.filter.getFilters().get(i); - // assertNotNull(f); - // assertEquals(SubProperty.class.getName(), f.getClass().getName()); - // - // assertEquals("pname1", ((SubProperty) f).getProperty()); - // assertEquals(Conjunction.class.getName(), ((SubProperty) - // f).getFilter().getClass() - // .getName()); - // assertNotNull(((SubProperty) f).getFilter().getFilters()); - // assertFalse(((SubProperty) f).getFilter().getFilters().isEmpty()); - // f = ((SubProperty) f).getFilter().getFilters().getFirst(); - // assertNotNull(f); - // assertEquals(POV.class.getName(), f.getClass().getName()); - // assertEquals("POV(pname2,=,val2)", f.toString()); - // - // } + @Test + public void testQuery4() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(query4)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println("query4: " + sfq.toStringTree(parser)); + + // 5 children: FIND, entity, EMPTY_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("ename", sfq.getChild(1).getText()); + // entity_filter + assertEquals( + "WHICH HAS A PROPERTY pname1=val1 AND pname1 HAS A PROPERTY pname2=val2", + sfq.getChild(3).getText()); + final ParseTree entity_filter1 = sfq.getChild(3); + + // 2 children: WHICH_EXP, conjunction + assertEquals(2, entity_filter1.getChildCount()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter1.getChild(0).getText()); + + // conjunction + assertEquals( + "pname1=val1 AND pname1 HAS A PROPERTY pname2=val2", entity_filter1.getChild(1).getText()); + final ParseTree conjunction = entity_filter1.getChild(1); + + // 3 children: pov, AND, pov + assertEquals(3, conjunction.getChildCount()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(1).getText()); + assertEquals("pname1 HAS A PROPERTY pname2=val2", conjunction.getChild(2).getText()); + + // subproperty + final ParseTree subproperty = conjunction.getChild(2); + + // 3 children: property, entity_filter + assertEquals(2, subproperty.getChildCount()); + assertEquals("pname1 ", subproperty.getChild(0).getText()); + assertEquals("HAS A PROPERTY pname2=val2", subproperty.getChild(1).getText()); + + // entity_filter + final ParseTree entity_filter2 = subproperty.getChild(1).getChild(0); + + // 3 children: WHICH_EXP pov + assertEquals(2, entity_filter2.getChildCount()); + assertEquals("HAS A PROPERTY ", entity_filter2.getChild(0).getText()); + assertEquals("pname2=val2", entity_filter2.getChild(1).getText()); + + final ParseTree pov2 = entity_filter2.getChild(1).getChild(0); + + // 3 children: property, operator, value + assertEquals(3, pov2.getChildCount()); + assertEquals("pname2", pov2.getChild(0).getText()); + assertEquals("=", pov2.getChild(1).getText()); + assertEquals("val2", pov2.getChild(2).getText()); + + assertEquals("ename", sfq.e.toString()); + assertNotNull(sfq.filter); + assertEquals(Conjunction.class.getName(), sfq.filter.getClass().getName()); + assertNotNull(sfq.filter); + + EntityFilterInterface f = sfq.filter; + assertNotNull(f); + assertEquals(Conjunction.class.getName(), f.getClass().getName()); + assertEquals("POV(pname1,=,val1)", ((Conjunction) f).getFilters().get(0).toString()); + + f = ((Conjunction) sfq.filter).getFilters().get(1); + assertEquals("POV(pname1,null,null)", f.toString()); + + f = ((POV) f).getSubProperty().getFilter(); + assertEquals("POV(pname2,=,val2)", f.toString()); + } @Test public void testQuery5() { @@ -820,30 +798,32 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); // r children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // assertEquals(4, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter assertEquals( - "WHICHHAS APROPERTYpname1=val1AND(pname2=val2ORpname3=val3)", sfq.getChild(2).getText()); - final ParseTree entity_filter1 = sfq.getChild(2); + "WHICH HAS A PROPERTY pname1=val1 AND (pname2=val2 OR pname3=val3)", + sfq.getChild(3).getText()); + final ParseTree entity_filter1 = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter1.getChildCount()); - assertEquals("WHICHHAS APROPERTY", entity_filter1.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter1.getChild(0).getText()); // conjunction - assertEquals("pname1=val1AND(pname2=val2ORpname3=val3)", entity_filter1.getChild(1).getText()); + assertEquals( + "pname1=val1 AND (pname2=val2 OR pname3=val3)", entity_filter1.getChild(1).getText()); final ParseTree conjunction = entity_filter1.getChild(1); // 5 children: pov, AND, LPAREN, disjunction, RPAREN assertEquals(5, conjunction.getChildCount()); - assertEquals("pname1=val1", conjunction.getChild(0).getText()); - assertEquals("AND", conjunction.getChild(1).getText()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(1).getText()); assertEquals("(", conjunction.getChild(2).getText()); - assertEquals("pname2=val2ORpname3=val3", conjunction.getChild(3).getText()); + assertEquals("pname2=val2 OR pname3=val3", conjunction.getChild(3).getText()); assertEquals(")", conjunction.getChild(4).getText()); // pov @@ -853,15 +833,15 @@ public class TestCQL { assertEquals(3, pov1.getChildCount()); assertEquals("pname1", pov1.getChild(0).getText()); assertEquals("=", pov1.getChild(1).getText()); - assertEquals("val1", pov1.getChild(2).getText()); + assertEquals("val1 ", pov1.getChild(2).getText()); // disjunction final ParseTree disjunction = conjunction.getChild(3); // 3 children: pov, OR, pov assertEquals(3, disjunction.getChildCount()); - assertEquals("pname2=val2", disjunction.getChild(0).getText()); - assertEquals("OR", disjunction.getChild(1).getText()); + assertEquals("pname2=val2 ", disjunction.getChild(0).getText()); + assertEquals("OR ", disjunction.getChild(1).getText()); assertEquals("pname3=val3", disjunction.getChild(2).getText()); // pov @@ -874,7 +854,7 @@ public class TestCQL { assertEquals("pname2", pov2.getChild(0).getText()); assertEquals("=", pov2.getChild(1).getText()); - assertEquals("val2", pov2.getChild(2).getText()); + assertEquals("val2 ", pov2.getChild(2).getText()); assertEquals("pname3", pov3.getChild(0).getText()); assertEquals("=", pov3.getChild(1).getText()); @@ -921,34 +901,34 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 4 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals("WHICHHAS Apname->ename2", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals("WHICH HAS A pname -> ename2", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); - assertEquals("pname->ename2", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); + assertEquals("pname -> ename2", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: pov assertEquals(1, conjunction.getChildCount()); - assertEquals("pname->ename2", conjunction.getChild(0).getText()); + assertEquals("pname -> ename2", conjunction.getChild(0).getText()); // pov final ParseTree pov1 = conjunction.getChild(0); - // 3 chidren: property, operator, value - assertEquals(3, pov1.getChildCount()); - assertEquals("pname", pov1.getChild(0).getText()); + // 4 chidren: property, operator, WHITE_SPACE, value + assertEquals(4, pov1.getChildCount()); + assertEquals("pname ", pov1.getChild(0).getText()); assertEquals("->", pov1.getChild(1).getText()); - assertEquals("ename2", pov1.getChild(2).getText()); + assertEquals("ename2", pov1.getChild(3).getText()); assertEquals("ename1", sfq.e.toString()); assertNotNull(sfq.filter); @@ -972,35 +952,35 @@ public class TestCQL { } System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals("WHICH->ename2AS Apname", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals("WHICH -> ename2 AS A pname", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("->ename2AS Apname", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("-> ename2 AS A pname", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: pov assertEquals(1, conjunction.getChildCount()); - assertEquals("->ename2AS Apname", conjunction.getChild(0).getText()); + assertEquals("-> ename2 AS A pname", conjunction.getChild(0).getText()); // pov final ParseTree pov1 = conjunction.getChild(0); - // 4 chidren: value operator, AS_A, property - assertEquals(4, pov1.getChildCount()); + // 5 children: operator, WHITE_SPACE, entity, AS_A, property + assertEquals(5, pov1.getChildCount()); assertEquals("->", pov1.getChild(0).getText()); - assertEquals("ename2", pov1.getChild(1).getText()); - assertEquals("AS A", pov1.getChild(2).getText()); - assertEquals("pname", pov1.getChild(3).getText()); + assertEquals("ename2 ", pov1.getChild(2).getText()); + assertEquals("AS A ", pov1.getChild(3).getText()); + assertEquals("pname", pov1.getChild(4).getText()); assertEquals("ename1", sfq.e.toString()); assertNotNull(sfq.filter); @@ -1019,33 +999,34 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); + assertEquals(" ", sfq.getChild(2).getText()); // entity_filter - assertEquals("WHICHHAS A->ename2", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + assertEquals("WHICH HAS A -> ename2", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); + assertEquals(5, sfq.getChildCount()); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); - assertEquals("->ename2", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); + assertEquals("-> ename2", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: pov assertEquals(1, conjunction.getChildCount()); - assertEquals("->ename2", conjunction.getChild(0).getText()); + assertEquals("-> ename2", conjunction.getChild(0).getText()); // pov final ParseTree pov1 = conjunction.getChild(0); - // 2 children: operator, value - assertEquals(2, pov1.getChildCount()); + // 3 children: operator, WHITE_SPACE, value + assertEquals(3, pov1.getChildCount()); assertEquals("->", pov1.getChild(0).getText()); - assertEquals("ename2", pov1.getChild(1).getText()); + assertEquals("ename2", pov1.getChild(2).getText()); assertEquals("ename1", sfq.e.toString()); assertNotNull(sfq.filter); @@ -1065,33 +1046,33 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals("WHICHIS REFERENCEDBYename2", sfq.getChild(2).getText()); + assertEquals("WHICH IS REFERENCED BY ename2", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS REFERENCEDBYename2", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS REFERENCED BY ename2", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: backreference assertEquals(1, conjunction.getChildCount()); - assertEquals("IS REFERENCEDBYename2", conjunction.getChild(0).getText()); + assertEquals("IS REFERENCED BY ename2", conjunction.getChild(0).getText()); // backreference final ParseTree backreference = conjunction.getChild(0); // 3 children: IS_REFERENCED, BY, entity assertEquals(3, backreference.getChildCount()); - assertEquals("IS REFERENCED", backreference.getChild(0).getText()); - assertEquals("BY", backreference.getChild(1).getText()); + assertEquals("IS REFERENCED ", backreference.getChild(0).getText()); + assertEquals("BY ", backreference.getChild(1).getText()); assertEquals("ename2", backreference.getChild(2).getText()); assertEquals("ename1", sfq.e.toString()); @@ -1113,25 +1094,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals("WHICHIS REFERENCEDBYename2AS Apname1", sfq.getChild(2).getText()); + assertEquals("WHICH IS REFERENCED BY ename2 AS A pname1", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS REFERENCEDBYename2AS Apname1", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS REFERENCED BY ename2 AS A pname1", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: backreference assertEquals(1, conjunction.getChildCount()); - assertEquals("IS REFERENCEDBYename2AS Apname1", conjunction.getChild(0).getText()); + assertEquals("IS REFERENCED BY ename2 AS A pname1", conjunction.getChild(0).getText()); // backreference final ParseTree backreference = conjunction.getChild(0); @@ -1141,13 +1122,13 @@ public class TestCQL { assertNotNull(sfq.filter); assertEquals("@(ename2,pname1)", sfq.filter.toString()); - // 5 children: IS_REFERENCED, BY, entity, AS_A, property - assertEquals(5, backreference.getChildCount()); - assertEquals("IS REFERENCED", backreference.getChild(0).getText()); - assertEquals("BY", backreference.getChild(1).getText()); + // 6 children: IS_REFERENCED, BY, entity, WHITE_SPACE, AS_A, property + assertEquals(6, backreference.getChildCount()); + assertEquals("IS REFERENCED ", backreference.getChild(0).getText()); + assertEquals("BY ", backreference.getChild(1).getText()); assertEquals("ename2", backreference.getChild(2).getText()); - assertEquals("AS A", backreference.getChild(3).getText()); - assertEquals("pname1", backreference.getChild(4).getText()); + assertEquals("AS A ", backreference.getChild(4).getText()); + assertEquals("pname1", backreference.getChild(5).getText()); assertEquals("ename1", sfq.e.toString()); assertNotNull(sfq.filter); } @@ -1161,42 +1142,42 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - assertEquals(Backreference.class.getName(), sfq.filter.getClass().getName()); - assertEquals("@(ename2,pname1)", sfq.filter.toString()); - - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals("WHICHIS REFERENCEDBYANename2AS Apname1", sfq.getChild(2).getText()); + assertEquals("WHICH IS REFERENCED BY AN ename2 AS A pname1", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS REFERENCEDBYANename2AS Apname1", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS REFERENCED BY AN ename2 AS A pname1", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: backreference assertEquals(1, conjunction.getChildCount()); - assertEquals("IS REFERENCEDBYANename2AS Apname1", conjunction.getChild(0).getText()); + assertEquals("IS REFERENCED BY AN ename2 AS A pname1", conjunction.getChild(0).getText()); // backreference final ParseTree backreference = conjunction.getChild(0); - // 6 children: IS_REFERENCED, BY, AN, entity, AS_A, property - assertEquals(6, backreference.getChildCount()); - assertEquals("IS REFERENCED", backreference.getChild(0).getText()); - assertEquals("BY", backreference.getChild(1).getText()); - assertEquals("AN", backreference.getChild(2).getText()); + // 6 children: IS_REFERENCED, BY, AN, entity, WHITE_SPACE, AS_A, property + assertEquals(7, backreference.getChildCount()); + assertEquals("IS REFERENCED ", backreference.getChild(0).getText()); + assertEquals("BY ", backreference.getChild(1).getText()); + assertEquals("AN ", backreference.getChild(2).getText()); assertEquals("ename2", backreference.getChild(3).getText()); - assertEquals("AS A", backreference.getChild(4).getText()); - assertEquals("pname1", backreference.getChild(5).getText()); + assertEquals("AS A ", backreference.getChild(5).getText()); + assertEquals("pname1", backreference.getChild(6).getText()); assertEquals("ename1", sfq.e.toString()); assertNotNull(sfq.filter); + + assertEquals(Backreference.class.getName(), sfq.filter.getClass().getName()); + assertEquals("@(ename2,pname1)", sfq.filter.toString()); } @Test @@ -1208,32 +1189,32 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals("WITHNOTpname1=val1", sfq.getChild(2).getText()); + assertEquals("WITH NOT pname1=val1", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WITH", entity_filter.getChild(0).getText()); + assertEquals("WITH ", entity_filter.getChild(0).getText()); // conjunction - assertEquals("NOTpname1=val1", entity_filter.getChild(1).getText()); + assertEquals("NOT pname1=val1", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: negation assertEquals(1, conjunction.getChildCount()); - assertEquals("NOTpname1=val1", conjunction.getChild(0).getText()); + assertEquals("NOT pname1=val1", conjunction.getChild(0).getText()); final ParseTree negation = conjunction.getChild(0); // 2 children: NOT, pov assertEquals(2, negation.getChildCount()); - assertEquals("NOT", negation.getChild(0).getText()); + assertEquals("NOT ", negation.getChild(0).getText()); assertEquals("pname1=val1", negation.getChild(1).getText()); final ParseTree pov1 = negation.getChild(1).getChild(0); @@ -1260,32 +1241,32 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals("WHICHDOES NOT HAVE Apname1!=val1", sfq.getChild(2).getText()); + assertEquals("WHICH DOES NOT HAVE A pname1!=val1", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); // conjunction - assertEquals("DOES NOT HAVE Apname1!=val1", entity_filter.getChild(1).getText()); + assertEquals("DOES NOT HAVE A pname1!=val1", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: negation assertEquals(1, conjunction.getChildCount()); - assertEquals("DOES NOT HAVE Apname1!=val1", conjunction.getChild(0).getText()); + assertEquals("DOES NOT HAVE A pname1!=val1", conjunction.getChild(0).getText()); final ParseTree negation = conjunction.getChild(0); // 2 children: DOESN'T, pov assertEquals(2, negation.getChildCount()); - assertEquals("DOES NOT HAVE A", negation.getChild(0).getText()); + assertEquals("DOES NOT HAVE A ", negation.getChild(0).getText()); assertEquals("pname1!=val1", negation.getChild(1).getText()); final ParseTree pov1 = negation.getChild(1).getChild(0); @@ -1313,11 +1294,11 @@ public class TestCQL { final CqContext sfq = parser.cq(); // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(".!pname1=val1", sfq.getChild(2).getText()); + assertEquals(4, sfq.getChildCount()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); @@ -1366,12 +1347,12 @@ public class TestCQL { // 4 children: FIND, entity, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter - assertEquals(".pname1=val1AND.pname2=val2", sfq.getChild(2).getText()); + assertEquals(".pname1=val1 AND .pname2=val2", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction @@ -1379,13 +1360,13 @@ public class TestCQL { assertEquals(".", entity_filter.getChild(0).getText()); // conjunction - assertEquals("pname1=val1AND.pname2=val2", entity_filter.getChild(1).getText()); + assertEquals("pname1=val1 AND .pname2=val2", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 4 child: pov1, AND, which_exp, pov2 assertEquals(4, conjunction.getChildCount()); - assertEquals("pname1=val1", conjunction.getChild(0).getText()); - assertEquals("AND", conjunction.getChild(1).getText()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(1).getText()); assertEquals(".", conjunction.getChild(2).getText()); assertEquals("pname2=val2", conjunction.getChild(3).getText()); final ParseTree pov1 = conjunction.getChild(0).getChild(0); @@ -1395,7 +1376,7 @@ public class TestCQL { assertEquals(3, pov1.getChildCount()); assertEquals("pname1", pov1.getChild(0).getText()); assertEquals("=", pov1.getChild(1).getText()); - assertEquals("val1", pov1.getChild(2).getText()); + assertEquals("val1 ", pov1.getChild(2).getText()); assertEquals(3, pov2.getChildCount()); assertEquals("pname2", pov2.getChild(0).getText()); @@ -1423,46 +1404,47 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); // entity_filter assertEquals( - "WHICHDOESN'T HAVE Apname1=val1ANDWHICHHAS Apname2=val2", sfq.getChild(2).getText()); - final ParseTree entity_filter = sfq.getChild(2); + "WHICH DOESN'T HAVE A pname1=val1 AND WHICH HAS A pname2=val2", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); // conjunction assertEquals( - "DOESN'T HAVE Apname1=val1ANDWHICHHAS Apname2=val2", entity_filter.getChild(1).getText()); + "DOESN'T HAVE A pname1=val1 AND WHICH HAS A pname2=val2", + entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 4 children: pov1, AND, which_exp, pov2 assertEquals(4, conjunction.getChildCount()); - assertEquals("DOESN'T HAVE Apname1=val1", conjunction.getChild(0).getText()); - assertEquals("AND", conjunction.getChild(1).getText()); - assertEquals("WHICHHAS A", conjunction.getChild(2).getText()); + assertEquals("DOESN'T HAVE A pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(1).getText()); + assertEquals("WHICH HAS A ", conjunction.getChild(2).getText()); assertEquals("pname2=val2", conjunction.getChild(3).getText()); final ParseTree pov2 = conjunction.getChild(3).getChild(0); final ParseTree negation = conjunction.getChild(0).getChild(0); // 2 children: NOT, pov assertEquals(2, negation.getChildCount()); - assertEquals("DOESN'T HAVE A", negation.getChild(0).getText()); - assertEquals("pname1=val1", negation.getChild(1).getText()); + assertEquals("DOESN'T HAVE A ", negation.getChild(0).getText()); + assertEquals("pname1=val1 ", negation.getChild(1).getText()); final ParseTree pov1 = negation.getChild(1).getChild(0); // 3 children: property, operator, value assertEquals(3, pov1.getChildCount()); assertEquals("pname1", pov1.getChild(0).getText()); assertEquals("=", pov1.getChild(1).getText()); - assertEquals("val1", pov1.getChild(2).getText()); + assertEquals("val1 ", pov1.getChild(2).getText()); assertEquals(3, pov2.getChildCount()); assertEquals("pname2", pov2.getChild(0).getText()); @@ -1499,7 +1481,7 @@ public class TestCQL { // 3 children: FIND, role, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("FILE", sfq.getChild(1).getText()); assertEquals(null, sfq.e); assertEquals(Query.Role.FILE, sfq.r); @@ -1522,27 +1504,27 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("File", sfq.getChild(1).getText()); - assertEquals("WHICHISNOTREFERENCED", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("File ", sfq.getChild(1).getText()); + assertEquals("WHICH IS NOT REFERENCED", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHIS", entity_filter.getChild(0).getText()); - assertEquals("NOTREFERENCED", entity_filter.getChild(1).getText()); + assertEquals("WHICH IS ", entity_filter.getChild(0).getText()); + assertEquals("NOT REFERENCED", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: negation assertEquals(1, conjunction.getChildCount()); - assertEquals("NOTREFERENCED", conjunction.getChild(0).getText()); + assertEquals("NOT REFERENCED", conjunction.getChild(0).getText()); final ParseTree negation = conjunction.getChild(0); // 2 children: NOT, backreference assertEquals(2, negation.getChildCount()); - assertEquals("NOT", negation.getChild(0).getText()); + assertEquals("NOT ", negation.getChild(0).getText()); assertEquals("REFERENCED", negation.getChild(1).getText()); // backreference @@ -1579,26 +1561,26 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT\"/bla/bla/bla\"", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT \"/bla/bla/bla\"", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT\"/bla/bla/bla\"", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT \"/bla/bla/bla\"", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT\"/bla/bla/bla\"", conjunction.getChild(0).getText()); + assertEquals("IS STORED AT \"/bla/bla/bla\"", conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("\"/bla/bla/bla\"", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1624,27 +1606,27 @@ public class TestCQL { assertEquals(StoredAt.class.getName(), sfq.filter.getClass().getName()); // 4 children: FIND, role, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/bla/bla/bla", sfq.getChild(2).getText()); + // assertEquals(4, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /bla/bla/bla", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT/bla/bla/bla", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla/bla", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT/bla/bla/bla", conjunction.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla/bla", conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("/bla/bla/bla", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1671,26 +1653,26 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/bla/bla/bla/", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /bla/bla/bla/", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT/bla/bla/bla/", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla/bla/", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT/bla/bla/bla/", conjunction.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla/bla/", conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("/bla/bla/bla/", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1720,27 +1702,27 @@ public class TestCQL { assertEquals(StoredAt.class.getName(), sfq.filter.getClass().getName()); // 4 children: FIND, role, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/bla/bla/bla.html", sfq.getChild(2).getText()); + // assertEquals(4, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /bla/bla/bla.html", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT/bla/bla/bla.html", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla/bla.html", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT/bla/bla/bla.html", conjunction.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla/bla.html", conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("/bla/bla/bla.html", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1772,26 +1754,26 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT//bla///bla.html", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT //bla///bla.html", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT//bla///bla.html", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT //bla///bla.html", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT//bla///bla.html", conjunction.getChild(0).getText()); + assertEquals("IS STORED AT //bla///bla.html", conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("//bla///bla.html", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1823,26 +1805,26 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/bla/bla_bla.html", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /bla/bla_bla.html", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT/bla/bla_bla.html", entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla_bla.html", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT/bla/bla_bla.html", conjunction.getChild(0).getText()); + assertEquals("IS STORED AT /bla/bla_bla.html", conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("/bla/bla_bla.html", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1876,26 +1858,26 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT" + origPath, sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT " + origPath, sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); - assertEquals("IS STORED AT" + origPath, entity_filter.getChild(1).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); + assertEquals("IS STORED AT " + origPath, entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: storedAt assertEquals(1, conjunction.getChildCount()); - assertEquals("IS STORED AT" + origPath, conjunction.getChild(0).getText()); + assertEquals("IS STORED AT " + origPath, conjunction.getChild(0).getText()); final ParseTree storedat = conjunction.getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals(origPath, storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1926,30 +1908,31 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT\"/bla/bla/bla\"ORHAS Apname2=val2", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals( + "WHICH IS STORED AT \"/bla/bla/bla\" OR HAS A pname2=val2", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); assertEquals( - "IS STORED AT\"/bla/bla/bla\"ORHAS Apname2=val2", entity_filter.getChild(1).getText()); + "IS STORED AT \"/bla/bla/bla\" OR HAS A pname2=val2", entity_filter.getChild(1).getText()); final ParseTree disjunction = entity_filter.getChild(1); // 4 child: storedAt, OR, HAS_A, POV assertEquals(4, disjunction.getChildCount()); - assertEquals("IS STORED AT\"/bla/bla/bla\"", disjunction.getChild(0).getText()); - assertEquals("OR", disjunction.getChild(1).getText()); - assertEquals("HAS A", disjunction.getChild(2).getText()); + assertEquals("IS STORED AT \"/bla/bla/bla\" ", disjunction.getChild(0).getText()); + assertEquals("OR ", disjunction.getChild(1).getText()); + assertEquals("HAS A ", disjunction.getChild(2).getText()); assertEquals("pname2=val2", disjunction.getChild(3).getText()); final ParseTree storedat = disjunction.getChild(0).getChild(0); - // 2 children: IS_STORED_AT, loc - assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + // 3 children: IS_STORED_AT, loc, WHITE_SPACE + assertEquals(3, storedat.getChildCount()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("\"/bla/bla/bla\"", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -1987,36 +1970,38 @@ public class TestCQL { // 4 children: FIND, role, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHHAS Apname2=val2ORIS STORED AT\"/bla/bla/bla\"", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals( + "WHICH HAS A pname2=val2 OR IS STORED AT \"/bla/bla/bla\"", sfq.getChild(2).getText()); // entity_filter final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); - assertEquals("pname2=val2ORIS STORED AT\"/bla/bla/bla\"", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); + assertEquals( + "pname2=val2 OR IS STORED AT \"/bla/bla/bla\"", entity_filter.getChild(1).getText()); final ParseTree disjunction = entity_filter.getChild(1); // 3 children: POV, OR, storedAt assertEquals(3, disjunction.getChildCount()); - assertEquals("pname2=val2", disjunction.getChild(0).getText()); - assertEquals("OR", disjunction.getChild(1).getText()); - assertEquals("IS STORED AT\"/bla/bla/bla\"", disjunction.getChild(2).getText()); + assertEquals("pname2=val2 ", disjunction.getChild(0).getText()); + assertEquals("OR ", disjunction.getChild(1).getText()); + assertEquals("IS STORED AT \"/bla/bla/bla\"", disjunction.getChild(2).getText()); final ParseTree pov = disjunction.getChild(0).getChild(0); // 3 children: p, o, v assertEquals(3, pov.getChildCount()); assertEquals("pname2", pov.getChild(0).getText()); assertEquals("=", pov.getChild(1).getText()); - assertEquals("val2", pov.getChild(2).getText()); + assertEquals("val2 ", pov.getChild(2).getText()); final ParseTree storedat = disjunction.getChild(2).getChild(0); // 2 children: IS_STORED_AT, loc assertEquals(2, storedat.getChildCount()); - assertEquals("IS STORED AT", storedat.getChild(0).getText()); + assertEquals("IS STORED AT ", storedat.getChild(0).getText()); assertEquals("\"/bla/bla/bla\"", storedat.getChild(1).getText()); assertEquals(null, sfq.e); @@ -2053,48 +2038,50 @@ public class TestCQL { // assertEquals(Conjunction.class.getName(), // sfq.filter.getClass().getName()); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals( - "WHICHHAS APROPERTY(pname1=val1WHICHHAS APROPERTYpname2=val2)", sfq.getChild(2).getText()); + "WHICH HAS A PROPERTY ( pname1=val1 WHICH HAS A PROPERTY pname2=val2 )", + sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 4 children: WHICH_EXP, (, conjunction, ) assertEquals(4, entity_filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", entity_filter.getChild(0).getText()); - assertEquals("(", entity_filter.getChild(1).getText()); - assertEquals("pname1=val1WHICHHAS APROPERTYpname2=val2", entity_filter.getChild(2).getText()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter.getChild(0).getText()); + assertEquals("( ", entity_filter.getChild(1).getText()); + assertEquals( + "pname1=val1 WHICH HAS A PROPERTY pname2=val2 ", entity_filter.getChild(2).getText()); assertEquals(")", entity_filter.getChild(3).getText()); final ParseTree conjunction = entity_filter.getChild(2); // 2 children: pov subp assertEquals(2, conjunction.getChildCount()); - assertEquals("pname1=val1", conjunction.getChild(0).getText()); - assertEquals("WHICHHAS APROPERTYpname2=val2", conjunction.getChild(1).getText()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY pname2=val2 ", conjunction.getChild(1).getText()); final ParseTree pov = conjunction.getChild(0); // 3 children: p, o, v assertEquals(3, pov.getChildCount()); assertEquals("pname1", pov.getChild(0).getText()); assertEquals("=", pov.getChild(1).getText()); - assertEquals("val1", pov.getChild(2).getText()); + assertEquals("val1 ", pov.getChild(2).getText()); final ParseTree subproperty = conjunction.getChild(1); // 1 child: entity_filter assertEquals(1, subproperty.getChildCount()); - assertEquals("WHICHHAS APROPERTYpname2=val2", subproperty.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY pname2=val2 ", subproperty.getChild(0).getText()); final ParseTree subEntityFilter = subproperty.getChild(0); // 2 children: WHICH_EXP, pov assertEquals(2, subEntityFilter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", subEntityFilter.getChild(0).getText()); - assertEquals("pname2=val2", subEntityFilter.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY ", subEntityFilter.getChild(0).getText()); + assertEquals("pname2=val2 ", subEntityFilter.getChild(1).getText()); assertEquals("ename", sfq.e.toString()); assertEquals(null, sfq.r); @@ -2125,45 +2112,47 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals( - "WHICHHAS APROPERTYpname1=val1WHICHHAS APROPERTYpname2=val2", sfq.getChild(2).getText()); + "WHICH HAS A PROPERTY pname1=val1 WHICH HAS A PROPERTY pname2=val2", + sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", entity_filter.getChild(0).getText()); - assertEquals("pname1=val1WHICHHAS APROPERTYpname2=val2", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter.getChild(0).getText()); + assertEquals( + "pname1=val1 WHICH HAS A PROPERTY pname2=val2", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 2 children: pov, subp assertEquals(2, conjunction.getChildCount()); - assertEquals("pname1=val1", conjunction.getChild(0).getText()); - assertEquals("WHICHHAS APROPERTYpname2=val2", conjunction.getChild(1).getText()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY pname2=val2", conjunction.getChild(1).getText()); final ParseTree pov = conjunction.getChild(0); // 3 children: p, o, v assertEquals(3, pov.getChildCount()); assertEquals("pname1", pov.getChild(0).getText()); assertEquals("=", pov.getChild(1).getText()); - assertEquals("val1", pov.getChild(2).getText()); + assertEquals("val1 ", pov.getChild(2).getText()); final ParseTree subproperty = conjunction.getChild(1); // 1 child: entity_filter assertEquals(1, subproperty.getChildCount()); - assertEquals("WHICHHAS APROPERTYpname2=val2", subproperty.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY pname2=val2", subproperty.getChild(0).getText()); final ParseTree subEntityFilter = subproperty.getChild(0); // 2 children: WHICH_EXP, pov assertEquals(2, subEntityFilter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", subEntityFilter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", subEntityFilter.getChild(0).getText()); assertEquals("pname2=val2", subEntityFilter.getChild(1).getText()); assertEquals("ename", sfq.e.toString()); @@ -2194,14 +2183,14 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals(".pname1=val1.pname2=val2", sfq.getChild(2).getText()); + assertEquals(".pname1=val1.pname2=val2", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); @@ -2263,14 +2252,14 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals(".(pname1=val1.pname2=val2)", sfq.getChild(2).getText()); + assertEquals(".(pname1=val1.pname2=val2)", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 4 children: WHICH_EXP, (, conjunction, ) assertEquals(4, entity_filter.getChildCount()); @@ -2336,44 +2325,44 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals("WHICHHAS Apname1=val1WITHpname2=val2", sfq.getChild(2).getText()); + assertEquals("WHICH HAS A pname1=val1 WITH pname2=val2", sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); - assertEquals("pname1=val1WITHpname2=val2", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); + assertEquals("pname1=val1 WITH pname2=val2", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 2 children: pov, subp assertEquals(2, conjunction.getChildCount()); - assertEquals("pname1=val1", conjunction.getChild(0).getText()); - assertEquals("WITHpname2=val2", conjunction.getChild(1).getText()); + assertEquals("pname1=val1 ", conjunction.getChild(0).getText()); + assertEquals("WITH pname2=val2", conjunction.getChild(1).getText()); final ParseTree pov = conjunction.getChild(0); // 3 children: p, o, v assertEquals(3, pov.getChildCount()); assertEquals("pname1", pov.getChild(0).getText()); assertEquals("=", pov.getChild(1).getText()); - assertEquals("val1", pov.getChild(2).getText()); + assertEquals("val1 ", pov.getChild(2).getText()); final ParseTree subproperty = conjunction.getChild(1); // 1 child: entity_filter assertEquals(1, subproperty.getChildCount()); - assertEquals("WITHpname2=val2", subproperty.getChild(0).getText()); + assertEquals("WITH pname2=val2", subproperty.getChild(0).getText()); final ParseTree subEntityFilter = subproperty.getChild(0); // 2 children: WHICH_EXP, pov assertEquals(2, subEntityFilter.getChildCount()); - assertEquals("WITH", subEntityFilter.getChild(0).getText()); + assertEquals("WITH ", subEntityFilter.getChild(0).getText()); assertEquals("pname2=val2", subEntityFilter.getChild(1).getText()); assertEquals("ename", sfq.e.toString()); @@ -2405,50 +2394,53 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals( - "WHICHHAS A->ename2WHICHHAS A->ename3WHICHHAS APROPERTYpname1=val1", - sfq.getChild(2).getText()); + "WHICH HAS A -> ename2 WHICH HAS A -> ename3 WHICH HAS A PROPERTY pname1=val1", + sfq.getChild(3).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICHHAS A", entity_filter.getChild(0).getText()); + assertEquals("WHICH HAS A ", entity_filter.getChild(0).getText()); assertEquals( - "->ename2WHICHHAS A->ename3WHICHHAS APROPERTYpname1=val1", + "-> ename2 WHICH HAS A -> ename3 WHICH HAS A PROPERTY pname1=val1", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 2 children: pov, subp assertEquals(2, conjunction.getChildCount()); - assertEquals("->ename2", conjunction.getChild(0).getText()); + assertEquals("-> ename2 ", conjunction.getChild(0).getText()); assertEquals( - "WHICHHAS A->ename3WHICHHAS APROPERTYpname1=val1", conjunction.getChild(1).getText()); + "WHICH HAS A -> ename3 WHICH HAS A PROPERTY pname1=val1", + conjunction.getChild(1).getText()); final ParseTree pov = conjunction.getChild(0); - // 2 children: ->, ? - assertEquals(2, pov.getChildCount()); + // 3 children: ->, WHITE_SPACE, ename2 + assertEquals(3, pov.getChildCount()); assertEquals("->", pov.getChild(0).getText()); - assertEquals("ename2", pov.getChild(1).getText()); + assertEquals("ename2 ", pov.getChild(2).getText()); final ParseTree subproperty = conjunction.getChild(1); // 1 child: entity_filter assertEquals(1, subproperty.getChildCount()); assertEquals( - "WHICHHAS A->ename3WHICHHAS APROPERTYpname1=val1", subproperty.getChild(0).getText()); + "WHICH HAS A -> ename3 WHICH HAS A PROPERTY pname1=val1", + subproperty.getChild(0).getText()); final ParseTree subEntityFilter = subproperty.getChild(0); // 2 children: WHICH_EXP, subp assertEquals(2, subEntityFilter.getChildCount()); - assertEquals("WHICHHAS A", subEntityFilter.getChild(0).getText()); - assertEquals("->ename3WHICHHAS APROPERTYpname1=val1", subEntityFilter.getChild(1).getText()); + assertEquals("WHICH HAS A ", subEntityFilter.getChild(0).getText()); + assertEquals( + "-> ename3 WHICH HAS A PROPERTY pname1=val1", subEntityFilter.getChild(1).getText()); assertEquals("ename1", sfq.e.toString()); assertEquals(null, sfq.r); @@ -2486,7 +2478,7 @@ public class TestCQL { // 4 children: FIND, ename, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(".ename2.ename3.pname1=val1", sfq.getChild(2).getText()); @@ -2553,40 +2545,37 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - // assertEquals("WHICH HAS A PROPERTY ( ename2 WHICH HAS A PROPERTY - // pname1=val1)", - // sfq - // .getChild(4).getText()); - // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + assertEquals( + "WHICH HAS A PROPERTY ( WHICH HAS A PROPERTY pname1=val1)", sfq.getChild(3).getText()); + final ParseTree entity_filter = sfq.getChild(3); - // 4 children: WHICH_EXP, (, conjunction, ) + // 4 children: WHICH_EXP, (, subproperty, ) assertEquals(4, entity_filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", entity_filter.getChild(0).getText()); - assertEquals("(", entity_filter.getChild(1).getText()); - assertEquals("WHICHHAS APROPERTYpname1=val1", entity_filter.getChild(2).getText()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter.getChild(0).getText()); + assertEquals("( ", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY pname1=val1", entity_filter.getChild(2).getText()); assertEquals(")", entity_filter.getChild(3).getText()); - final ParseTree conjunction = entity_filter.getChild(2); + final ParseTree subproperty1 = entity_filter.getChild(2); - // 1 children - assertEquals(1, conjunction.getChildCount()); - assertEquals("WHICHHAS APROPERTYpname1=val1", conjunction.getChild(0).getText()); - final ParseTree subp = conjunction.getChild(0); + // 1 child: filter + assertEquals(1, subproperty1.getChildCount()); + assertEquals("WHICH HAS A PROPERTY pname1=val1", subproperty1.getChild(0).getText()); + final ParseTree filter = subproperty1.getChild(0); // 1 children: filter - assertEquals(1, subp.getChildCount()); - assertEquals("WHICHHAS APROPERTYpname1=val1", subp.getChild(0).getText()); + assertEquals(1, filter.getChildCount()); + assertEquals("WHICH HAS A PROPERTY pname1=val1", filter.getChild(0).getText()); - final ParseTree subproperty = subp.getChild(0); + final ParseTree subproperty = filter.getChild(0); - // 2 children: entity_filter + // 2 children: WHICH entity_filter assertEquals(2, subproperty.getChildCount()); - assertEquals("WHICHHAS APROPERTY", subproperty.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", subproperty.getChild(0).getText()); assertEquals("pname1=val1", subproperty.getChild(1).getText()); assertEquals("ename1", sfq.e.toString()); @@ -2613,41 +2602,44 @@ public class TestCQL { assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); assertEquals(null, sfq.r); - // 4 children: FIND, ename, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename1", sfq.getChild(1).getText()); // entity_filter - final ParseTree entity_filter = sfq.getChild(2); + final ParseTree entity_filter = sfq.getChild(3); // 4 children: WHICH_EXP, (, conjunction, ) assertEquals(4, entity_filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", entity_filter.getChild(0).getText()); - assertEquals("(", entity_filter.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY ", entity_filter.getChild(0).getText()); + assertEquals("( ", entity_filter.getChild(1).getText()); assertEquals( - "WHICHHAS APROPERTY(WHICHHAS APROPERTY(pname1=val1))", entity_filter.getChild(2).getText()); + "WHICH HAS A PROPERTY ( WHICH HAS A PROPERTY (pname1=val1) ) ", + entity_filter.getChild(2).getText()); assertEquals(")", entity_filter.getChild(3).getText()); final ParseTree conjunction = entity_filter.getChild(2); // 1 children assertEquals(1, conjunction.getChildCount()); assertEquals( - "WHICHHAS APROPERTY(WHICHHAS APROPERTY(pname1=val1))", conjunction.getChild(0).getText()); + "WHICH HAS A PROPERTY ( WHICH HAS A PROPERTY (pname1=val1) ) ", + conjunction.getChild(0).getText()); final ParseTree subp = conjunction.getChild(0); // 1 children: filter assertEquals(1, subp.getChildCount()); - assertEquals("WHICHHAS APROPERTY(WHICHHAS APROPERTY(pname1=val1))", subp.getChild(0).getText()); + assertEquals( + "WHICH HAS A PROPERTY ( WHICH HAS A PROPERTY (pname1=val1) ) ", subp.getChild(0).getText()); final ParseTree subproperty = subp.getChild(0); // 4 children: which, (, conjunction, ) assertEquals(4, subproperty.getChildCount()); - assertEquals("WHICHHAS APROPERTY", subproperty.getChild(0).getText()); - assertEquals("(", subproperty.getChild(1).getText()); - assertEquals("WHICHHAS APROPERTY(pname1=val1)", subproperty.getChild(2).getText()); - assertEquals(")", subproperty.getChild(3).getText()); + assertEquals("WHICH HAS A PROPERTY ", subproperty.getChild(0).getText()); + assertEquals("( ", subproperty.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY (pname1=val1) ", subproperty.getChild(2).getText()); + assertEquals(") ", subproperty.getChild(3).getText()); assertEquals("ename1", sfq.e.toString()); assertEquals(null, sfq.r); @@ -2681,13 +2673,13 @@ public class TestCQL { // 4 children: FIND, ename, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals(".", entity_filter.getChild(0).getText()); + assertEquals(". ", entity_filter.getChild(0).getText()); assertEquals("pname->ename", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); @@ -2739,7 +2731,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename#", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); } @@ -2756,7 +2748,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("#ename", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); } @@ -2775,7 +2767,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("<<ename-regexp>>", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_REGEXP, sfq.e.type); assertEquals("ename-regexp", sfq.e.toString()); @@ -2798,7 +2790,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("<<ename\\>>regexp>>", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_REGEXP, sfq.e.type); assertEquals("ename>>regexp", sfq.e.toString()); @@ -2822,7 +2814,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("*ename", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_LIKE, sfq.e.type); assertEquals("%ename", sfq.e.toString()); @@ -2845,7 +2837,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("en*ame", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_LIKE, sfq.e.type); assertEquals("en%ame", sfq.e.toString()); @@ -2868,7 +2860,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("SimpleD*Property", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_LIKE, sfq.e.type); assertEquals("SimpleD%Property", sfq.e.toString()); @@ -2891,7 +2883,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename*", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_LIKE, sfq.e.type); assertEquals("ename%", sfq.e.toString()); @@ -2914,7 +2906,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("<<SimpleD.*Property>>", sfq.getChild(1).getText()); assertEquals(Query.Pattern.TYPE_REGEXP, sfq.e.type); assertEquals("SimpleD.*Property", sfq.e.toString()); @@ -2935,11 +2927,11 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals(".THEGREATESTpname", sfq.getChild(2).getText()); + assertEquals(". THE GREATEST pname", sfq.getChild(3).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); } @@ -2958,11 +2950,11 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals(".THESMALLESTpname", sfq.getChild(2).getText()); + assertEquals(". THE SMALLEST pname", sfq.getChild(3).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); } @@ -2982,17 +2974,17 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("SimpleRecordType", sfq.getChild(1).getText()); - assertEquals("WITHTHEGREATESTSimpleDoubleProperty>0", sfq.getChild(2).getText()); + assertEquals("WITH THE GREATEST SimpleDoubleProperty>0", sfq.getChild(3).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals(2, sfq.getChild(2).getChildCount()); - final ParseTree filter = sfq.getChild(2); - assertEquals("WITH", filter.getChild(0).getText()); - assertEquals("THEGREATESTSimpleDoubleProperty>0", filter.getChild(1).getText()); + assertEquals(2, sfq.getChild(3).getChildCount()); + final ParseTree filter = sfq.getChild(3); + assertEquals("WITH ", filter.getChild(0).getText()); + assertEquals("THE GREATEST SimpleDoubleProperty>0", filter.getChild(1).getText()); assertEquals(1, filter.getChild(1).getChildCount()); assertEquals(3, filter.getChild(1).getChild(0).getChildCount()); @@ -3021,13 +3013,13 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'ANDAPROPERTY(BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016')", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND A PROPERTY ( BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016')", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); assertNotNull(sfq.filter); @@ -3066,24 +3058,24 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHHAS APROPERTYpname=val1AND(pname=val2)", sfq.getChild(2).getText()); + assertEquals("WHICH HAS A PROPERTY pname=val1 AND (pname=val2)", sfq.getChild(3).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(2); + final ParseTree filter = sfq.getChild(3); assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); - assertEquals("pname=val1AND(pname=val2)", filter.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); + assertEquals("pname=val1 AND (pname=val2)", filter.getChild(1).getText()); final ParseTree conjunction1 = filter.getChild(1); // 5 children: pov, AND, (, pov, ) assertEquals(5, conjunction1.getChildCount()); - assertEquals("pname=val1", conjunction1.getChild(0).getText()); - assertEquals("AND", conjunction1.getChild(1).getText()); + assertEquals("pname=val1 ", conjunction1.getChild(0).getText()); + assertEquals("AND ", conjunction1.getChild(1).getText()); assertEquals("(", conjunction1.getChild(2).getText()); assertEquals("pname=val2", conjunction1.getChild(3).getText()); assertEquals(")", conjunction1.getChild(4).getText()); @@ -3120,41 +3112,41 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'AND(BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016')", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND ( BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016')", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); + final ParseTree filter = sfq.getChild(4); assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); assertEquals( - "ObstacleRadius='2.0'AND(BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016')", + "ObstacleRadius = '2.0' AND ( BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016')", filter.getChild(1).getText()); final ParseTree conjunction1 = filter.getChild(1); assertEquals(5, conjunction1.getChildCount()); - assertEquals("ObstacleRadius='2.0'", conjunction1.getChild(0).getText()); - assertEquals("AND", conjunction1.getChild(1).getText()); - assertEquals("(", conjunction1.getChild(2).getText()); + assertEquals("ObstacleRadius = '2.0' ", conjunction1.getChild(0).getText()); + assertEquals("AND ", conjunction1.getChild(1).getText()); + assertEquals("( ", conjunction1.getChild(2).getText()); assertEquals( - "BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", + "BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", conjunction1.getChild(3).getText()); assertEquals(")", conjunction1.getChild(4).getText()); final ParseTree sub = conjunction1.getChild(3); assertEquals(2, sub.getChildCount()); - assertEquals("BarkleyModelSimulation", sub.getChild(0).getText()); - assertEquals("WHICHHAS APROPERTYTimeStep='0.0016'", sub.getChild(1).getText()); + assertEquals("BarkleyModelSimulation ", sub.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY TimeStep='0.0016'", sub.getChild(1).getText()); final ParseTree con2 = sub.getChild(1).getChild(0); assertEquals(2, con2.getChildCount()); - assertEquals("WHICHHAS APROPERTY", con2.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", con2.getChild(0).getText()); assertEquals("TimeStep='0.0016'", con2.getChild(1).getText()); assertNotNull(sfq.filter); @@ -3194,42 +3186,42 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'ANDBarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); + final ParseTree filter = sfq.getChild(4); assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); assertEquals( - "ObstacleRadius='2.0'ANDBarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", + "ObstacleRadius = '2.0' AND BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", filter.getChild(1).getText()); final ParseTree conjunction1 = filter.getChild(1); assertEquals(3, conjunction1.getChildCount()); - assertEquals("ObstacleRadius='2.0'", conjunction1.getChild(0).getText()); - assertEquals("AND", conjunction1.getChild(1).getText()); + assertEquals("ObstacleRadius = '2.0' ", conjunction1.getChild(0).getText()); + assertEquals("AND ", conjunction1.getChild(1).getText()); assertEquals( - "BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", + "BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", conjunction1.getChild(2).getText()); final ParseTree filter_exp = conjunction1.getChild(2); assertEquals(2, filter_exp.getChildCount()); - assertEquals("BarkleyModelSimulation", filter_exp.getChild(0).getText()); - assertEquals("WHICHHAS APROPERTYTimeStep='0.0016'", filter_exp.getChild(1).getText()); + assertEquals("BarkleyModelSimulation ", filter_exp.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY TimeStep='0.0016'", filter_exp.getChild(1).getText()); final ParseTree sub = filter_exp.getChild(1); assertEquals(1, sub.getChildCount()); - assertEquals("WHICHHAS APROPERTYTimeStep='0.0016'", sub.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY TimeStep='0.0016'", sub.getChild(0).getText()); final ParseTree sub1 = sub.getChild(0); assertEquals(2, sub1.getChildCount()); - assertEquals("WHICHHAS APROPERTY", sub1.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", sub1.getChild(0).getText()); assertEquals("TimeStep='0.0016'", sub1.getChild(1).getText()); assertNotNull(sfq.filter); @@ -3271,43 +3263,43 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'ANDAPROPERTYBarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND A PROPERTY BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); + final ParseTree filter = sfq.getChild(4); assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); assertEquals( - "ObstacleRadius='2.0'ANDAPROPERTYBarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", + "ObstacleRadius = '2.0' AND A PROPERTY BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", filter.getChild(1).getText()); final ParseTree conjunction1 = filter.getChild(1); assertEquals(5, conjunction1.getChildCount()); - assertEquals("ObstacleRadius='2.0'", conjunction1.getChild(0).getText()); - assertEquals("AND", conjunction1.getChild(1).getText()); - assertEquals("A", conjunction1.getChild(2).getText()); - assertEquals("PROPERTY", conjunction1.getChild(3).getText()); + assertEquals("ObstacleRadius = '2.0' ", conjunction1.getChild(0).getText()); + assertEquals("AND ", conjunction1.getChild(1).getText()); + assertEquals("A ", conjunction1.getChild(2).getText()); + assertEquals("PROPERTY ", conjunction1.getChild(3).getText()); assertEquals( - "BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", + "BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", conjunction1.getChild(4).getText()); final ParseTree filter_exp = conjunction1.getChild(4); - assertEquals("BarkleyModelSimulation", filter_exp.getChild(0).getText()); - assertEquals("WHICHHAS APROPERTYTimeStep='0.0016'", filter_exp.getChild(1).getText()); + assertEquals("BarkleyModelSimulation ", filter_exp.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY TimeStep='0.0016'", filter_exp.getChild(1).getText()); final ParseTree sub = filter_exp.getChild(1); assertEquals(1, sub.getChildCount()); - assertEquals("WHICHHAS APROPERTYTimeStep='0.0016'", sub.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY TimeStep='0.0016'", sub.getChild(0).getText()); final ParseTree sub1 = sub.getChild(0); assertEquals(2, sub1.getChildCount()); - assertEquals("WHICHHAS APROPERTY", sub1.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", sub1.getChild(0).getText()); assertEquals("TimeStep='0.0016'", sub1.getChild(1).getText()); assertNotNull(sfq.filter); @@ -3349,30 +3341,31 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'ANDAPROPERTY(BarkleyModelSimulation)", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND A PROPERTY ( BarkleyModelSimulation )", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); + final ParseTree filter = sfq.getChild(4); assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); assertEquals( - "ObstacleRadius='2.0'ANDAPROPERTY(BarkleyModelSimulation)", filter.getChild(1).getText()); + "ObstacleRadius = '2.0' AND A PROPERTY ( BarkleyModelSimulation )", + filter.getChild(1).getText()); final ParseTree conjunction1 = filter.getChild(1); assertEquals(7, conjunction1.getChildCount()); - assertEquals("ObstacleRadius='2.0'", conjunction1.getChild(0).getText()); - assertEquals("AND", conjunction1.getChild(1).getText()); - assertEquals("A", conjunction1.getChild(2).getText()); - assertEquals("PROPERTY", conjunction1.getChild(3).getText()); - assertEquals("(", conjunction1.getChild(4).getText()); - assertEquals("BarkleyModelSimulation", conjunction1.getChild(5).getText()); + assertEquals("ObstacleRadius = '2.0' ", conjunction1.getChild(0).getText()); + assertEquals("AND ", conjunction1.getChild(1).getText()); + assertEquals("A ", conjunction1.getChild(2).getText()); + assertEquals("PROPERTY ", conjunction1.getChild(3).getText()); + assertEquals("( ", conjunction1.getChild(4).getText()); + assertEquals("BarkleyModelSimulation ", conjunction1.getChild(5).getText()); assertEquals(")", conjunction1.getChild(6).getText()); assertNotNull(sfq.filter); @@ -3409,33 +3402,33 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, entity_filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHHAS APROPERTYpname1=val1AND(pname2)", sfq.getChild(2).getText()); + assertEquals("WHICH HAS A PROPERTY pname1=val1 AND ( pname2 )", sfq.getChild(3).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(2); + final ParseTree filter = sfq.getChild(3); assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); - assertEquals("pname1=val1AND(pname2)", filter.getChild(1).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); + assertEquals("pname1=val1 AND ( pname2 )", filter.getChild(1).getText()); final ParseTree conjunction1 = filter.getChild(1); assertEquals(5, conjunction1.getChildCount()); - assertEquals("pname1=val1", conjunction1.getChild(0).getText()); - assertEquals("AND", conjunction1.getChild(1).getText()); - assertEquals("(", conjunction1.getChild(2).getText()); - assertEquals("pname2", conjunction1.getChild(3).getText()); + assertEquals("pname1=val1 ", conjunction1.getChild(0).getText()); + assertEquals("AND ", conjunction1.getChild(1).getText()); + assertEquals("( ", conjunction1.getChild(2).getText()); + assertEquals("pname2 ", conjunction1.getChild(3).getText()); assertEquals(")", conjunction1.getChild(4).getText()); final ParseTree sub = conjunction1.getChild(3); assertEquals(1, sub.getChildCount()); - assertEquals("pname2", sub.getChild(0).getText()); + assertEquals("pname2 ", sub.getChild(0).getText()); final ParseTree sub1 = sub.getChild(0); assertEquals(1, sub1.getChildCount()); - assertEquals("pname2", sub1.getChild(0).getText()); + assertEquals("pname2 ", sub1.getChild(0).getText()); assertNotNull(sfq.filter); assertEquals(Conjunction.class.getName(), sfq.filter.getClass().getName()); @@ -3466,14 +3459,14 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'ANDAPROPERTY(BarkleyModelSimulationWHICHHAS ATimeStep='0.0016')", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND A PROPERTY ( BarkleyModelSimulation WHICH HAS A TimeStep='0.0016')", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); assertNotNull(sfq.filter); @@ -3509,14 +3502,14 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYObstacleRadius='2.0'ANDAPROPERTY(BarkleyModelSimulationWHICHHASTimeStep='0.0016')", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ObstacleRadius = '2.0' AND A PROPERTY ( BarkleyModelSimulation WHICH HAS TimeStep='0.0016')", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); } @@ -3573,14 +3566,14 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, RECORD, entity, entity_filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, RECORD, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("ticket147_FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYticket147_ObstacleRadius='2.0'ANDAPROPERTY(ticket147_BarkleyModelSimulationWHICHHASticket147_TimeStep='0.0016')", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ticket147_ObstacleRadius = '2.0' AND A PROPERTY ( ticket147_BarkleyModelSimulation WHICH HAS ticket147_TimeStep='0.0016')", + sfq.getChild(4).getText()); assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); } @@ -3601,7 +3594,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("'Some name with spaces and 1234 numbers'", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); @@ -3626,7 +3619,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("\"Some name with spaces and 1234 numbers\"", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); @@ -3651,7 +3644,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("'Some name with spaces and 1234 numbers and \"'", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); @@ -3677,7 +3670,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("'Some name with spaces and 1234 numbers and \\*'", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); @@ -3702,7 +3695,7 @@ public class TestCQL { // 3 children: FIND, entity, EOF assertEquals(3, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("'Some name with spaces and 1234 numbers and *'", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_LIKE, sfq.e.type); @@ -3711,7 +3704,7 @@ public class TestCQL { } /* - * String query28 = "FIND ename . pname=2.0"; + * String query28 = "FIND ename . pname=2.02"; */ @Test public void testQuery28() @@ -3724,20 +3717,21 @@ public class TestCQL { final CqContext sfq = parser.cq(); System.out.println(sfq.toStringTree(parser)); + assertEquals("POV(pname,=,2.02)", sfq.filter.toString()); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals(".pname=2.0", sfq.getChild(2).getText()); + assertEquals(". pname=2.02", sfq.getChild(3).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - assertEquals(2, sfq.getChild(2).getChildCount()); - assertEquals(".", sfq.getChild(2).getChild(0).getText()); - assertEquals("pname=2.0", sfq.getChild(2).getChild(1).getText()); - assertEquals(1, sfq.getChild(2).getChild(1).getChildCount()); - assertEquals("pname=2.0", sfq.getChild(2).getChild(1).getChild(0).getText()); - assertEquals("2.0", sfq.getChild(2).getChild(1).getChild(0).getChild(2).getText()); + assertEquals(2, sfq.getChild(3).getChildCount()); + assertEquals(". ", sfq.getChild(3).getChild(0).getText()); + assertEquals("pname=2.02", sfq.getChild(3).getChild(1).getText()); + assertEquals(1, sfq.getChild(3).getChild(1).getChildCount()); + assertEquals("pname=2.02", sfq.getChild(3).getChild(1).getChild(0).getText()); + assertEquals("2.02", sfq.getChild(3).getChild(1).getChild(0).getChild(2).getText()); } /* @@ -3754,58 +3748,88 @@ public class TestCQL { final CqContext sfq = parser.cq(); System.out.println(sfq.toStringTree(parser)); + assertEquals("POV(pname,=,2.0prop=test)", sfq.filter.toString()); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals(".pname=2.0prop=test", sfq.getChild(2).getText()); + assertEquals(". pname=2.0prop=test", sfq.getChild(3).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); + } - assertEquals(2, sfq.getChild(2).getChildCount()); - assertEquals(".", sfq.getChild(2).getChild(0).getText()); - assertEquals("pname=2.0prop=test", sfq.getChild(2).getChild(1).getText()); - assertEquals(2, sfq.getChild(2).getChild(1).getChildCount()); - assertEquals("pname=2", sfq.getChild(2).getChild(1).getChild(0).getText()); - assertEquals(".0prop=test", sfq.getChild(2).getChild(1).getChild(1).getText()); - assertEquals("2", sfq.getChild(2).getChild(1).getChild(0).getChild(2).getText()); - assertEquals(1, sfq.getChild(2).getChild(1).getChild(1).getChildCount()); - assertEquals(".0prop=test", sfq.getChild(2).getChild(1).getChild(1).getChild(0).getText()); - assertEquals(2, sfq.getChild(2).getChild(1).getChild(1).getChild(0).getChildCount()); - assertEquals(".", sfq.getChild(2).getChild(1).getChild(1).getChild(0).getChild(0).getText()); - assertEquals( - "0prop=test", - sfq.getChild(2).getChild(1).getChild(1).getChild(0).getChild(1).getChild(0).getText()); - assertEquals( - "0prop", - sfq.getChild(2) - .getChild(1) - .getChild(1) - .getChild(0) - .getChild(1) - .getChild(0) - .getChild(0) - .getText()); - assertEquals( - "=", - sfq.getChild(2) - .getChild(1) - .getChild(1) - .getChild(0) - .getChild(1) - .getChild(0) - .getChild(1) - .getText()); - assertEquals( - "test", - sfq.getChild(2) - .getChild(1) - .getChild(1) - .getChild(0) - .getChild(1) - .getChild(0) - .getChild(2) - .getText()); + /** String query28b = "FIND ename . pname = 1.02m"; */ + @Test + public void testQuery28b() + throws InterruptedException, SQLException, ConnectionException, QueryException { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query28b)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + assertEquals("POV(pname,=,1.02m)", sfq.filter.toString()); + + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("ename", sfq.getChild(1).getText()); + assertEquals(". pname = 1.02m", sfq.getChild(3).getText()); + assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); + + assertEquals(2, sfq.getChild(3).getChildCount()); + assertEquals(". ", sfq.getChild(3).getChild(0).getText()); + assertEquals("pname = 1.02m", sfq.getChild(3).getChild(1).getText()); + assertEquals(1, sfq.getChild(3).getChild(1).getChildCount()); + assertEquals("pname = 1.02m", sfq.getChild(3).getChild(1).getChild(0).getText()); + assertEquals("1.02m", sfq.getChild(3).getChild(1).getChild(0).getChild(3).getText()); + } + + /** String query28c = "FIND ename . pname = .02"; */ + @Test + public void testQuery28c() + throws InterruptedException, SQLException, ConnectionException, QueryException { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query28c)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + assertEquals("POV(pname,=,.02)", sfq.filter.toString()); + } + + /** String query28d = "FIND ename . pname =.02m"; */ + @Test + public void testQuery28d() + throws InterruptedException, SQLException, ConnectionException, QueryException { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query28d)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + assertEquals("POV(pname,=,.02m)", sfq.filter.toString()); + } + + /** String query28e = "FIND ename . pname =.02 1/m^2"; */ + @Test + public void testQuery28e() + throws InterruptedException, SQLException, ConnectionException, QueryException { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query28e)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + assertEquals("POV(pname,=,.02 1/m^2)", sfq.filter.toString()); } /* @@ -3825,45 +3849,45 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTY(BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016')ANDAPROPERTYMinPeakHeight='0.5'", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY ( BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016') AND A PROPERTY MinPeakHeight = '0.5'", + sfq.getChild(4).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); - // 2 children: WHICH, conjunction + final ParseTree filter = sfq.getChild(4); + // 2 children: WHICH , conjunction assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); assertEquals( - "(BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016')ANDAPROPERTYMinPeakHeight='0.5'", + "( BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016') AND A PROPERTY MinPeakHeight = '0.5'", filter.getChild(1).getText()); final ParseTree conjunction = filter.getChild(1); // 7 children: (, filter, ), AND, A, PROPERTY, filter assertEquals(7, conjunction.getChildCount()); - assertEquals("(", conjunction.getChild(0).getText()); + assertEquals("( ", conjunction.getChild(0).getText()); assertEquals( - "BarkleyModelSimulationWHICHHAS APROPERTYTimeStep='0.0016'", + "BarkleyModelSimulation WHICH HAS A PROPERTY TimeStep='0.0016'", conjunction.getChild(1).getText()); - assertEquals(")", conjunction.getChild(2).getText()); - assertEquals("AND", conjunction.getChild(3).getText()); - assertEquals("A", conjunction.getChild(4).getText()); - assertEquals("PROPERTY", conjunction.getChild(5).getText()); - assertEquals("MinPeakHeight='0.5'", conjunction.getChild(6).getText()); + assertEquals(") ", conjunction.getChild(2).getText()); + assertEquals("AND ", conjunction.getChild(3).getText()); + assertEquals("A ", conjunction.getChild(4).getText()); + assertEquals("PROPERTY ", conjunction.getChild(5).getText()); + assertEquals("MinPeakHeight = '0.5'", conjunction.getChild(6).getText()); final ParseTree pov1 = conjunction.getChild(1).getChild(0); - assertEquals("BarkleyModelSimulation", pov1.getText()); + assertEquals("BarkleyModelSimulation ", pov1.getText()); final ParseTree subp = conjunction.getChild(1).getChild(1); - assertEquals("WHICHHAS APROPERTYTimeStep='0.0016'", subp.getText()); + assertEquals("WHICH HAS A PROPERTY TimeStep='0.0016'", subp.getText()); final ParseTree pov2 = subp.getChild(0).getChild(1); assertEquals("TimeStep='0.0016'", pov2.getText()); final ParseTree pov3 = conjunction.getChild(6).getChild(0); - assertEquals("MinPeakHeight='0.5'", pov3.getText()); + assertEquals("MinPeakHeight = '0.5'", pov3.getText()); assertEquals(sfq.e.toString(), "FrequencyMeasurement"); assertEquals(sfq.r, Query.Role.RECORD); @@ -3898,44 +3922,44 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, role, entity, filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, role, entity, WHITE_SPACE, filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - ".(BarkleyModelSimulation.TimeStep='0.0016')ANDAPROPERTYMinPeakHeight='0.5'", - sfq.getChild(3).getText()); + ". ( BarkleyModelSimulation . TimeStep='0.0016') AND A PROPERTY MinPeakHeight = '0.5'", + sfq.getChild(4).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); - // 2 children: WHICH, conjunction + final ParseTree filter = sfq.getChild(4); + // 2 children: WHICH , conjunction assertEquals(2, filter.getChildCount()); - assertEquals(".", filter.getChild(0).getText()); + assertEquals(". ", filter.getChild(0).getText()); assertEquals( - "(BarkleyModelSimulation.TimeStep='0.0016')ANDAPROPERTYMinPeakHeight='0.5'", + "( BarkleyModelSimulation . TimeStep='0.0016') AND A PROPERTY MinPeakHeight = '0.5'", filter.getChild(1).getText()); final ParseTree conjunction = filter.getChild(1); // 7 children: (, filter, ), AND, A, PROPERTY, filter assertEquals(7, conjunction.getChildCount()); - assertEquals("(", conjunction.getChild(0).getText()); - assertEquals("BarkleyModelSimulation.TimeStep='0.0016'", conjunction.getChild(1).getText()); - assertEquals(")", conjunction.getChild(2).getText()); - assertEquals("AND", conjunction.getChild(3).getText()); - assertEquals("A", conjunction.getChild(4).getText()); - assertEquals("PROPERTY", conjunction.getChild(5).getText()); - assertEquals("MinPeakHeight='0.5'", conjunction.getChild(6).getText()); + assertEquals("( ", conjunction.getChild(0).getText()); + assertEquals("BarkleyModelSimulation . TimeStep='0.0016'", conjunction.getChild(1).getText()); + assertEquals(") ", conjunction.getChild(2).getText()); + assertEquals("AND ", conjunction.getChild(3).getText()); + assertEquals("A ", conjunction.getChild(4).getText()); + assertEquals("PROPERTY ", conjunction.getChild(5).getText()); + assertEquals("MinPeakHeight = '0.5'", conjunction.getChild(6).getText()); final ParseTree pov1 = conjunction.getChild(1).getChild(0); - assertEquals("BarkleyModelSimulation", pov1.getText()); + assertEquals("BarkleyModelSimulation ", pov1.getText()); final ParseTree subp = conjunction.getChild(1).getChild(1); - assertEquals(".TimeStep='0.0016'", subp.getText()); + assertEquals(". TimeStep='0.0016'", subp.getText()); final ParseTree pov2 = subp.getChild(0).getChild(1); assertEquals("TimeStep='0.0016'", pov2.getText()); final ParseTree pov3 = conjunction.getChild(6).getChild(0); - assertEquals("MinPeakHeight='0.5'", pov3.getText()); + assertEquals("MinPeakHeight = '0.5'", pov3.getText()); assertEquals(sfq.e.toString(), "FrequencyMeasurement"); assertEquals(sfq.r, Query.Role.RECORD); @@ -3969,47 +3993,47 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 5 children: FIND, role, entity, filter, EOF - assertEquals(5, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + // 6 children: FIND, role, entity, WHITE_SPACE, filter, EOF + assertEquals(6, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); assertEquals("FrequencyMeasurement", sfq.getChild(2).getText()); assertEquals( - "WHICHHAS APROPERTYMinPeakHeight='0.5'ANDAPROPERTY(BarkleyModelSimulation.TimeStep='0.0016')", - sfq.getChild(3).getText()); + "WHICH HAS A PROPERTY MinPeakHeight = '0.5' AND A PROPERTY ( BarkleyModelSimulation . TimeStep='0.0016')", + sfq.getChild(4).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(3); - // 2 children: WHICH, conjunction, + final ParseTree filter = sfq.getChild(4); + // 2 children: WHICH , conjunction, assertEquals(2, filter.getChildCount()); - assertEquals("WHICHHAS APROPERTY", filter.getChild(0).getText()); + assertEquals("WHICH HAS A PROPERTY ", filter.getChild(0).getText()); assertEquals( - "MinPeakHeight='0.5'ANDAPROPERTY(BarkleyModelSimulation.TimeStep='0.0016')", + "MinPeakHeight = '0.5' AND A PROPERTY ( BarkleyModelSimulation . TimeStep='0.0016')", filter.getChild(1).getText()); final ParseTree conjunction = filter.getChild(1); // 7 children: filter_exp, AND, A, PROPERTY, (, filter_exp, ) assertEquals(7, conjunction.getChildCount()); - assertEquals("MinPeakHeight='0.5'", conjunction.getChild(0).getText()); - assertEquals("AND", conjunction.getChild(1).getText()); - assertEquals("A", conjunction.getChild(2).getText()); - assertEquals("PROPERTY", conjunction.getChild(3).getText()); - assertEquals("(", conjunction.getChild(4).getText()); - assertEquals("BarkleyModelSimulation.TimeStep='0.0016'", conjunction.getChild(5).getText()); + assertEquals("MinPeakHeight = '0.5' ", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(1).getText()); + assertEquals("A ", conjunction.getChild(2).getText()); + assertEquals("PROPERTY ", conjunction.getChild(3).getText()); + assertEquals("( ", conjunction.getChild(4).getText()); + assertEquals("BarkleyModelSimulation . TimeStep='0.0016'", conjunction.getChild(5).getText()); assertEquals(")", conjunction.getChild(6).getText()); final ParseTree pov1 = conjunction.getChild(0).getChild(0); - assertEquals(3, pov1.getChildCount()); - assertEquals("MinPeakHeight", pov1.getChild(0).getText()); + assertEquals(4, pov1.getChildCount()); + assertEquals("MinPeakHeight ", pov1.getChild(0).getText()); assertEquals("=", pov1.getChild(1).getText()); - assertEquals("'0.5'", pov1.getChild(2).getText()); + assertEquals("'0.5' ", pov1.getChild(3).getText()); final ParseTree ref = conjunction.getChild(5); assertEquals(2, ref.getChildCount()); - assertEquals("BarkleyModelSimulation", ref.getChild(0).getText()); - assertEquals(".TimeStep='0.0016'", ref.getChild(1).getText()); + assertEquals("BarkleyModelSimulation ", ref.getChild(0).getText()); + assertEquals(". TimeStep='0.0016'", ref.getChild(1).getText()); // this is a pov final ParseTree subp = ref.getChild(1).getChild(0).getChild(1).getChild(0); @@ -4031,25 +4055,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHpnameIS NULL", sfq.getChild(2).getText()); + assertEquals("WHICH pname IS NULL", sfq.getChild(3).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(2); - // 2 children: WHICH, pov, + final ParseTree filter = sfq.getChild(3); + // 2 children: WHICH , pov, assertEquals(2, filter.getChildCount()); - assertEquals("WHICH", filter.getChild(0).getText()); - assertEquals("pnameIS NULL", filter.getChild(1).getText()); + assertEquals("WHICH ", filter.getChild(0).getText()); + assertEquals("pname IS NULL", filter.getChild(1).getText()); final ParseTree conjunction = filter.getChild(1).getChild(0); // 2 children: pname, IS_NOT_NULL assertEquals(2, conjunction.getChildCount()); - assertEquals("pname", conjunction.getChild(0).getText()); + assertEquals("pname ", conjunction.getChild(0).getText()); assertEquals("IS NULL", conjunction.getChild(1).getText()); assertEquals("POV(pname,0,null)", sfq.filter.toString()); } @@ -4066,25 +4090,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHpnameIS NOT NULL", sfq.getChild(2).getText()); + assertEquals("WHICH pname IS NOT NULL", sfq.getChild(3).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); - final ParseTree filter = sfq.getChild(2); - // 2 children: WHICH, pov, + final ParseTree filter = sfq.getChild(3); + // 2 children: WHICH , pov, assertEquals(2, filter.getChildCount()); - assertEquals("WHICH", filter.getChild(0).getText()); - assertEquals("pnameIS NOT NULL", filter.getChild(1).getText()); + assertEquals("WHICH ", filter.getChild(0).getText()); + assertEquals("pname IS NOT NULL", filter.getChild(1).getText()); final ParseTree conjunction = filter.getChild(1).getChild(0); // 2 children: pname, IS_NOT_NULL assertEquals(2, conjunction.getChildCount()); - assertEquals("pname", conjunction.getChild(0).getText()); + assertEquals("pname ", conjunction.getChild(0).getText()); assertEquals("IS NOT NULL", conjunction.getChild(1).getText()); assertEquals("POV(pname,!0,null)", sfq.filter.toString()); } @@ -4101,12 +4125,6 @@ public class TestCQL { final String on, final String date) { - if (neg && !sugar.contains("N'T")) { - sugar = sugar.replaceFirst("\\s", ""); - } - if (someone_else != null) { - someone_else = someone_else.replace(" ", ""); - } CQLLexer lexer; lexer = new CQLLexer(CharStreams.fromString(query)); final CommonTokenStream tokens = new CommonTokenStream(lexer); @@ -4114,20 +4132,22 @@ public class TestCQL { final CQLParser parser = new CQLParser(tokens); final CqContext sfq = parser.cq(); - // 4 children: FIND, ename, FILTER, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, FILTER, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals( - "WHICH" + "WHICH " + sugar + + " " + transaction + + " " + (by != null ? by : "") - + (someone_else != null ? someone_else : "") - + (but != null ? but : "") - + (person != null ? person : "") - + (on != null ? on + date : ""), - sfq.getChild(2).getText()); + + (someone_else != null ? " " + someone_else : "") + + (but != null ? " " + but : "") + + (person != null ? " " + person : "") + + (on != null ? " " + on + " " + date : ""), + sfq.getChild(3).getText()); } @Test @@ -4295,26 +4315,27 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); // 4 children: FIND, entity, filter, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("RECORD", sfq.getChild(1).getText()); - assertEquals(".CREATEDBY'henrik.tomwoerden'ANDTHEGREATESTID", sfq.getChild(2).getText()); + assertEquals(".CREATED BY 'henrik.tomwoerden' AND THE GREATEST ID", sfq.getChild(2).getText()); assertNull(sfq.e); + assertEquals(4, sfq.getChildCount()); final ParseTree filter = sfq.getChild(2); - // 2 children: WHICH, conjunction, + // 2 children: WHICH , conjunction, assertEquals(2, filter.getChildCount()); assertEquals(".", filter.getChild(0).getText()); - assertEquals("CREATEDBY'henrik.tomwoerden'ANDTHEGREATESTID", filter.getChild(1).getText()); + assertEquals( + "CREATED BY 'henrik.tomwoerden' AND THE GREATEST ID", filter.getChild(1).getText()); final ParseTree conjunction = filter.getChild(1); - // 3 children: transaction, AND, id_filter - assertEquals(3, conjunction.getChildCount()); - assertEquals("CREATEDBY'henrik.tomwoerden'", conjunction.getChild(0).getText()); - assertEquals("AND", conjunction.getChild(1).getText()); - assertEquals("THEGREATESTID", conjunction.getChild(2).getText()); + // 4 children: transaction, WHITE_SPACE, AND, id_filter + assertEquals(4, conjunction.getChildCount()); + assertEquals("CREATED BY 'henrik.tomwoerden'", conjunction.getChild(0).getText()); + assertEquals("AND ", conjunction.getChild(2).getText()); + assertEquals("THE GREATEST ID", conjunction.getChild(3).getText()); } /** String query31 = "FIND PROPERTIES WHICH ARE INSERTED TODAY"; */ @@ -4331,22 +4352,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("PROPERTIES", sfq.getChild(1).getText()); - assertEquals("WHICHWEREINSERTEDTODAY", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("PROPERTIES ", sfq.getChild(1).getText()); + assertEquals("WHICH WERE INSERTED TODAY", sfq.getChild(2).getText()); assertEquals(null, sfq.e); assertEquals(Query.Role.PROPERTY, sfq.r); assertEquals("TransactionFilter", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, transaction + // 2 children: WHICH , transaction assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICHWERE", whichclause.getChild(0).getText()); - assertEquals("INSERTEDTODAY", whichclause.getChild(1).getText()); + assertEquals("WHICH WERE ", whichclause.getChild(0).getText()); + assertEquals("INSERTED TODAY", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("INSERTED", transactionFilter.getChild(0).getText()); + assertEquals("INSERTED ", transactionFilter.getChild(0).getText()); assertEquals("TODAY", transactionFilter.getChild(1).getText()); } @@ -4365,7 +4386,7 @@ public class TestCQL { // 4 children: FIND, EMPTY_SPACE, entity, entity_filter assertEquals(4, sfq.getChildCount()); - assertEquals("COUNT", sfq.getChild(0).getText()); + assertEquals("COUNT ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); assertEquals(Pattern.TYPE_NORMAL, sfq.e.type); @@ -4418,16 +4439,16 @@ public class TestCQL { // 4 children: FIND, EMPTY_SPACE, entity, entity_filter assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("Entity", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("Entity ", sfq.getChild(1).getText()); // entity_filter - assertEquals(".pname=2.0", sfq.getChild(2).getText()); + assertEquals(". pname=2.0", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals(".", entity_filter.getChild(0).getText()); + assertEquals(". ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname=2.0", entity_filter.getChild(1).getText()); @@ -4472,35 +4493,36 @@ public class TestCQL { // 4 children: FIND, RECORD, entity_filter, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("Record", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("Record ", sfq.getChild(1).getText()); // entity_filter assertEquals( - "WHICHIS REFERENCEDBYannotationWITHcomment='blablabla'", sfq.getChild(2).getText()); + "WHICH IS REFERENCED BY annotation WITH comment='blablabla'", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, filter assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); // filter assertEquals( - "IS REFERENCEDBYannotationWITHcomment='blablabla'", entity_filter.getChild(1).getText()); + "IS REFERENCED BY annotation WITH comment='blablabla'", + entity_filter.getChild(1).getText()); final ParseTree filter = entity_filter.getChild(1); // 2 children: backref, subproperty assertEquals(2, filter.getChildCount()); - assertEquals("IS REFERENCEDBYannotation", filter.getChild(0).getText()); - assertEquals("WITHcomment='blablabla'", filter.getChild(1).getText()); + assertEquals("IS REFERENCED BY annotation ", filter.getChild(0).getText()); + assertEquals("WITH comment='blablabla'", filter.getChild(1).getText()); // backref final ParseTree backref = filter.getChild(0); // 3 children: IS_REFERENCED, BY, entity - assertEquals(3, backref.getChildCount()); - assertEquals("IS REFERENCED", backref.getChild(0).getText()); - assertEquals("BY", backref.getChild(1).getText()); + assertEquals(4, backref.getChildCount()); + assertEquals("IS REFERENCED ", backref.getChild(0).getText()); + assertEquals("BY ", backref.getChild(1).getText()); assertEquals("annotation", backref.getChild(2).getText()); // subproperty @@ -4508,14 +4530,14 @@ public class TestCQL { // 1 children: subquery assertEquals(1, subp.getChildCount()); - assertEquals("WITHcomment='blablabla'", subp.getChild(0).getText()); + assertEquals("WITH comment='blablabla'", subp.getChild(0).getText()); // subquery final ParseTree subquery = subp.getChild(0); // 2 children: WHICH_EXP filter assertEquals(2, subquery.getChildCount()); - assertEquals("WITH", subquery.getChild(0).getText()); + assertEquals("WITH ", subquery.getChild(0).getText()); assertEquals("comment='blablabla'", subquery.getChild(1).getText()); assertEquals(Query.Role.RECORD, sfq.r); @@ -4544,16 +4566,16 @@ public class TestCQL { // 4 children: FIND, EMPTY_SPACE, entity, entity_filter assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("Entity", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("Entity ", sfq.getChild(1).getText()); // entity_filter - assertEquals("WITHpname=-1", sfq.getChild(2).getText()); + assertEquals("WITH pname=-1", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WITH", entity_filter.getChild(0).getText()); + assertEquals("WITH ", entity_filter.getChild(0).getText()); // conjunction assertEquals("pname=-1", entity_filter.getChild(1).getText()); @@ -4597,32 +4619,32 @@ public class TestCQL { // 4 children: FIND, EMPTY_SPACE, entity, entity_filter assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("RECORD", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("RECORD ", sfq.getChild(1).getText()); // entity_filter - assertEquals("WHICH->10594", sfq.getChild(2).getText()); + assertEquals("WHICH -> 10594", sfq.getChild(2).getText()); final ParseTree entity_filter = sfq.getChild(2); // 2 children: WHICH_EXP, conjunction assertEquals(2, entity_filter.getChildCount()); - assertEquals("WHICH", entity_filter.getChild(0).getText()); + assertEquals("WHICH ", entity_filter.getChild(0).getText()); // conjunction - assertEquals("->10594", entity_filter.getChild(1).getText()); + assertEquals("-> 10594", entity_filter.getChild(1).getText()); final ParseTree conjunction = entity_filter.getChild(1); // 1 child: pov assertEquals(1, conjunction.getChildCount()); - assertEquals("->10594", conjunction.getChild(0).getText()); + assertEquals("-> 10594", conjunction.getChild(0).getText()); // pov final ParseTree pov1 = conjunction.getChild(0); - // 2 children: (no property), operator, value - assertEquals(2, pov1.getChildCount()); + // 3 children: (no property), operator, WHITE_SPACE, value + assertEquals(3, pov1.getChildCount()); assertEquals("->", pov1.getChild(0).getText()); - assertEquals("10594", pov1.getChild(1).getText()); + assertEquals("10594", pov1.getChild(2).getText()); assertEquals(Query.Role.RECORD, sfq.r); assertNotNull(sfq.filter); @@ -4647,25 +4669,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WITHadateIN2015", sfq.getChild(2).getText()); + assertEquals("WITH a date IN 2015", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, transaction + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , transaction assertEquals(2, whichclause.getChildCount()); - assertEquals("WITHa", whichclause.getChild(0).getText()); - assertEquals("dateIN2015", whichclause.getChild(1).getText()); + assertEquals("WITH a ", whichclause.getChild(0).getText()); + assertEquals("date IN 2015", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("date", transactionFilter.getChild(0).getText()); - assertEquals("IN", transactionFilter.getChild(1).getText()); + assertEquals("date ", transactionFilter.getChild(0).getText()); + assertEquals("IN ", transactionFilter.getChild(1).getText()); assertEquals("2015", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -4686,25 +4708,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, role, WHITE_SPACE, WHICHCLAUSE, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WITHadateIN\"2015\"", sfq.getChild(2).getText()); + assertEquals("WITH a date IN \"2015\"", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children; WHICH, transaction + final ParseTree whichclause = sfq.getChild(3); + // 2 children; WHICH , transaction assertEquals(2, whichclause.getChildCount()); - assertEquals("WITHa", whichclause.getChild(0).getText()); - assertEquals("dateIN\"2015\"", whichclause.getChild(1).getText()); + assertEquals("WITH a ", whichclause.getChild(0).getText()); + assertEquals("date IN \"2015\"", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("date", transactionFilter.getChild(0).getText()); - assertEquals("IN", transactionFilter.getChild(1).getText()); + assertEquals("date ", transactionFilter.getChild(0).getText()); + assertEquals("IN ", transactionFilter.getChild(1).getText()); assertEquals("\"2015\"", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -4725,26 +4747,26 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WITHadateNOTIN2015", sfq.getChild(2).getText()); + assertEquals("WITH a date NOT IN 2015", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WITHa", whichclause.getChild(0).getText()); - assertEquals("dateNOTIN2015", whichclause.getChild(1).getText()); + assertEquals("WITH a ", whichclause.getChild(0).getText()); + assertEquals("date NOT IN 2015", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(4, transactionFilter.getChildCount()); - assertEquals("date", transactionFilter.getChild(0).getText()); - assertEquals("NOT", transactionFilter.getChild(1).getText()); - assertEquals("IN", transactionFilter.getChild(2).getText()); + assertEquals("date ", transactionFilter.getChild(0).getText()); + assertEquals("NOT ", transactionFilter.getChild(1).getText()); + assertEquals("IN ", transactionFilter.getChild(2).getText()); assertEquals("2015", transactionFilter.getChild(3).getText()); assertTrue(sfq.filter instanceof POV); @@ -4765,26 +4787,26 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, role, WHITE_SPACE, WHICHCLAUSE, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WITHadateNOTIN\"2015\"", sfq.getChild(2).getText()); + assertEquals("WITH a date NOT IN \"2015\"", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WITHa", whichclause.getChild(0).getText()); - assertEquals("dateNOTIN\"2015\"", whichclause.getChild(1).getText()); + assertEquals("WITH a ", whichclause.getChild(0).getText()); + assertEquals("date NOT IN \"2015\"", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(4, transactionFilter.getChildCount()); - assertEquals("date", transactionFilter.getChild(0).getText()); - assertEquals("NOT", transactionFilter.getChild(1).getText()); - assertEquals("IN", transactionFilter.getChild(2).getText()); + assertEquals("date ", transactionFilter.getChild(0).getText()); + assertEquals("NOT ", transactionFilter.getChild(1).getText()); + assertEquals("IN ", transactionFilter.getChild(2).getText()); assertEquals("\"2015\"", transactionFilter.getChild(3).getText()); assertTrue(sfq.filter instanceof POV); @@ -4805,25 +4827,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WITHadateIN2015-01-01", sfq.getChild(2).getText()); + assertEquals("WITH a date IN 2015-01-01", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WITHa", whichclause.getChild(0).getText()); - assertEquals("dateIN2015-01-01", whichclause.getChild(1).getText()); + assertEquals("WITH a ", whichclause.getChild(0).getText()); + assertEquals("date IN 2015-01-01", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("date", transactionFilter.getChild(0).getText()); - assertEquals("IN", transactionFilter.getChild(1).getText()); + assertEquals("date ", transactionFilter.getChild(0).getText()); + assertEquals("IN ", transactionFilter.getChild(1).getText()); assertEquals("2015-01-01", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -4844,25 +4866,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, role, WHITE_SPACE, WHICHCLAUSE, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WITHadateIN\"2015-01-01\"", sfq.getChild(2).getText()); + assertEquals("WITH a date IN \"2015-01-01\"", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WITHa", whichclause.getChild(0).getText()); - assertEquals("dateIN\"2015-01-01\"", whichclause.getChild(1).getText()); + assertEquals("WITH a ", whichclause.getChild(0).getText()); + assertEquals("date IN \"2015-01-01\"", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("date", transactionFilter.getChild(0).getText()); - assertEquals("IN", transactionFilter.getChild(1).getText()); + assertEquals("date ", transactionFilter.getChild(0).getText()); + assertEquals("IN ", transactionFilter.getChild(1).getText()); assertEquals("\"2015-01-01\"", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -4885,23 +4907,23 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals(".pnameLIKE\"wil*card\"", sfq.getChild(2).getText()); + assertEquals(".pname LIKE \"wil*card\"", sfq.getChild(2).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); assertEquals(".", whichclause.getChild(0).getText()); - assertEquals("pnameLIKE\"wil*card\"", whichclause.getChild(1).getText()); + assertEquals("pname LIKE \"wil*card\"", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("pname", transactionFilter.getChild(0).getText()); - assertEquals("LIKE", transactionFilter.getChild(1).getText()); + assertEquals("pname ", transactionFilter.getChild(0).getText()); + assertEquals("LIKE ", transactionFilter.getChild(1).getText()); assertEquals("\"wil*card\"", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -4924,23 +4946,23 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals(".pnameLIKEwil*card", sfq.getChild(2).getText()); + assertEquals(".pname LIKE wil*card", sfq.getChild(2).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); assertEquals(".", whichclause.getChild(0).getText()); - assertEquals("pnameLIKEwil*card", whichclause.getChild(1).getText()); + assertEquals("pname LIKE wil*card", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("pname", transactionFilter.getChild(0).getText()); - assertEquals("LIKE", transactionFilter.getChild(1).getText()); + assertEquals("pname ", transactionFilter.getChild(0).getText()); + assertEquals("LIKE ", transactionFilter.getChild(1).getText()); assertEquals("wil*card", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -4961,25 +4983,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, ename, WHITE_SPACE, WHICHCLAUSE, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHHAS ApnameLIKE\"wil*\"", sfq.getChild(2).getText()); + assertEquals("WHICH HAS A pname LIKE \"wil*\"", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICHHAS A", whichclause.getChild(0).getText()); - assertEquals("pnameLIKE\"wil*\"", whichclause.getChild(1).getText()); + assertEquals("WHICH HAS A ", whichclause.getChild(0).getText()); + assertEquals("pname LIKE \"wil*\"", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("pname", transactionFilter.getChild(0).getText()); - assertEquals("LIKE", transactionFilter.getChild(1).getText()); + assertEquals("pname ", transactionFilter.getChild(0).getText()); + assertEquals("LIKE ", transactionFilter.getChild(1).getText()); assertEquals("\"wil*\"", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -5000,25 +5022,25 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHHAS ApnameLIKEwil*", sfq.getChild(2).getText()); + assertEquals("WHICH HAS A pname LIKE wil*", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICHHAS A", whichclause.getChild(0).getText()); - assertEquals("pnameLIKEwil*", whichclause.getChild(1).getText()); + assertEquals("WHICH HAS A ", whichclause.getChild(0).getText()); + assertEquals("pname LIKE wil*", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(3, transactionFilter.getChildCount()); - assertEquals("pname", transactionFilter.getChild(0).getText()); - assertEquals("LIKE", transactionFilter.getChild(1).getText()); + assertEquals("pname ", transactionFilter.getChild(0).getText()); + assertEquals("LIKE ", transactionFilter.getChild(1).getText()); assertEquals("wil*", transactionFilter.getChild(2).getText()); assertTrue(sfq.filter instanceof POV); @@ -5039,26 +5061,26 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - // 4 children: FIND, role, WHICHCLAUSE, EOF - assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); + // 5 children: FIND, entity, WHITE_SPACE, entity_filter, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals("FIND ", sfq.getChild(0).getText()); assertEquals("ename", sfq.getChild(1).getText()); - assertEquals("WHICHHAS Apname=wil*", sfq.getChild(2).getText()); + assertEquals("WHICH HAS A pname = wil*", sfq.getChild(3).getText()); assertEquals("ename", sfq.e.toString()); assertNull(sfq.r); assertEquals("POV", sfq.filter.getClass().getSimpleName()); - final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + final ParseTree whichclause = sfq.getChild(3); + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICHHAS A", whichclause.getChild(0).getText()); - assertEquals("pname=wil*", whichclause.getChild(1).getText()); + assertEquals("WHICH HAS A ", whichclause.getChild(0).getText()); + assertEquals("pname = wil*", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); - assertEquals(3, transactionFilter.getChildCount()); - assertEquals("pname", transactionFilter.getChild(0).getText()); + assertEquals(4, transactionFilter.getChildCount()); + assertEquals("pname ", transactionFilter.getChild(0).getText()); assertEquals("=", transactionFilter.getChild(1).getText()); - assertEquals("wil*", transactionFilter.getChild(2).getText()); + assertEquals("wil*", transactionFilter.getChild(3).getText()); assertTrue(sfq.filter instanceof POV); final POV pov = (POV) sfq.filter; @@ -5080,22 +5102,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/data/bla.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /data/bla.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/data/bla.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /data/bla.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/data/bla.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5117,22 +5139,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/*", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /*", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/*", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /*", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/*", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5155,22 +5177,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/*/", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /*/", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/*/", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /*/", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/*/", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5193,22 +5215,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/**/", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /**/", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/**/", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /**/", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/**/", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5230,22 +5252,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/**/*", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /**/*", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/**/*", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /**/*", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/**/*", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5267,22 +5289,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/*/*", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /*/*", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/*/*", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /*/*", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/*/*", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5304,22 +5326,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/*/*.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /*/*.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/*/*.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /*/*.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/*/*.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5341,22 +5363,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/**/*.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /**/*.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/**/*.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /**/*.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/**/*.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5378,22 +5400,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/*.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /*.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/*.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /*.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/*.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5415,22 +5437,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT*.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT *.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT*.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT *.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("*.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5452,22 +5474,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT*", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT *", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT*", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT *", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("*", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5597,22 +5619,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT*%.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT *%.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT*%.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT *%.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("*%.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5634,22 +5656,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT*\\*.acq", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT *\\*.acq", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT*\\*.acq", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT *\\*.acq", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("*\\*.acq", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -5685,8 +5707,8 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - assertTrue(sfq.filter instanceof TransactionFilter); assertEquals("TRANS(Insert,null,Transactor(some.user,=))", sfq.filter.toString()); + assertTrue(sfq.filter instanceof TransactionFilter); } /** String ticket262a = "COUNT FILE which is not referenced"; */ @@ -5871,6 +5893,21 @@ public class TestCQL { final CqContext sfq = parser.cq(); System.out.println(sfq.toStringTree(parser)); + assertEquals(4, sfq.getChildCount()); + assertEquals( + "WHICH IS NOT REFERENCED BY AN entity WHICH WAS created by me", sfq.getChild(2).getText()); + + ParseTree filter = sfq.getChild(2).getChild(1); + assertEquals(1, filter.getChildCount()); + ParseTree negation = filter.getChild(0); + assertEquals(2, negation.getChildCount()); + assertEquals("NOT ", negation.getChild(0).getText()); + assertEquals("REFERENCED BY AN entity WHICH WAS created by me", negation.getChild(1).getText()); + ParseTree refereced = negation.getChild(1).getChild(0); + assertEquals("REFERENCED ", refereced.getChild(0).getText()); + assertEquals("BY ", refereced.getChild(1).getText()); + assertEquals("AN ", refereced.getChild(2).getText()); + assertEquals("entity ", refereced.getChild(3).getText()); assertTrue(sfq.filter instanceof Negation); final EntityFilterInterface backref = ((Negation) sfq.filter).getFilter(); @@ -6124,22 +6161,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/dir/with/date/2016-05-15", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /dir/with/date/2016-05-15", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/dir/with/date/2016-05-15", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /dir/with/date/2016-05-15", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/dir/with/date/2016-05-15", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -6159,22 +6196,22 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); - assertEquals("WHICHIS STORED AT/dir/with/date/2016-05-15/**", sfq.getChild(2).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); + assertEquals("WHICH IS STORED AT /dir/with/date/2016-05-15/**", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); - assertEquals("IS STORED AT/dir/with/date/2016-05-15/**", whichclause.getChild(1).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); + assertEquals("IS STORED AT /dir/with/date/2016-05-15/**", whichclause.getChild(1).getText()); final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); assertEquals(2, transactionFilter.getChildCount()); - assertEquals("IS STORED AT", transactionFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", transactionFilter.getChild(0).getText()); assertEquals("/dir/with/date/2016-05-15/**", transactionFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -6317,25 +6354,26 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); assertEquals( - "WHICHIS STORED AT'/SimulationData/2016_single/2018-01-10/**'", sfq.getChild(2).getText()); + "WHICH IS STORED AT '/SimulationData/2016_single/2018-01-10/**'", + sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); assertEquals( - "IS STORED AT'/SimulationData/2016_single/2018-01-10/**'", + "IS STORED AT '/SimulationData/2016_single/2018-01-10/**'", whichclause.getChild(1).getText()); final ParseTree satFilter = whichclause.getChild(1).getChild(0); assertEquals(2, satFilter.getChildCount()); - assertEquals("IS STORED AT", satFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", satFilter.getChild(0).getText()); assertEquals("'/SimulationData/2016_single/2018-01-10/**'", satFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -6354,24 +6392,25 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("FIND", sfq.getChild(0).getText()); - assertEquals("FILE", sfq.getChild(1).getText()); + assertEquals("FIND ", sfq.getChild(0).getText()); + assertEquals("FILE ", sfq.getChild(1).getText()); assertEquals( - "WHICHIS STORED AT/SimulationData/2016_single/2018-01-10/**", sfq.getChild(2).getText()); + "WHICH IS STORED AT /SimulationData/2016_single/2018-01-10/**", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); final ParseTree whichclause = sfq.getChild(2); - // 2 children: WHICH, POV + // 2 children: WHICH , POV assertEquals(2, whichclause.getChildCount()); - assertEquals("WHICH", whichclause.getChild(0).getText()); + assertEquals("WHICH ", whichclause.getChild(0).getText()); assertEquals( - "IS STORED AT/SimulationData/2016_single/2018-01-10/**", whichclause.getChild(1).getText()); + "IS STORED AT /SimulationData/2016_single/2018-01-10/**", + whichclause.getChild(1).getText()); final ParseTree satFilter = whichclause.getChild(1).getChild(0); assertEquals(2, satFilter.getChildCount()); - assertEquals("IS STORED AT", satFilter.getChild(0).getText()); + assertEquals("IS STORED AT ", satFilter.getChild(0).getText()); assertEquals("/SimulationData/2016_single/2018-01-10/**", satFilter.getChild(1).getText()); assertTrue(sfq.filter instanceof StoredAt); @@ -6393,8 +6432,7 @@ public class TestCQL { } /** String queryIssue31 = "FIND FILE WHICH IS STORED AT /data/in0.foo"; */ - // FIXME Remove "expected" annotation. - @Test(expected = AssertionError.class) + @Test public void testIssue31() { CQLLexer lexer; lexer = new CQLLexer(CharStreams.fromString(this.queryIssue31)); @@ -6407,7 +6445,7 @@ public class TestCQL { // 4 children: FIND, role, WHICHCLAUSE, EOF assertEquals(4, sfq.getChildCount()); - assertEquals("WHICHIS STORED AT/data/in0.foo", sfq.getChild(2).getText()); + assertEquals("WHICH IS STORED AT /data/in0.foo", sfq.getChild(2).getText()); assertEquals("FILE", sfq.r.toString()); assertNull(sfq.e); assertEquals("StoredAt", sfq.filter.getClass().getSimpleName()); @@ -6417,4 +6455,235 @@ public class TestCQL { final StoredAt storedAt = (StoredAt) sfq.filter; assertEquals("SAT(/data/in0.foo)", storedAt.toString()); } + + /** + * Testing a conjunction which begins with a nested disjunction. The bug was a parsing error which + * was caused by a missing option for a disjunction/conjunction, wrapped into parenthesis as first + * element of a conjunction/disjunction + * + * <p>String queryMR56 = "FIND ENTITY WITH ((p0 = v0 OR p1=v1) AND p2=v2)"; + */ + @Test + public void testMR56() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.queryMR56)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 4 children: FIND, role, WHICHCLAUSE, EOF + assertEquals(4, sfq.getChildCount()); + assertEquals("WITH ((p0 = v0 OR p1=v1) AND p2=v2)", sfq.getChild(2).getText()); + assertEquals("ENTITY", sfq.r.toString()); + assertEquals("Conjunction", sfq.filter.getClass().getSimpleName()); + final ParseTree whichclause = sfq.getChild(2); + final ParseTree conjunction = whichclause.getChild(2); + assertEquals("(p0 = v0 OR p1=v1) AND p2=v2", conjunction.getText()); + final ParseTree disjunction = conjunction.getChild(1); + assertEquals("p0 = v0 OR p1=v1", disjunction.getText()); + final ParseTree pov = conjunction.getChild(4); + assertEquals("p2=v2", pov.getText()); + } + + /** String versionedQuery1 = "FIND ANY VERSION OF ENTITY e1"; */ + @Test + public void testVersionedQuery1() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.versionedQuery1)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 5 children: FIND, version, role, entity, EOF + assertEquals(5, sfq.getChildCount()); + assertEquals(VersionFilter.ANY_VERSION, sfq.v); + assertEquals(Query.Role.ENTITY, sfq.r); + assertEquals("e1", sfq.e.toString()); + } + + /** String query57a = "FIND ENTITY"; */ + @Test + public void testQuery57a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query57a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 3 children: FIND, ENTITY, EOF + assertEquals(3, sfq.getChildCount()); + assertEquals(Query.Role.ENTITY, sfq.r); + assertNull(sfq.e); + } + + /** String query57b = "FIND ENTITY WITH ID"; */ + @Test + public void testQuery57b() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query57b)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 4 children: FIND, ENTITY, WHICHCLAUSE, EOF + assertEquals(4, sfq.getChildCount()); + assertEquals(Query.Role.ENTITY, sfq.r); + assertNull(sfq.e); + assertEquals("WITH ID", sfq.getChild(2).getText()); + } + + /** String query57c = "FIND ENTITY WITH ID = 123"; */ + @Test + public void testQuery57c() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query57c)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 4 children: FIND, ENTITY, WHICHCLAUSE, EOF + assertEquals(4, sfq.getChildCount()); + assertEquals(Query.Role.ENTITY, sfq.r); + assertNull(sfq.e); + assertEquals("WITH ID = 123", sfq.getChild(2).getText()); + } + + /** String queryIssue116 = "FIND *"; */ + @Test + public void testIssue116() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.queryIssue116)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 3 children: FIND, ENTITY, EOF + assertEquals(3, sfq.getChildCount()); + assertNull(sfq.r); + assertTrue(sfq.e.type == Query.Pattern.TYPE_LIKE); + assertEquals("%", sfq.e.toString()); + } + + /** String query54e = "SELECT id FROM ename"; */ + @Test + public void testQuery54e() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query54e)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals(5, sfq.getChildCount()); + assertEquals("SELECT ", sfq.getChild(0).getText()); + assertEquals("id ", sfq.getChild(1).getText()); + assertEquals("FROM ", sfq.getChild(2).getText()); + assertEquals("ename", sfq.getChild(3).getText()); + assertEquals(Query.Pattern.TYPE_NORMAL, sfq.e.type); + assertEquals("ename", sfq.e.toString()); + assertNull(sfq.r); + assertEquals(Query.Type.FIND, sfq.t); + assertNotNull(sfq.s); + assertFalse(sfq.s.isEmpty()); + assertEquals(1, sfq.s.size()); + assertEquals("id", sfq.s.get(0).toString()); + } + + /** String query58a = "FIND ENTITY WITH endswith"; */ + @Test + public void testQuery58a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query58a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("POV(endswith,null,null)", sfq.filter.toString()); + } + + /** String query58b = "FIND ENTITY WITH endswith = val1"; */ + @Test + public void testQuery58b() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query58b)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("POV(endswith,=,val1)", sfq.filter.toString()); + } + + /** String query58c = "FIND ENTITY WITH 0with = val1"; */ + @Test + public void testQuery58c() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query58c)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertNull(((POV) sfq.filter).getSubProperty()); + assertEquals("POV(0with,=,val1)", sfq.filter.toString()); + } + + /** String query58d = "FIND ENTITY WITH WITH"; */ + @Test + public void testQuery58d() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query58d)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("POV(withdrawn,=,TRUE)", sfq.filter.toString()); + assertNull(((POV) sfq.filter).getSubProperty()); + } + + /** String query58e = "FIND ENTITY WITH pname=with"; */ + @Test + public void testQuery58e() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query58e)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("POV(pname,=,with)", sfq.filter.toString()); + assertNull(((POV) sfq.filter).getSubProperty()); + } } diff --git a/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java b/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java index 1dcf71d8f330ab6807e65bd23022138e941fca1c..5d81ca738d507bcfd75dbe137756135cede3989b 100644 --- a/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java +++ b/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java @@ -26,6 +26,7 @@ import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.TransactionBenchmark; import org.caosdb.server.permissions.PermissionRule; +import org.caosdb.server.utils.WebinterfaceUtils; import org.jdom2.Element; import org.junit.BeforeClass; import org.junit.Rule; @@ -125,6 +126,11 @@ public class TestAbstractCaosDBServerResource { // TODO Auto-generated method stub return user; } + + @Override + public WebinterfaceUtils getUtils() { + return WebinterfaceUtils.getInstance(getRootRef().toString()); + } }; provideUserSourcesFile(); Element response = s.generateRootElement(); diff --git a/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java b/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java index 28fdd55e78db1a2142b5eac86327617b8f371089..61be0c5c3a06319dbeb826a79ddf1c6e43a0d672 100644 --- a/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java +++ b/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java @@ -39,6 +39,7 @@ import org.apache.commons.io.FileUtils; import org.caosdb.CaosDBTestClass; import org.caosdb.server.CaosDBException; import org.caosdb.server.CaosDBServer; +import org.caosdb.server.ServerProperties; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.FileProperties; import org.caosdb.server.entity.Message; @@ -68,6 +69,10 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { @BeforeClass public static void setupTestFolder() throws IOException { + CaosDBServer.getServerProperties() + .setProperty( + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS, testFolder.getAbsolutePath()); + FileUtils.forceDeleteOnExit(testFolder); FileUtils.forceDeleteOnExit(testFile); FileUtils.forceDeleteOnExit(testExecutable); @@ -305,7 +310,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testExeExit(1); - assertEquals(1, caller.callScript()); + assertEquals(1, caller.callScript(cmd[0], cmd, null, emptyEnv)); } @Test @@ -319,7 +324,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testExeExit(0); - assertEquals(0, caller.callScript()); + assertEquals(0, caller.callScript(cmd[0], cmd, null, emptyEnv)); } @Test @@ -387,7 +392,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testSleep(10); this.exception.expect(TimeoutException.class); - caller.callScript(); + caller.callScript(cmd[0], cmd, null, emptyEnv); } @Test @@ -418,7 +423,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { throws FileNotFoundException, CaosDBException {} @Override - public int callScript() { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) { return 0; } @@ -456,7 +462,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { } @Override - public int callScript() { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) { return 0; } @@ -496,7 +503,9 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { public void createTmpHomeDir() throws Exception {} @Override - public int callScript() throws IOException { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) + throws IOException { throw new IOException(); } @@ -532,7 +541,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { throws FileNotFoundException, CaosDBException {} @Override - public int callScript() { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) { return 0; } @@ -562,7 +572,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testPrintArgsToStdErr(); - caller.callScript(); + caller.callScript(cmd[0], cmd, "authToken", emptyEnv); assertEquals( "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdErrFile())); @@ -579,7 +589,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testPrintArgsToStdOut(); - caller.callScript(); + caller.callScript(cmd[0], cmd, "authToken", emptyEnv); assertEquals( "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdOutFile())); @@ -599,7 +609,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { preparePrintEnv("TEST"); - caller.callScript(); + caller.callScript(cmd[0], cmd, null, env); assertEquals("-testcontent-\n", caller.getStdOut()); caller.cleanup(); diff --git a/src/test/java/org/caosdb/server/transaction/UpdateTest.java b/src/test/java/org/caosdb/server/transaction/UpdateTest.java index 6b0e7b3f988f18142c6ebdb535212cd7454233f0..d22fbea8982b1f8d88c9ccdf069a0c8a816add25 100644 --- a/src/test/java/org/caosdb/server/transaction/UpdateTest.java +++ b/src/test/java/org/caosdb/server/transaction/UpdateTest.java @@ -44,7 +44,7 @@ public class UpdateTest { throws NoSuchAlgorithmException, IOException, CaosDBException { final Entity newEntity = new Entity("Name"); final Entity oldEntity = new Entity("Name"); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), EntityStatus.VALID); } @@ -53,7 +53,7 @@ public class UpdateTest { throws NoSuchAlgorithmException, IOException, CaosDBException { final Entity newEntity = new Entity("NewName"); final Entity oldEntity = new Entity("OldName"); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), EntityStatus.QUALIFIED); } @@ -67,7 +67,7 @@ public class UpdateTest { final Entity oldEntity = new Entity(); oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), VALID); } @@ -83,7 +83,7 @@ public class UpdateTest { final Entity oldEntity = new Entity(); oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), QUALIFIED); assertEquals(newProperty.getEntityStatus(), VALID); assertEquals(newProperty2.getEntityStatus(), QUALIFIED); @@ -124,7 +124,7 @@ public class UpdateTest { oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newUnit.getEntityStatus(), VALID); assertEquals(newProperty.getEntityStatus(), VALID); assertEquals(newEntity.getEntityStatus(), VALID); @@ -165,7 +165,7 @@ public class UpdateTest { oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), QUALIFIED); assertEquals(newProperty.getEntityStatus(), QUALIFIED); }