diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91c3a12920a4750a261bde5642800bbccfa37f31..d3331a3b24145747836c0898ff852a8ade438bb4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,14 +42,11 @@ build-testenv: - schedules script: - cd src/test/docker - - time docker load < /image-cache/caosdb-server-testenv.tar || true - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY # use here general latest or specific branch latest... - docker build --pull -t $CI_REGISTRY_IMAGE . - - docker save $CI_REGISTRY_IMAGE > image.tar; - mv image.tar /image-cache/caosdb-server-testenv.tar; - docker push $CI_REGISTRY_IMAGE # Test: run unit tests of the server @@ -79,20 +76,21 @@ trigger_build: # 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: +pages_prepare: &pages_prepare 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" + - echo "Deploying..." - make doc - cp -r build/doc/html public artifacts: paths: - public +pages: + <<: *pages_prepare + only: + refs: + - main diff --git a/CHANGELOG.md b/CHANGELOG.md index 305dd69b00b84c30b5caa91dec9f97b6beaa4b92..df5b85f849385868b6eb8e1d7daa605bc1b4e8d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* New EntityState plug-in. The plug-in disabled by default and can be enabled + by setting the server property `EXT_ENTITY_STATE=ENABLED`. See + [!62](https://gitlab.com/caosdb/caosdb-server/-/merge_requests/62) for more + information. * `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). @@ -16,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. +* Documentation for the overall server structure. +* Add `BEFORE`, `AFTER`, `UNTIL`, `SINCE` keywords for query transaction ### Changed @@ -25,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* #131 - CQL Parsing error when white space characters before some units. +* #134 - CQL Parsing error when multiple white space characters after `FROM`. +* #130 - Error during `FIND ENTITY` when + `QUERY_FILTER_ENTITIES_WITHOUT_RETRIEVE_PERMISSIONS=False`. +* #125 - `bend_symlinks` script did not allow whitespace in filename. * #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. @@ -51,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* Server can be started without TLS even when not in debug mode. * 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 diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 3744c7b7d654c5f2379b396c5894f8c054907ee4..1b0ba73efbf05f7c884d4067f5f0cf49694c4e09 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -2,3 +2,4 @@ * Java 11 * Apache Maven >= 3.6.0 * make >= 4.2.0 +- easy-units >= 0.0.1 https://gitlab.com/timm.fitschen/easy-units diff --git a/Makefile b/Makefile index d73f140f065ca0b3db62651e40162194f4ffb4eb..1543286b67c6c7be20772a3b6932b9e0dcec27d5 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ # CAOSDB_SERVER_VERSION ?= $(shell mvn org.apache.maven.plugins:maven-help-plugin:3.1.0:evaluate -Dexpression=project.version -q -DforceStdout) +CAOSDB_COMMAND_LINE_OPTIONS ?= SHELL:=/bin/bash JPDA_PORT ?= 9000 JMX_PORT ?= 9090 @@ -41,13 +42,14 @@ run: compile mvn exec:java@run run-debug: jar - java -Xrunjdwp:transport=dt_socket,address=0.0.0.0:$(JPDA_PORT),server=y,suspend=n -Dcaosdb.debug=true -jar target/caosdb-server.jar + java -Xrunjdwp:transport=dt_socket,address=0.0.0.0:$(JPDA_PORT),server=y,suspend=n -Dcaosdb.debug=true -jar target/caosdb-server.jar $(CAOSDB_COMMAND_LINE_OPTIONS) + run-debug-single: - java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=$(JMX_PORT) -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Xrunjdwp:transport=dt_socket,address=0.0.0.0:$(JPDA_PORT),server=y,suspend=n -Dcaosdb.debug=true -jar target/caosdb-server.jar + java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=$(JMX_PORT) -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Xrunjdwp:transport=dt_socket,address=0.0.0.0:$(JPDA_PORT),server=y,suspend=n -Dcaosdb.debug=true -jar target/caosdb-server.jar $(CAOSDB_COMMAND_LINE_OPTIONS) run-single: - java -jar target/caosdb-server.jar + java -jar target/caosdb-server.jar $(CAOSDB_COMMAND_LINE_OPTIONS) formatting: mvn fmt:format @@ -64,7 +66,7 @@ antlr: mvn antlr4:antlr4 test: print-version easy-units - MAVEN_DEBUG_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE -Dcaosdb.debug=true -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=0.0.0.0:9000" + MAVEN_DEBUG_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE -Dcaosdb.debug=true -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=0.0.0.0:$(JPDA_PORT)" mvn test -X test_misc: diff --git a/README.md b/README.md index 21d5400e5383d4c2571f8a409f50cd72f926187a..62da38e86df18a34deddf7c31a651dd12d81b7e8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ when creating the merge request. This allows our team to work with you on your r - 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**. +- You can also contact us at **info (AT) caosdb.de** and join the + CaosDB community on + [#caosdb:matrix.org](https://matrix.to/#/!unwwlTfOznjEnMMXxf:matrix.org). ## License diff --git a/README_SETUP.md b/README_SETUP.md index f47f5a08624c20de194829b3534d72c2e06b461c..d83ab788738d10924009bccb19707fa246686064 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -21,6 +21,7 @@ Here, you find information on requirements, the installation, configuration and * `unzip` * `openpyxl` (for XLS/ODS export) * `openssl` (if a custom TLS certificate is required) +- `easy-units` >= 0.0.1 https://gitlab.com/timm.fitschen/easy-units #### Install the requirements on Debian On Debian, the required packages can be installed with: @@ -43,7 +44,7 @@ versa. - 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](https://http://caosdb.gitlab.io/caosdb-webui/getting_started.html). + [documentation](https://docs.indiscale.com/caosdb-webui/getting_started.html). #### PAM ### Authentication via PAM is possible, for this the PAM development library must be @@ -79,7 +80,7 @@ server: Replace `localhost` by your host name, if you want. - `keytool -importkeystore -srckeystore caosdb.jks -destkeystore caosdb.p12 -deststoretype PKCS12 -srcalias selfsigned` - 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. + 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: @@ -197,7 +198,11 @@ Stand-alone documentation is built using Sphinx: `make doc` ### Requirements ## +- plantuml +- recommonmark - sphinx +- sphinx-rtd-theme +- sphinxcontrib-plantuml - javasphinx :: `pip3 install --user javasphinx` - Alternative, if javasphinx fails because python3-sphinx is too recent: (`l_` not found): diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index 5d89ceec00bbd8ba228bb497964f0eeb437891f7..5c1fd68d462212e9b81dbc803fc590006df36a3d 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -21,13 +21,13 @@ guidelines of the CaosDB Project 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. +4. Merge the release branch into the main branch. -5. Tag the latest commit of the master branch with `v<VERSION>`. +5. Tag the latest commit of the main branch with `v<VERSION>`. 6. Delete the release branch. -7. Merge the master branch back into the dev branch. +7. Merge the main 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 8c59cc861d646cbdba0ec749ba052656f67fd58d..5dfe879722bd01acc5209c581b60bf0ac49635b6 160000 --- a/caosdb-webui +++ b/caosdb-webui @@ -1 +1 @@ -Subproject commit 8c59cc861d646cbdba0ec749ba052656f67fd58d +Subproject commit 5dfe879722bd01acc5209c581b60bf0ac49635b6 diff --git a/conf/core/server.conf b/conf/core/server.conf index c9151888e785b8114f104fdcd14c43714fc80b37..76ed6030523f24f977f41a510c703fe075f30042 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -188,3 +188,11 @@ GLOBAL_ENTITY_PERMISSIONS_FILE=./conf/core/global_entity_permissions.xml # If set to true, versioning of entities' history is enabled. ENTITY_VERSIONING_ENABLED=true + + +# -------------------------------------------------- +# Extension settings +# -------------------------------------------------- + +# Enabling the state machine extension +# EXT_STATE_ENTITY=ENABLE diff --git a/doc/Query.md b/doc/Query.md index 4ff07fbad5c8c6acffac9d6c57e325ed92c8a9cd..1d7748438cd7f1a4a84f0ab66215564cbc5ad36d 100644 --- a/doc/Query.md +++ b/doc/Query.md @@ -226,14 +226,13 @@ The following query returns entities which have a _pname1_ property with any val ### TransactionFilter *Definition* - sugar:: `HAS BEEN` | `HAVE BEEN` | `HAD BEEN` | `WAS` | `IS` | + 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. + datetime:: A datetime string of the form `YYYY[-MM[-DD(T| )[hh[:mm[:ss[.nnn][(+|-)zzzz]]]]]]` + time_clause:: `[AT|ON|IN|BEFORE|AFTER|UNTIL|SINCE] (datetime) ` -`FIND ename WHICH ($sugar|$negated_sugar)? (NOT)? (CREATED|INSERTED|UPDATED|DELETED) (by_clause time_clause?| time_clause by_clause?)` +`FIND ename WHICH (sugar|negated_sugar)? (NOT)? (CREATED|INSERTED|UPDATED) (by_clause time_clause?| time_clause by_clause?)` *Examples* @@ -247,8 +246,9 @@ The following query returns entities which have a _pname1_ property with any val `FIND ename WHICH HAS BEEN CREATED BY erwin` -`FIND ename . CREATED BY erwin ON ` +`FIND ename WHICH HAS BEEN INSERTED SINCE 2021-04` +Note that `SINCE` and `UNTIL` are inclusive, while `BEFORE` and `AFTER` are not. ### File Location diff --git a/doc/devel/Benchmarking.md b/doc/devel/Benchmarking.md index e07f6f8973784bfe3d77191f6730662d261f73fb..5fc3f75fe01114558f325662048cde904480c910 100644 --- a/doc/devel/Benchmarking.md +++ b/doc/devel/Benchmarking.md @@ -1,18 +1,153 @@ -# Profiling # -If the server is started with the `run-debug-single` make target, it will expose -the JMX interface, by default on port 9090. Using a profiler such as VisualVM, -one can then connect to the CaosDB server and profile execution times. -## Example settings for VisualVM ## +# Benchmarking CaosDB # -In the sampler settings, you may want to add these expressions to the blocked -packages: `org.restlet.**, com.mysql.**`. Branches on the call tree which are -entirely inside the blacklist, will become leaves. Alternatively, specify a -whitelist, for example with `org.caosdb.server.database.backend.implementation.**`, -if you only want to see the time spent for certain MySQL calls. +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 called 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. + + +## Before you start ## +In order to obtain meaningful results, you should disable caching. + +### MariaDB +Set the corresponding variable to 0: `SET GLOBAL query_cache_type = 0;` + +### Java Server +In the config: +```conf +CACHE_DISABLE=true +``` + + +## 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`: + +### Python Script `fill_database.py` ### -# Manual Java-side benchmarking # +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. + +### Python Script `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. + + +### Python Script `sql_routine_measurement.py` + + + +Simply call `./sql_routine_measurement.py` in the scripts directory. An sql +file is automatically executed which enables the correct `performance_schema` +tables. However, the performance_schema of mariadb needs to be enabled. Add +`performance_schema=ON` to the configuration file of mariadb as it needs to be +enabled on start up. +This script expects the MariaDB server to be accessible on 127.0.0.1 with the default caosdb user +and password (caosdb;random1234). + + +The performance schema must be enabled (see below). + +### MariaDB General Query Log ### + +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/): +Add to the mysql configuration: +``` +log_output=TABLE +general_log +``` +or calling +```sql +SET GLOBAL log_output = 'TABLE'; +SET GLOBAL general_log = 'ON'; +``` + +In the Docker environment LinkAhead, this can conveniently be +done with `linkahead mysqllog {on,off,store}`. + +### MariaDB Slow Query Log ### +See [slow query log docs](https://mariadb.com/kb/en/slow-query-log-overview/) + +### MariaDB Performance Schema ### +The most detailed information on execution times can be acquired using the performance schema. + +To use it, the `performance_schema` setting in the MariaDB server must be enabled([docs](https://mariadb.com/kb/en/performance-schema-overview/#enabling-the-performance-schema), for example by setting +this in the config files: +``` +[mysqld] + +performance_schema=ON +``` + +The performance schema provides many different tables in the `performance_schema`. You can instruct MariaDB to create +those tables by setting the appropriate `instrument` and `consumer` variables. E.g. +```SQL +update performance_schema.setup_instruments set enabled='YES', timed='YES' WHERE NAME LIKE '%statement%'; +update performance_schema.setup_consumers set enabled='YES' WHERE NAME LIKE '%statement%'; +``` +This can also be done via the configuration. +``` +[mysqld] + +performance_schema=ON +performance-schema-instrument='statement/%=ON' +performance-schema-consumer-events-statements-history=ON +performance-schema-consumer-events-statements-history-long=ON +``` +You may want to look at the result of the following commands: +```sql + +select * from performance_schema.setup_consumers; +select * from performance_schema.setup_instruments; +``` + +Note, that the `base_settings.sql` enables appropriate instruments and consumers. + +Before you start a measurement, you will want to empty the tables. E.g.: +```sql +truncate table performance_schema.events_statements_history_long ; +``` +The procedure `reset_stats` in `base_settings.sql` clears the typically used ones. + +The tables contain many columns. An example to get an informative view is +```sql +select left(sql_text,50), left(digest_text,50), ms(timer_wait) from performance_schema.events_statements_history_long order by ms(timer_wait); +``` +where the function `ms` is defined in `base_settings.sql`. +Or a very useful one: +```sql +select left(digest_text,100) as digest,ms(sum_timer_wait) as time_ms, count_star from performance_schema.events_statements_summary_by_digest order by time_ms; +``` + +### Useful SQL configuration with docker +In order to allow easy testing and debugging the following is useful when using docker. +Change the docker-compose file to include the following for the mariadb service: +``` + networks: + # available on port 3306, host name 'sqldb' + - caosnet + ports: + - 3306:3306 +``` +Check it with `mysql -ucaosdb -prandom1234 -h127.0.0.1 caosdb` +Add the appropriate changes (e.g. `performance_schema=ON`) to `profiles/empty/custom/mariadb.conf.d/mariadb.cnf` (or in the profile folder that you use). + +### Manual Java-side benchmarking # Benchmarking can be done using the `TransactionBenchmark` class (in package `org.caosdb.server.database.misc`). @@ -26,9 +161,95 @@ Benchmarking can be done using the `TransactionBenchmark` class (in package - `Container.getTransactionBenchmark().addBenchmark()` - `Query.addBenchmark()` -# Miscellaneous notes # -Notes to self, details, etc. +To enable transaction benchmarks and disable caching in the server, set these +server settings: +```conf +TRANSACTION_BENCHMARK_ENABLED=true +CACHE_DISABLE=true +``` +Additionally, the server should be started via `make run-debug` (instead of +`make run-single`), otherwise the benchmarking will not be active. + +#### Notable benchmarks and where to find them ## + +| Name | Where measured | What measured | +|--------------------------------------|----------------------------------------------|-------------------------------| +| `Retrieve.init` | transaction/Transaction.java#135 | transaction/Retrieve.java#48 | +| `Retrieve.transaction` | transaction/Transaction.java#174 | transaction/Retrieve.java#133 | +| `Retrieve.post_transaction` | transaction/Transaction.java#182 | transaction/Retrieve.java#77 | +| `EntityResource.httpGetInChildClass` | resource/transaction/EntityResource.java#118 | all except XML generation | +| `ExecuteQuery` | ? | ? | +| | | | + +### 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 +``` +Alternatively, start the server (without docker) with the `run-debug-single` make target, it will expose +the JMX interface, by default on port 9090. + +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). + + +#### Example settings for VisualVM + +In the sampler settings, you may want to add these expressions to the blocked +packages: `org.restlet.**, com.mysql.**`. Branches on the call tree which are +entirely inside the blacklist, will become leaves. Alternatively, specify a +whitelist, for example with `org.caosdb.server.database.backend.implementation.**`, +if you only want to see the time spent for certain MySQL calls. + + +## 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. ## On method calling order and benchmarked events ## @@ -56,29 +277,37 @@ Notes to self, details, etc. - Executing the SQL statement - Java-side caching -## Server settings ## +## What is measured ## -- To enable the SQL general logs, log into the SQL server and do: - ```sql -SET GLOBAL log_output = 'TABLE'; -SET GLOBAL general_log = 'ON'; -``` -- To enable transaction benchmarks and disable caching in the server, set these - server settings: -```conf -TRANSACTION_BENCHMARK_ENABLED=true -CACHE_DISABLE=true -``` -- Additionally, the server should be started via `make run-debug` (instead of - `make run-single`), otherwise the benchmarking will not be active. +For a consistent interpretation, the exact definitions of the measured times are as follows: -## Notable benchmarks and where to find them ## +### SQL logs ### -| Name | Where measured | What measured | -|--------------------------------------|----------------------------------------------|-------------------------------| -| `Retrieve.init` | transaction/Transaction.java#135 | transaction/Retrieve.java#48 | -| `Retrieve.transaction` | transaction/Transaction.java#174 | transaction/Retrieve.java#133 | -| `Retrieve.post_transaction` | transaction/Transaction.java#182 | transaction/Retrieve.java#77 | -| `EntityResource.httpGetInChildClass` | resource/transaction/EntityResource.java#118 | all except XML generation | -| `ExecuteQuery` | ? | ? | -| | | | +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/misc/bend_symlinks/src/main.sh b/misc/bend_symlinks/src/main.sh index c2a6a94766d437e41619c2602d1c62417e09ee42..1d148df30533529855f65141ba5705d76b0cb687 100644 --- a/misc/bend_symlinks/src/main.sh +++ b/misc/bend_symlinks/src/main.sh @@ -66,8 +66,10 @@ if [ $IS_MOVE -eq 1 ] ; then REPLACEMENT=$(new_dir "$REPLACEMENT") fi + set -o noglob -for syml in $(find -P $(realpath $FILE_SYSTEM_ROOT) -type l) ; do +find -P $(realpath $FILE_SYSTEM_ROOT) -type l -print0 | + while ISF= read -r -d '' syml; do OLD_TARGET=$(realpath -m "$syml" | sed -n -r "/$REGEX_OLD/p") if [ -z "$OLD_TARGET" ] ; then # filter non matching diff --git a/misc/bend_symlinks/src/utils.sh b/misc/bend_symlinks/src/utils.sh index 0e10fe9acc0c1c27fa3f2f58add8e7daf845e24c..2ddda289639a9a38e13540da2caad32dc439aa4a 100644 --- a/misc/bend_symlinks/src/utils.sh +++ b/misc/bend_symlinks/src/utils.sh @@ -37,6 +37,8 @@ function escape_simple_path () { SPATH=$(echo "$SPATH" | sed -r "s/\(/\\\\(/g") # { SPATH=$(echo "$SPATH" | sed -r "s/\{/\\\\{/g") + # white space + SPATH=$(echo "$SPATH" | sed -r "s/ /\\ /g") echo "$SPATH" } diff --git a/misc/bend_symlinks/test/test_suite.sh b/misc/bend_symlinks/test/test_suite.sh index 9ffa6b1fffe9ec095ebbc3a25bdfa5ee767e1a69..2def1fda1450ad9c3b412f033a6085bddd840d8f 100755 --- a/misc/bend_symlinks/test/test_suite.sh +++ b/misc/bend_symlinks/test/test_suite.sh @@ -23,8 +23,11 @@ tearDown () { _make_test_file () { touch "$DATA_DIR/$1" - ln -s $(realpath "$DATA_DIR/$1") "$FILE_SYSTEM_ROOT/$1" - assertEquals "initial target $1" $(realpath "$FILE_SYSTEM_ROOT/$1") $(realpath "$DATA_DIR/$1") + TARGET=$(realpath "$DATA_DIR/$1") + LINK=$FILE_SYSTEM_ROOT/$1 + ln -s "$TARGET" "$LINK" + LINKED=$(realpath "$LINK") + assertEquals "initial target $1" "$LINKED" "$TARGET" } _break_link_move_file () { @@ -35,7 +38,8 @@ _break_link_move_file () { NEW_PATH_REAL=$(realpath "$NEW_PATH") LINK="$FILE_SYSTEM_ROOT/$1" mv "$OLD_PATH_REAL" "$NEW_PATH_REAL" - assertEquals "still target $OLD_PATH_REAL" $(realpath "$LINK") "$OLD_PATH_REAL" + LINKED=$(realpath "$LINK") + assertEquals "still target $OLD_PATH_REAL" "$LINKED" "$OLD_PATH_REAL" assertFalse "$LINK link is broken" "[ -f '$LINK' ]" assertFalse "$OLD_PATH_REAL was moved" "[ -f '$OLD_PATH_REAL' ]" assertTrue "$NEW_PATH_REAL is there" "[ -f '$NEW_PATH_REAL' ]" @@ -51,7 +55,7 @@ assertLinkOk () { LINK=$(realpath "$FILE_SYSTEM_ROOT/$1") TARGET=$(realpath "$DATA_DIR/$2") assertTrue "target exists $LINK" "[ -f '$LINK' ]" - assertEquals "target matches $TARGET" $TARGET "$LINK" + assertEquals "target matches $TARGET" "$TARGET" "$LINK" set +o noglob } @@ -135,6 +139,7 @@ testFullPathWithStrangeChars () { _testFullPathWithStrageChars "{" _testFullPathWithStrageChars "]" _testFullPathWithStrageChars "[.]" + _testFullPathWithStrageChars " " } testRegex () { diff --git a/src/doc/CaosDB-Query-Language.md b/src/doc/CaosDB-Query-Language.md index 07fbdbc310b8d22de4642f041918710e6e707488..ffa889c6b8acd0cd635049ed2aaceb748d56f2a9 100644 --- a/src/doc/CaosDB-Query-Language.md +++ b/src/doc/CaosDB-Query-Language.md @@ -121,7 +121,7 @@ Examples: ##### `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) +* ''True'' iff the time of d1 is after the the time of d2 according to [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time). * ''False'' otherwise. ###### [SemiCompleteDateTime](Datatype#datetime) diff --git a/src/doc/Permissions.rst b/src/doc/Permissions.rst index c624092aff4f479bc8ed48f439109530cc5295aa..2a1278ec450a9bf38ee03546b7301f2aabd76151 100644 --- a/src/doc/Permissions.rst +++ b/src/doc/Permissions.rst @@ -1,4 +1,5 @@ -#Permissions +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 @@ -28,7 +29,7 @@ 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 +- A :doc:`role <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. diff --git a/src/doc/administration/configuration.rst b/src/doc/administration/configuration.rst index bbafcf22bd8576e141cb9d7e8388212ccf24d934..196acb34ee9afbe6b35d530b058fac6c275299aa 100644 --- a/src/doc/administration/configuration.rst +++ b/src/doc/administration/configuration.rst @@ -19,18 +19,21 @@ 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>`_. + the `default file + <https://gitlab.indiscale.com/caosdb/src/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>`_. + :doc:`Permissions<../Permissions>` which are automatically set, based on user roles. See the + `default file + <https://gitlab.indiscale.com/caosdb/src/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>`_ + file + <https://gitlab.indiscale.com/caosdb/src/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`. @@ -38,13 +41,13 @@ In each of these directories, the server looks for the following files: 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>`_. + <https://gitlab.indiscale.com/caosdb/src/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>`_. + <https://gitlab.indiscale.com/caosdb/src/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 diff --git a/src/doc/administration/maintenance.rst b/src/doc/administration/maintenance.rst index 67d8475bf469957416a6f42e495db07b1533f151..5b9faeb1c5da3e68def79acae2d6901efa2e78de 100644 --- a/src/doc/administration/maintenance.rst +++ b/src/doc/administration/maintenance.rst @@ -21,12 +21,17 @@ 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:: +You can also save the content of CaosDB using XML. This is **not recommended** since it produces +less reproducible results than a plain SQL backup. However there may be cases in which an XML backup +is necessary, e.g., when transferring entities between two different CaosDB instances. + +Collect the entities that you want to export in a +:any:`Container<caosdb-pylib:caosdb.common.models.Container>`, named ``cont`` here. 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) @@ -38,11 +43,11 @@ named ``cont``. Then you can export the XML with:: Restoring a Backup ------------------ -.. note : +.. warning:: 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:: +backup is a tarball:: tar xvf /path/to/caosroot.tar.gz @@ -52,16 +57,17 @@ You find the documentation on how to restore the data in the SQL-Backend :any:` 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 = 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). +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 index 1f2b9a1d80e11abed48ae89502ab6f80c099507e..6dc08999f38e1912e72440c609b8cde1ea7ce0bb 100644 --- a/src/doc/administration/server_side_scripting.rst +++ b/src/doc/administration/server_side_scripting.rst @@ -31,7 +31,7 @@ The script has to be executable and must be placed somewhere in one of the direc 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>` +For more information see the :doc:`specification of the API <../specification/Server-side-scripting>` Environment ------------ diff --git a/src/doc/conf.py b/src/doc/conf.py index fb3d6264cc4add018b83d08378813aab9d10964d..46580733a72952fff673567ba1564903b60d584c 100644 --- a/src/doc/conf.py +++ b/src/doc/conf.py @@ -47,6 +47,7 @@ extensions = [ "recommonmark", # For markdown files. "sphinx.ext.autosectionlabel", # Allow reference sections using its title "sphinx_rtd_theme", + "sphinxcontrib.plantuml", # PlantUML diagrams ] # Add any paths that contain templates here, relative to this directory. @@ -55,8 +56,8 @@ 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' +source_suffix = ['.rst', '.md'] +# source_suffix = '.rst' # The master toctree document. master_doc = 'index' @@ -190,14 +191,18 @@ epub_exclude_files = ['search.html'] # '<namespace_here>' : ('<base_url_here>', 'javadoc'), # } +javadoc_url_map = { + 'org.restlet': ('https://javadocs.restlet.talend.com/2.4/jse/api', '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), + "caosdb-pylib": ("https://docs.indiscale.com/caosdb-pylib/", None), + "caosdb-mysqlbackend": ("https://docs.indiscale.com/caosdb-mysqlbackend/", None), } diff --git a/src/doc/development/benchmarking.md b/src/doc/development/benchmarking.md index f2d663f6e69799c7a87a28fbdf32f15fab892ac1..0be781453a6f85577dd95f89844fd95f2b4141ba 100644 --- a/src/doc/development/benchmarking.md +++ b/src/doc/development/benchmarking.md @@ -1,130 +1,4 @@ - # 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. +Please refer to the file `doc/devel/Benchmarking.md` in the CaosDB sources for developer resources +how to do benchmarking and profiling of CaosDB. diff --git a/src/doc/development/devel.rst b/src/doc/development/devel.rst new file mode 100644 index 0000000000000000000000000000000000000000..141b045594fe6f8a58c86f758734e160e5a8c2fe --- /dev/null +++ b/src/doc/development/devel.rst @@ -0,0 +1,25 @@ +Developing CaosDB +================= + +.. toctree:: + :glob: + :maxdepth: 2 + + Structure of the Java code <structure> + Benchmarking CaosDB <benchmarking> + +CaosDB is an Open-Source project, so anyone may modify the source as they like. These pages aim to +provide some help for all CaosDB developers. + +More generally, these are the most common ways to contribute to CaosDB: + +- 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 this `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 the developers at *info (AT) caosdb.de*. diff --git a/src/doc/development/structure.rst b/src/doc/development/structure.rst new file mode 100644 index 0000000000000000000000000000000000000000..3848e983fca5d05871212e0985b98d539c04b7fe --- /dev/null +++ b/src/doc/development/structure.rst @@ -0,0 +1,178 @@ +CaosDB's Internal Structure +=========================== + +The CaosDB server + +- builds upon the `Restlet <https://restlet.talend.com/>`_ framework to provide a REST interface to + the network. See the :ref:`HTTP Resources` section for more information. +- uses an SQL database (MariaDB or MySQL) as the backend for data storage. This is documented in + the :ref:`MySQL Backend` section. +- has an internal scheduling framework to organize the required backend jobs. Read more on this in + the :ref:`Transactions and Schedules<transactions>` section. +- may use a number of authentication providers. Documentation for this is still to come. + +.. _HTTP Resources: + +HTTP Resources +-------------- + +HTTP resources are implemented in the :java:ref:`resource<org.caosdb.server.resource>` package, in +classes inheriting from :java:ref:`AbstractCaosDBServerResource` (which inherits Restlet's +:java:ref:`Resource<org.restlet.resource.Resource>` class). The main :java:ref:`CaosDBServer` class +defines which HTTP resource (for example ``/Entity/{specifier}``) will be handled by which class +(:java:ref:`EntityResource` in this case). + +Implementing classes need to overwrite for example the ``httpGetInChildClass()`` method (or methods +corresponding to other HTTP request methods such as POST, PUT, ...). Typically, they might call the +``execute()`` method of a :java:ref:`Transaction<org.caosdb.server.transaction.Transaction>` object. +Transactions are explained in detail in the :ref:`Transactions and Schedules<transactions>` section. + +.. uml:: + + @startuml + abstract AbstractCaosDBServerResource { + {abstract} httpGetInChildClass() + {abstract} httpPostInChildClass() + {abstract} ...InChildClass() + } + abstract RetrieveEntityResource + class EntityResource + AbstractCaosDBServerResource <|-- RetrieveEntityResource + RetrieveEntityResource <|-- EntityResource + @enduml + +.. _MySQL Backend: + +MySQL Backend +------------- + +The MySQL backend in CaosDB may be substituted by other backends, but at the time of writing this +documentation, only MySQL (MariaDB is used for testing) is implemented. There are the following +main packages which handle the backend: + +:java:ref:`backend.interfaces<interfaces>` + Interfaces which backends may implement. The main method for most interfaces is + ``execute(...)`` with arguments depending on the specific interface, and benchmarking methods + (``getBenchmark()`` and ``setTransactionBenchmark(b)`` may also be required. + +:java:ref:`backend.implementation.MySQL<MySQL>` + MySQL implementations of the interfaces. Typical "simple" implementations create a prepared SQL + statement from the arguments to ``execute(...)`` and send it to the SQL server. They may also + have methods for undoing and cleanup, using an :java:ref:`UndoHandler`. + +:java:ref:`backend.transaction<backend.transaction>` classes + Subclasses of the abstract :java:ref:`BackendTransaction` which implement the ``execute()`` + method. These classes may use specific backend implementations (like for example the MySQL + implementations) to interact with the backend database. + +For example, the structure when getting an Entity ID by name looks like this: + +.. uml:: + + @startuml + together { + abstract BackendTransaction { + HashMap impl // stores all implementations + {abstract} execute() + } + note left of BackendTransaction::impl + Stores the + implementation + for each + interface." + end note + package ...backend.interfaces { + interface GetIDByNameImpl { + {abstract} execute(String name, String role, String limit) + } + } + } + together { + package ...backend.transaction { + class GetIDByName extends BackendTransaction { + execute() + } + } + package ...backend.implementation.MySQL { + class MySQLGetIDByName implements GetIDByNameImpl { + execute(String name, String role, String limit) + } + } + } + + GetIDByName::execute --r-> MySQLGetIDByName + @enduml + +.. _transactions: + +Transactions and Schedules +-------------------------- + +In CaosDB, several client requests may be handled concurrently. This poses no problem as long as +only read-only requests are processed, but writing transactions need to block other requests. +Therefore all transactions (between their first and last access) block write transactions other than +themselves from writing to the backend, while read transactions may happen at any time, except when +a write transaction actually writes to the backend. + +.. note:: + + There is a fine distinction between write transactions on the CaosDB server and actually writing + to the backend, since even transactions which need only very short write access to the backend may + require extensive read access before, for example to check for permissions or to check if the + intended write action makes sense (linked entities must exist, they may need to be of the correct + RecordType, etc.). + +The request handling in CaosDB is organized in the following way: + +- HTTP resources usually create a :java:ref:`Transaction` object and call its + :java:ref:`Transaction.execute()` method. Entities are passed to and from the transaction via + :java:ref:`TransactionContainers<TransactionContainer>` (basically normal + :java:ref:`Containers<Container>`, enriched with some metadata). +- The Transaction keeps a :java:ref:`Schedule` of related :java:ref:`Jobs<Job>` (each also wrapping + a specific Transaction), which may be called at different stages, called + :java:ref:`TransactionStages<TransactionStage>`. +- The Transaction's ``execute()`` method, when called, in turn calls a number of methods for + initialization, checks, preparations, cleanup etc. Additionally the scheduled jobs are executed + at their specified stages, for example all jobs scheduled for ``INIT`` are executed immediately + after calling ``Transaction.init()``. Please consult the API documentation for + :java:ref:`Transaction.execute()` for details. + + Most importantly, the (abstract) method ``transaction()`` is called by ``execute()``, which in + inheriting classes typically interacts with the backend via :java:ref:`execute(BackendTransaction, + Access)<Transaction.execute(K t, Access access)>`, which in turn calls the + ``BackendTransaction``'s :java:ref:`BackendTransaction.executeTransaction()` method (just a thin + wrapper around its ``execute()`` method). + +Summarized, the classes are connected like this: + +.. uml:: + + @startuml + hide empty members + + class Container + class TransactionContainer extends Container + + abstract Transaction { + Schedule schedule + TransactionContainer container + execute() + execute(BackendTransaction t, Access access)\n // -> t.executeTransaction(t) + } + + class Schedule + class ScheduledJob + abstract Job { + TransactionStage stage + Transaction transaction + execute(BackendTransaction t)\n // -> transaction.execute(t, transaction.access) + } + + Schedule "*" *- ScheduledJob + ScheduledJob *- Job + Job o--d- Transaction + + TransactionContainer -* Transaction::container + Transaction::schedule *- Schedule + @enduml + diff --git a/src/doc/index.rst b/src/doc/index.rst index 6a9013c51720f13b4f473d9213bb64b8d4d27eec..2ae00f7e685407325903159257d6561912f94a66 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -12,20 +12,18 @@ Welcome to caosdb-server's documentation! Concepts <concepts> Query Language <CaosDB-Query-Language> administration - development/* + Development <development/devel> 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>`. +This documentation helps you to :doc:`get started<README_SETUP>`, explains the most important +:doc:`concepts<concepts>` and has information if you want to :doc:`develop<development/devel>` CaosDB yourself. Indices and tables ================== * :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/src/doc/roles.md b/src/doc/roles.md index 80729fb11f8113c64da7e9bf26b04de68fdd6681..5f4641b7a6f6fcb1b63cdd26edb706af117ee7d6 100644 --- a/src/doc/roles.md +++ b/src/doc/roles.md @@ -10,7 +10,7 @@ users may have the same role, and there may be roles without any users. 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 +is [permission](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 @@ -32,4 +32,4 @@ There are some special roles, which are automatically assigned to users: 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). +[permissions](Permissions). diff --git a/src/doc/specification/AbstractProperty.md b/src/doc/specification/AbstractProperty.md index d594f81967cacd708699ceaeccd9188685a7184d..a337fa5ed053742a0e0cee4e9771cee862c19d7c 100644 --- a/src/doc/specification/AbstractProperty.md +++ b/src/doc/specification/AbstractProperty.md @@ -1,7 +1,12 @@ +.. note :: + + This document has not been updated for a long time. Although it is concerned with the mostly + stable API, its content may no longer reflect the actual CaosDB behavior. + # AbstractProperty Specification ## Introduction -An `AbstractProperty` is one of the basal [objects of HeartDB](./HeartDBObject). +An `AbstractProperty` is one of the basal objects of HeartDB. An `AbstractProperty` MUST have the following _qualities_ (shortcut in brackets): * a persistent id (`id`) * an unique name (`name`) @@ -54,7 +59,7 @@ Any xml representation of an `AbstractProperty` that is retrieved from the Heart <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). +* If the called Property does not exist or if the Property called without permission, the HeartDB Server will return an Error. ### 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: @@ -80,7 +85,7 @@ Any xml representation of an `AbstractProperty` that is to be posted to the Hear * 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). +* If the `<Property/>` tag isn't compliant with these the HeartDB Server will return an Error. ---- ## Examples diff --git a/src/doc/specification/Fileserver.md b/src/doc/specification/Fileserver.md index fda2ec183c909e494a407f74eb1f4d03a5a23625..badb4b413f4ff898ee9455fce303f7bcaad90b9d 100644 --- a/src/doc/specification/Fileserver.md +++ b/src/doc/specification/Fileserver.md @@ -35,7 +35,8 @@ where ### 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). +There is an example on file upload using cURL described in detail in [the curl section of this +documentation](../administration/curl-access.md). 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. diff --git a/src/doc/specification/RecordType.md b/src/doc/specification/RecordType.md index 068c433d8c5390882176c0ff8783576127a2da2e..a7830b863712d8b1db3e54c617572ee16bf4e166 100644 --- a/src/doc/specification/RecordType.md +++ b/src/doc/specification/RecordType.md @@ -1,5 +1,5 @@ # 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. diff --git a/src/doc/specification/index.rst b/src/doc/specification/index.rst index e9683072f555725ee8b74a0ae468c6a407e244b2..3609aa4a8eb6165adf6070faae26bfb8bd4ba50f 100644 --- a/src/doc/specification/index.rst +++ b/src/doc/specification/index.rst @@ -8,15 +8,13 @@ Specification :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> + Server side scripting <Server-side-scripting> 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 4a69f24d4d905f5d8fea360c291dfc1522b414fc..74e70fc61feeb6ace0f1919610bee0cb868439cf 100644 --- a/src/main/java/org/caosdb/server/CaosDBServer.java +++ b/src/main/java/org/caosdb/server/CaosDBServer.java @@ -117,7 +117,7 @@ public class CaosDBServer extends Application { private static ArrayList<Runnable> postShutdownHooks = new ArrayList<Runnable>(); private static ArrayList<Runnable> preShutdownHooks = new ArrayList<Runnable>(); private static boolean START_BACKEND = true; - private static boolean INSECURE = false; + private static boolean NO_TLS = false; public static final String REQUEST_TIME_LOGGER = "REQUEST_TIME_LOGGER"; public static final String REQUEST_ERRORS_LOGGER = "REQUEST_ERRORS_LOGGER"; private static Scheduler SCHEDULER; @@ -160,24 +160,23 @@ public class CaosDBServer extends Application { * 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) + * <li>"--no-backend": flag to run caosdb without any backend (for testing purposes) + * <li>"--no-tls": 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. + * <p>The --no-backend flag is 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("nobackend")) { + if (s.equals("--no-backend")) { START_BACKEND = false; - } else if (s.equals("insecure")) { - INSECURE = true; + } else if (s.equals("--no-tls")) { + NO_TLS = true; } } - INSECURE = INSECURE && isDebugMode(); // only allow insecure in debug mode START_BACKEND = START_BACKEND || !isDebugMode(); // always start backend if not in debug mode } @@ -347,7 +346,7 @@ public class CaosDBServer extends Application { final int maxTotalConnections = Integer.parseInt(getServerProperty(ServerProperties.KEY_MAX_CONNECTIONS)); - if (INSECURE) { + if (NO_TLS) { runHTTPServer(port_http, initialConnections, maxTotalConnections); } else { runHTTPSServer( diff --git a/src/main/java/org/caosdb/server/database/BackendTransaction.java b/src/main/java/org/caosdb/server/database/BackendTransaction.java index 61aebf42a2dfd65fb9ed1c84459064e3aa274c06..965e7181d1d010528b341f6cbf34d4c0c67a9e5c 100644 --- a/src/main/java/org/caosdb/server/database/BackendTransaction.java +++ b/src/main/java/org/caosdb/server/database/BackendTransaction.java @@ -145,6 +145,7 @@ public abstract class BackendTransaction implements Undoable { protected abstract void execute(); + /** Like execute(), but with benchmarking measurement. */ public final void executeTransaction() { final long t1 = System.currentTimeMillis(); execute(); diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java index ec0c0c1b0a067d529ecb72309d7fe83a8b37f819..7af4022595f64b909231c72c83ae1e151a9d06c1 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java @@ -27,10 +27,14 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import org.apache.shiro.SecurityUtils; +import org.caosdb.server.database.DatabaseUtils; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.interfaces.RetrieveAllImpl; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.Role; +import org.caosdb.server.permissions.EntityACL; +import org.caosdb.server.permissions.EntityPermission; public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImpl { @@ -38,17 +42,20 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp super(access); } - public static final String STMT_GET_ALL_HEAD = "Select id from entities where id > 99"; + public static final String STMT_GET_ALL_HEAD = + "SELECT e.id AS ID, a.acl AS ACL FROM entities AS e JOIN entity_acl AS a ON (e.acl = a.id) WHERE e.id > 99"; public static final String STMT_ENTITY_WHERE_CLAUSE = - " AND ( role=? OR role='" + " AND ( e.role='" + + Role.Record + + "' OR e.role='" + Role.RecordType - + "' OR role='" + + "' OR e.role='" + Role.Property - + "' OR role='" + + "' OR e.role='" + Role.File + "'" + " )"; - public static final String STMT_OTHER_ROLES = " AND role=?"; + public static final String STMT_OTHER_ROLES = " AND e.role=?"; @Override public List<Integer> execute(final String role) throws TransactionException { @@ -58,10 +65,7 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp + (role.equalsIgnoreCase("ENTITY") ? STMT_ENTITY_WHERE_CLAUSE : STMT_OTHER_ROLES); final PreparedStatement stmt = prepareStatement(STMT_GET_ALL); - if (role.equalsIgnoreCase("ENTITY")) { - stmt.setString(1, Role.Record.toString()); - - } else { + if (!role.equalsIgnoreCase("ENTITY")) { stmt.setString(1, role); } @@ -69,7 +73,11 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp try { final ArrayList<Integer> ret = new ArrayList<Integer>(); while (rs.next()) { - ret.add(rs.getInt(1)); + String acl = DatabaseUtils.bytes2UTF8(rs.getBytes("ACL")); + if (EntityACL.deserialize(acl) + .isPermitted(SecurityUtils.getSubject(), EntityPermission.RETRIEVE_ENTITY)) { + ret.add(rs.getInt("ID")); + } } return ret; } finally { diff --git a/src/main/java/org/caosdb/server/entity/ClientMessage.java b/src/main/java/org/caosdb/server/entity/ClientMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..f6b996770b1d1d88992a56551acc0d5aaa5db1f0 --- /dev/null +++ b/src/main/java/org/caosdb/server/entity/ClientMessage.java @@ -0,0 +1,90 @@ +/* + * ** 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.entity; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import org.jdom2.Attribute; +import org.jdom2.Element; + +/** + * Class which represents client messages. Client messages is a way to extend the Entity API with + * special properties which may be used by plug-ins. + * + * <p>If no plug-in handles the client message, it is printed back to the response unaltered. + * + * <p>Client message can have arbitrary key-value (string-string typed) tuples {@link #properties}. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class ClientMessage extends Message { + + private static final long serialVersionUID = 1L; + private Map<String, String> properties = new HashMap<>(); + + public ClientMessage(String type, String body) { + super(type, null, null, null); + } + + @Override + public Element toElement() { + final Element e = new Element(this.type); + for (Entry<String, String> a : this.properties.entrySet()) { + e.setAttribute(a.getKey(), a.getValue()); + } + return e; + } + + @Override + public void addToElement(final Element parent) { + final Element e = toElement(); + parent.addContent(e); + } + + /** NB: This is the only place where properties are set in this class. */ + public static ClientMessage fromXML(Element pe) { + ClientMessage result = new ClientMessage(pe.getName(), pe.getText()); + for (Attribute a : pe.getAttributes()) { + result.properties.put(a.getName(), a.getValue()); + } + return result; + } + + public String getProperty(String key) { + return properties.get(key); + } + + @Override + public String toString() { + return this.type + " - " + this.properties.toString(); + } + + @Override + public int hashCode() { + return type.hashCode() + + (this.getBody() == null ? 0 : this.getBody().hashCode()) + + this.properties.hashCode(); + } +} diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java index aa4c96127cfc97b03f308a50cb587dc12949c105..0a60f9af59f731098a0e194e4848b578094d4e68 100644 --- a/src/main/java/org/caosdb/server/entity/Entity.java +++ b/src/main/java/org/caosdb/server/entity/Entity.java @@ -35,6 +35,7 @@ import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.Permission; import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBException; +import org.caosdb.server.accessControl.Principal; import org.caosdb.server.database.proto.SparseEntity; import org.caosdb.server.database.proto.VerySparseEntity; import org.caosdb.server.datatype.AbstractCollectionDatatype; @@ -104,10 +105,12 @@ public class Entity extends AbstractObservable implements EntityInterface { public void checkPermission(final Subject subject, final Permission permission) { try { if (!this.hasPermission(subject, permission)) { + String user = "The current user "; + if (subject.getPrincipal() instanceof Principal) { + user = ((Principal) subject.getPrincipal()).getUsername(); + } throw new AuthorizationException( - subject.getPrincipal().toString() - + " doesn't have permission " - + permission.toString()); + user + " doesn't have permission " + permission.toString()); } } catch (final NullPointerException e) { throw new AuthorizationException("This entity doesn't have an ACL!"); @@ -848,34 +851,7 @@ public class Entity extends AbstractObservable implements EntityInterface { } else if (getRole() == Role.QueryTemplate && pe.getName().equalsIgnoreCase("Query")) { setQueryTemplateDefinition(pe.getTextNormalize()); } else { - final String type = pe.getName(); - Integer code = null; - String localDescription = null; - String body = null; - - // Parse MESSAGE CODE. - if (pe.getAttribute("code") != null && !pe.getAttributeValue("code").equals("")) { - try { - code = Integer.parseInt(pe.getAttributeValue("code")); - } catch (final NumberFormatException e) { - addInfo("Message code was " + pe.getAttributeValue("code") + "."); - addError(ServerMessages.PARSING_FAILED); - setEntityStatus(EntityStatus.UNQUALIFIED); - } - } - - // Parse MESSAGE DESCRIPTION. - if (pe.getAttribute("description") != null - && !pe.getAttributeValue("description").equals("")) { - localDescription = pe.getAttributeValue("description"); - } - - // Parse MESSAGE BODY. - if (pe.getTextTrim() != null && !pe.getTextTrim().equals("")) { - body = pe.getTextTrim(); - } - - addMessage(new Message(type, code, localDescription, body)); + addMessage(ClientMessage.fromXML(pe)); } } diff --git a/src/main/java/org/caosdb/server/entity/Message.java b/src/main/java/org/caosdb/server/entity/Message.java index 743a2b62fa87570e0dbca05a1f279e6764920f0b..3e469b2dc84b07e3ac3de378f8990cc520989cc0 100644 --- a/src/main/java/org/caosdb/server/entity/Message.java +++ b/src/main/java/org/caosdb/server/entity/Message.java @@ -27,7 +27,7 @@ import org.jdom2.Element; public class Message extends Exception implements Comparable<Message>, ToElementable { - private final String type; + protected final String type; private final Integer code; private final String description; private final String body; @@ -127,7 +127,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement return this.code; } - public final Element toElement() { + public Element toElement() { final Element e = new Element(this.type); if (this.code != null) { e.setAttribute("code", Integer.toString(this.code)); @@ -142,7 +142,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement } @Override - public final void addToElement(final Element parent) { + public void addToElement(final Element parent) { final Element e = toElement(); parent.addContent(e); } diff --git a/src/main/java/org/caosdb/server/entity/UpdateEntity.java b/src/main/java/org/caosdb/server/entity/UpdateEntity.java index aa6d591602df66cc9317351cf6a1ea980bc53ea3..884632b5ed3f92d740f2eea69ed49ef77511fd1b 100644 --- a/src/main/java/org/caosdb/server/entity/UpdateEntity.java +++ b/src/main/java/org/caosdb/server/entity/UpdateEntity.java @@ -22,11 +22,21 @@ */ package org.caosdb.server.entity; +import org.caosdb.server.transaction.WriteTransaction; import org.caosdb.server.utils.EntityStatus; import org.jdom2.Element; +/** + * UpdateEntity class represents entities which are to be updated. The previous version is appeded + * during the {@link WriteTransaction} transactions initialization. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class UpdateEntity extends WritableEntity { + /** The previous version of this entity. */ + private EntityInterface original = null; + public UpdateEntity(final Element element) { super(element); } @@ -35,4 +45,12 @@ public class UpdateEntity extends WritableEntity { public boolean skipJob() { return getEntityStatus() != EntityStatus.QUALIFIED; } + + public void setOriginal(EntityInterface original) { + this.original = original; + } + + public EntityInterface getOriginal() { + return this.original; + } } diff --git a/src/main/java/org/caosdb/server/jobs/Job.java b/src/main/java/org/caosdb/server/jobs/Job.java index 28dc512b669eb6f1df0cfd24e32e2051f734b697..9432fc3916606cddbf64922850d7f2f9ff5284c2 100644 --- a/src/main/java/org/caosdb/server/jobs/Job.java +++ b/src/main/java/org/caosdb/server/jobs/Job.java @@ -50,10 +50,7 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.jobs.core.Mode; import org.caosdb.server.transaction.Transaction; -import org.caosdb.server.utils.AbstractObservable; import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; import org.reflections.Reflections; @@ -62,14 +59,19 @@ import org.reflections.Reflections; * * @todo Describe me. */ -public abstract class Job extends AbstractObservable implements Observer { +public abstract class Job { + /** All known Job classes, by name (actually lowercase getSimpleName()). */ + static HashMap<String, Class<? extends Job>> allClasses = null; + + private static List<Class<? extends Job>> loadAlways; + private Transaction<? extends TransactionContainer> transaction = null; private Mode mode = null; + private final TransactionStage stage; + private EntityInterface entity = null; public abstract JobTarget getTarget(); - private EntityInterface entity = null; - protected <S, T> HashMap<S, T> getCache(final String name) { return getTransaction().getCache(name); } @@ -115,9 +117,9 @@ public abstract class Job extends AbstractObservable implements Observer { protected Job() { if (this.getClass().isAnnotationPresent(JobAnnotation.class)) { - this.time = this.getClass().getAnnotation(JobAnnotation.class).time(); + this.stage = this.getClass().getAnnotation(JobAnnotation.class).stage(); } else { - this.time = JobExecutionTime.CHECK; + this.stage = TransactionStage.CHECK; } } @@ -139,10 +141,6 @@ public abstract class Job extends AbstractObservable implements Observer { getTransaction().getSchedule().runJob(entity, jobclass); } - protected void runJobFromSchedule(ScheduledJob job) { - getTransaction().getSchedule().runJob(job); - } - public EntityInterface getEntity() { return this.entity; } @@ -237,30 +235,33 @@ public abstract class Job extends AbstractObservable implements Observer { } } - @Override - public boolean notifyObserver(final String e, final Observable o) { - if (getEntity().getEntityStatus() != EntityStatus.UNQUALIFIED) { - getTransaction().getSchedule().runJob(this); - } - return true; - } - - static HashMap<String, Class<? extends Job>> allClasses = null; - private static List<Class<? extends Job>> loadAlways; - + /** + * Create a Job object with the given parameters. + * + * <p>This static method is used by other classes to create Job objects, instead of the private + * constructor. + * + * @return The generated Job object. + */ public static Job getJob( final String job, final Mode mode, final EntityInterface entity, final Transaction<? extends TransactionContainer> transaction) { + // Fill `allClasses` with available subclasses scanJobClasspath(); + // Get matching class for Job and generate it. final Class<? extends Job> jobClass = allClasses.get(job.toLowerCase()); return getJob(jobClass, mode, entity, transaction); } + /** + * Initialize {@code allClasses} with all {@code Job} classes found in the classpath. + * + * @todo Details when this has any effect. + */ private static void scanJobClasspath() { - if (allClasses == null || loadAlways == null) { allClasses = new HashMap<>(); loadAlways = new ArrayList<>(); @@ -463,23 +464,22 @@ public abstract class Job extends AbstractObservable implements Observer { + "]"; } - public void finish() { - super.removeAllObservers(); - } - public void print() { System.out.println(toString()); } - private final JobExecutionTime time; - - public JobExecutionTime getExecutionTime() { - return this.time; + public TransactionStage getTransactionStage() { + return this.stage; } + /** + * Return those matching jobs which are annotated with the "loadAlways" attribute. + * + * @return A list with the jobs. + */ public static List<Job> loadPermanentContainerJobs(Transaction<?> transaction) { final ArrayList<Job> jobs = new ArrayList<>(); - // load permanent jobs + // load permanent jobs: ContainerJob classes with the correct transaction for (Class<? extends Job> j : loadAlways) { if (ContainerJob.class.isAssignableFrom(j) && j.getAnnotation(JobAnnotation.class).transaction().isInstance(transaction)) { diff --git a/src/main/java/org/caosdb/server/jobs/JobAnnotation.java b/src/main/java/org/caosdb/server/jobs/JobAnnotation.java index 15193dcdd590745773f308c71b9ed3bb1351a413..bebebd75ed386c19f8abfb7c09e802dda5ec2941 100644 --- a/src/main/java/org/caosdb/server/jobs/JobAnnotation.java +++ b/src/main/java/org/caosdb/server/jobs/JobAnnotation.java @@ -26,9 +26,17 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.caosdb.server.transaction.TransactionInterface; +/** + * Jobs may be annotated with @JobAnnotation(...). + * + * <p>Without a JobAnnotation, the Job will run at the default {@link + * org.caosdb.server.transaction.Transaction#check() CHECK} stage. + * + * @see {@link TransactionStage} + */ @Retention(RetentionPolicy.RUNTIME) public @interface JobAnnotation { - JobExecutionTime time() default JobExecutionTime.CHECK; + TransactionStage stage() default TransactionStage.CHECK; String flag() default ""; diff --git a/src/main/java/org/caosdb/server/jobs/JobExecutionTime.java b/src/main/java/org/caosdb/server/jobs/JobExecutionTime.java deleted file mode 100644 index 9374ab9b1c2f4c285b538b1721441f0bc7272600..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/jobs/JobExecutionTime.java +++ /dev/null @@ -1,35 +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.jobs; - -public enum JobExecutionTime { - INIT, - PRE_CHECK, - CHECK, - POST_CHECK, - PRE_TRANSACTION, - TRANSACTION, - POST_TRANSACTION, - CLEANUP, - ROLL_BACK -} diff --git a/src/main/java/org/caosdb/server/jobs/Schedule.java b/src/main/java/org/caosdb/server/jobs/Schedule.java index 7f35106bcdda8ddc00512197091fa096a6bc157e..0090ec92dffb310052b5b8cc82424e2edd0288f9 100644 --- a/src/main/java/org/caosdb/server/jobs/Schedule.java +++ b/src/main/java/org/caosdb/server/jobs/Schedule.java @@ -22,94 +22,56 @@ */ package org.caosdb.server.jobs; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import org.caosdb.server.entity.EntityInterface; -class ScheduledJob { - - long runtime = 0; - final Job job; - private long startTime = -1; - - public ScheduledJob(final Job j) { - this.job = j; - } - - public void run() { - if (!hasStarted()) { - start(); - this.job.run(); - finish(); - - this.job.notifyObservers(null); - } - } - - private void start() { - this.startTime = System.currentTimeMillis(); - } - - private void finish() { - this.runtime += System.currentTimeMillis() - this.startTime; - this.job - .getContainer() - .getTransactionBenchmark() - .addMeasurement(this.job.getClass().getSimpleName(), this.runtime); - } - - void pause() { - this.runtime += System.currentTimeMillis() - this.startTime; - } - - void unpause() { - start(); - } - - private boolean hasStarted() { - return this.startTime != -1; - } - - public JobExecutionTime getExecutionTime() { - return this.job.getExecutionTime(); - } - - public boolean skip() { - return this.job.getTarget().skipJob(); - } -} - +/** + * Keeps track of Jobs. + * + * <p>The Schedule class orders jobs by {@link TransactionStage} and also assures that jobs are + * skipped when appropriate and to prevent that jobs run more than once (because sometimes they + * trigger each other). + */ public class Schedule { - private final CopyOnWriteArrayList<ScheduledJob> jobs = new CopyOnWriteArrayList<ScheduledJob>(); + private final Map<Integer, List<ScheduledJob>> jobLists = new HashMap<>(); private ScheduledJob running = null; - public void addAll(final Collection<Job> jobs) { + public List<ScheduledJob> addAll(final Collection<Job> jobs) { + final List<ScheduledJob> result = new ArrayList<ScheduledJob>(jobs.size()); for (final Job j : jobs) { - add(j); + result.add(add(j)); } + return result; } public ScheduledJob add(final Job j) { - ScheduledJob ret = new ScheduledJob(j); - this.jobs.add(ret); + final ScheduledJob ret = new ScheduledJob(j); + List<ScheduledJob> jobs = jobLists.get(ret.getTransactionStage().ordinal()); + if (jobs == null) { + jobs = new CopyOnWriteArrayList<ScheduledJob>(); + jobLists.put(ret.getTransactionStage().ordinal(), jobs); + } + jobs.add(ret); return ret; } - public void runJobs(final JobExecutionTime time) { - for (final ScheduledJob scheduledJob : this.jobs) { - if (scheduledJob.getExecutionTime().ordinal() == time.ordinal() - || (time.ordinal() <= JobExecutionTime.POST_CHECK.ordinal() - && scheduledJob.getExecutionTime().ordinal() < time.ordinal())) { + /** Run all Jobs from the specified {@link TransactionStage}. */ + public void runJobs(final TransactionStage stage) { + final List<ScheduledJob> jobs = this.jobLists.get(stage.ordinal()); + if (jobs != null) { + for (final ScheduledJob scheduledJob : jobs) { runJob(scheduledJob); } } } - protected void runJob(final ScheduledJob scheduledJob) { - if (!this.jobs.contains(scheduledJob)) { - throw new RuntimeException("Job was not in schedule."); - } + public void runJob(final ScheduledJob scheduledJob) { if (scheduledJob.skip()) { return; } @@ -127,18 +89,15 @@ public class Schedule { } } - public void runJob(final Job j) { - for (final ScheduledJob scheduledJob : this.jobs) { - if (scheduledJob.job == j) { - scheduledJob.run(); - return; - } - } - throw new RuntimeException("Job was not in schedule."); - } - + /** Run all scheduled Jobs of a given class for the given entity. */ public void runJob(final EntityInterface entity, final Class<? extends Job> jobclass) { - for (final ScheduledJob scheduledJob : this.jobs) { + + // the jobs of this class are in the jobList for the TransactionStage of the jobClass. + final List<ScheduledJob> jobs = + jobclass.isAnnotationPresent(JobAnnotation.class) + ? this.jobLists.get(jobclass.getAnnotation(JobAnnotation.class).stage().ordinal()) + : this.jobLists.get(TransactionStage.CHECK.ordinal()); + for (final ScheduledJob scheduledJob : jobs) { if (jobclass.isInstance(scheduledJob.job)) { if (scheduledJob.job.getEntity() == entity) { runJob(scheduledJob); diff --git a/src/main/java/org/caosdb/server/jobs/ScheduledJob.java b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java new file mode 100644 index 0000000000000000000000000000000000000000..65bc3a1947baf5b76539f281e0d855e878a3bdfd --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java @@ -0,0 +1,91 @@ +/* + * ** 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.jobs; + +/** + * ScheduledJob is a wrapper class for jobs held by the Scheduler. + * + * <p>It is mainly a means to have simplified interface for the Scheduler which also measures the + * execution time of the job "from outside". + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class ScheduledJob { + + long runtime = 0; + final Job job; + private long startTime = -1; + + ScheduledJob(final Job j) { + this.job = j; + } + + void run() { + if (!hasStarted()) { + start(); + this.job.run(); + finish(); + } + } + + @Override + public String toString() { + return this.job.toString(); + } + + /** Does not actually start the job, but only sets the startTime. */ + private void start() { + this.startTime = System.currentTimeMillis(); + } + + /** Calculate and set the runtime, and add the measurement. */ + private void finish() { + this.runtime += System.currentTimeMillis() - this.startTime; + this.job + .getContainer() + .getTransactionBenchmark() + .addMeasurement(this.job.getClass().getSimpleName(), this.runtime); + } + + void pause() { + this.runtime += System.currentTimeMillis() - this.startTime; + } + + void unpause() { + start(); + } + + private boolean hasStarted() { + return this.startTime != -1; + } + + /** Return the state of the inner Job. */ + public TransactionStage getTransactionStage() { + return this.job.getTransactionStage(); + } + + public boolean skip() { + return this.job.getTarget().skipJob(); + } +} diff --git a/src/main/java/org/caosdb/server/jobs/TransactionStage.java b/src/main/java/org/caosdb/server/jobs/TransactionStage.java new file mode 100644 index 0000000000000000000000000000000000000000..8248140d913e08acfb699cbb553a14749c8e2f1f --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/TransactionStage.java @@ -0,0 +1,63 @@ +/* + * ** 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.jobs; + +import org.caosdb.server.jobs.core.Strict; +import org.caosdb.server.transaction.Transaction; +import org.caosdb.server.utils.UndoHandler; + +/** + * Any {@link Transaction} of an Entity consists of sequence of stages. Jobs which have a {@link + * JobAnnotation} can specify the transaction stage in which they are to be executed. By default, + * any job is executed during the CHECK stage. + * <li>INIT - The transaction is being initialized (create schedule, aquire locks (one writing + * thread, many reading threads permitted). + * <li>PRE_CHECK - Prepare entities (e.g. check if any updates are to be processed, load/generate + * acl, cast objects into more specialized classes.) + * <li>CHECK - Do the actual consistency checking. + * <li>POST_CHECK - Do more consistency checking (reserved for those jobs which need the normal + * checks to be done already, e.g. the {@link Strict} job). + * <li>PRE_TRANSACTION - Prepare the entities for the transaction (e.g. for paging, translate the + * state messages into state properties). Also, in this stage, the full read and write lock is + * aquired. + * <li>TRANSACTION - Do the actual transaction. + * <li>POST_TRANSACTION - Post-process entities (Success messages, write history, transform stage + * properties into messages). + * <li>CLEANUP - Release all locks, remove temporary files, clean-up the {@link UndoHandler}s. + * <li>ROLL_BACK - In case an error occured in any of the stages, this special stage rolls-back any + * changes and calls the {@link UndoHandler#undo()} of the {@link UndoHandler}s. + * + * @see {@link Transaction#execute()}. + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public enum TransactionStage { + INIT, + PRE_CHECK, + CHECK, + POST_CHECK, + PRE_TRANSACTION, + TRANSACTION, + POST_TRANSACTION, + CLEANUP, + ROLL_BACK +} diff --git a/src/main/java/org/caosdb/server/jobs/core/AccessControl.java b/src/main/java/org/caosdb/server/jobs/core/AccessControl.java index d1a2113a2f87018cec63d381cce64ad6c8ad09f2..408384f416e1249501d5383ea1028ced56914ded 100644 --- a/src/main/java/org/caosdb/server/jobs/core/AccessControl.java +++ b/src/main/java/org/caosdb/server/jobs/core/AccessControl.java @@ -29,12 +29,12 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.jobs.ContainerJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.transaction.Retrieve; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; -@JobAnnotation(time = JobExecutionTime.INIT) +@JobAnnotation(stage = TransactionStage.INIT) public class AccessControl extends ContainerJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/Atomic.java b/src/main/java/org/caosdb/server/jobs/core/Atomic.java index 509f87fc8515ca08daa1a5cb8ef6571dfe6fa441..8639615ee6ac4ddd648a19d15262a1c526ef10c6 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Atomic.java +++ b/src/main/java/org/caosdb/server/jobs/core/Atomic.java @@ -26,17 +26,18 @@ import org.caosdb.server.entity.Entity; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.ContainerJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.transaction.WriteTransaction; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; @JobAnnotation( - time = JobExecutionTime.POST_CHECK, + stage = TransactionStage.PRE_TRANSACTION, transaction = WriteTransaction.class, loadAlways = true) -public class Atomic extends ContainerJob { +public class Atomic extends ContainerJob implements Observer { private boolean doCheck() { if (getContainer().getStatus() == EntityStatus.QUALIFIED) { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java index ce9943ecbefb4d669003a097e74bf9baff078167..f46013d278f268b95b1b105130e1eabd74f3a9f4 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java @@ -33,6 +33,7 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Role; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.jobs.Job; +import org.caosdb.server.jobs.ScheduledJob; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -81,8 +82,8 @@ public final class CheckDatatypePresent extends EntityJob { // run jobsreturn this.entities; final List<Job> datatypeJobs = loadDataTypeSpecificJobs(); - getTransaction().getSchedule().addAll(datatypeJobs); - for (final Job job : datatypeJobs) { + List<ScheduledJob> scheduledJobs = getTransaction().getSchedule().addAll(datatypeJobs); + for (final ScheduledJob job : scheduledJobs) { getTransaction().getSchedule().runJob(job); } } else { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java index c6d10a8f4f7cd0656f571a4cbf70385ab9cb7725..e663874189ee0301a5b30452468391711624b606 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java @@ -32,6 +32,8 @@ import org.caosdb.server.entity.Role; import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -41,6 +43,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ +@JobAnnotation(stage = TransactionStage.PRE_CHECK) public class CheckParValid extends EntityJob { @Override public final void run() { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java index 10ce6295a5b4910084d769278ca6348287e6223a..390deedde211c0931eca1c3677ac5ff9c8ee9d8f 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java @@ -31,6 +31,8 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -40,6 +42,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ +@JobAnnotation(stage = TransactionStage.PRE_CHECK) public class CheckPropValid extends EntityJob { @Override public final void run() { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java index 59fcfe1d877ca61c98674ee85919f03b7bd4278e..b9f804f519948b605a9fa8820c91495618463fba 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java @@ -37,6 +37,7 @@ import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; /** @@ -45,7 +46,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ -public class CheckRefidIsaParRefid extends EntityJob { +public class CheckRefidIsaParRefid extends EntityJob implements Observer { private void doJob() { try { 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 c9d06cb5487ff62a276ff1028c22864afa850562..8563ea716072602684dc1a78870392d573dc3a22 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java @@ -36,6 +36,7 @@ import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; /** @@ -43,7 +44,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ -public class CheckRefidValid extends EntityJob { +public class CheckRefidValid extends EntityJob implements Observer { @Override public final void run() { try { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java new file mode 100644 index 0000000000000000000000000000000000000000..d82a8a5be4baf265e6af263391751a68458a91f4 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java @@ -0,0 +1,200 @@ +/* + * ** 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.jobs.core; + +import java.util.Map; +import org.apache.shiro.authz.AuthorizationException; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.UpdateEntity; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; +import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.utils.ServerMessages; + +/** + * Check if the attempted state transition is allowed. + * + * <p>This job checks if the attempted state transition is in compliance with the state model. This + * job runs during the CHECK phase and should do all necessary consistency and permission checks. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@JobAnnotation(stage = TransactionStage.POST_CHECK, transaction = WriteTransaction.class) +public class CheckStateTransition extends EntityStateJob { + + private static final String PERMISSION_STATE_FORCE_FINAL = "STATE:FORCE:FINAL"; + private static final String PERMISSION_STATE_UNASSIGN = "STATE:UNASSIGN:"; + private static final String PERMISSION_STATE_ASSIGN = "STATE:ASSIGN:"; + private static final Message TRANSITION_NOT_ALLOWED = + new Message(MessageType.Error, "Transition not allowed."); + private static final Message INITIAL_STATE_NOT_ALLOWED = + new Message(MessageType.Error, "Initial state not allowed."); + private static final Message FINAL_STATE_NOT_ALLOWED = + new Message(MessageType.Error, "Final state not allowed."); + + /** + * The forceFinalState flag is especially useful if you want to delete entities in the middle of + * the state machine's usual state cycle. + */ + private static final String FLAG_FORCE_FINAL_STATE = "forceFinalState"; + + @Override + protected void run() { + try { + State newState = getState(); + if (newState != null) { + checkStateValid(newState); + } + if (getEntity() instanceof UpdateEntity) { + State oldState = getState(((UpdateEntity) getEntity()).getOriginal()); + checkStateTransition(oldState, newState); + } else if (getEntity() instanceof DeleteEntity) { + if (newState != null) checkFinalState(newState); + } else { // fresh Entity + if (newState != null) checkInitialState(newState); + } + } catch (Message m) { + getEntity().addError(m); + } catch (AuthorizationException e) { + getEntity().addError(ServerMessages.AUTHORIZATION_ERROR); + getEntity().addInfo(e.getMessage()); + } + } + + /** + * Check if the state belongs to the state model. + * + * <p>In practical terms, throw a Message if the state is invalid. + * + * @param state + * @throws Message + */ + private void checkStateValid(State state) throws Message { + if (state.isFinal() || state.isInitial() || state.getStateModel().getStates().contains(state)) { + return; + } + throw STATE_NOT_IN_STATE_MODEL; + } + + /** + * Check if state is valid and transition is allowed. + * + * <p>Especially, transitions between {@code null} states are allowed, non-trivial transitions + * from or to {@code null} must be initial or final states, respectively ({@link + * FORCE_FINAL_STATE} exception applies). + * + * @param oldState + * @param newState + * @throws Message if not + */ + private void checkStateTransition(State oldState, State newState) throws Message { + if (oldState == null && newState == null) { + return; + } else if (oldState == null && newState != null) { + checkInitialState(newState); + return; + } else if (newState == null && oldState != null) { + checkFinalState(oldState); + return; + } + + StateModel stateModel = findMatchingStateModel(oldState, newState); + if (stateModel == null) { + // change from one stateModel to another + checkInitialState(newState); + checkFinalState(oldState); + return; + } + + boolean transition_defined = false; + for (Transition t : stateModel.getTransitions()) { + if (t.isFromState(oldState) && t.isToState(newState)) { + transition_defined = true; + if (t.isPermitted(getUser())) { + return; + } + } + } + if (transition_defined) { + throw new AuthorizationException( + getUser().getPrincipal().toString() + + " doesn't have permission to perform this transition."); + } + throw TRANSITION_NOT_ALLOWED; + } + + /** + * Return the two State's common StateModel, or {@code null} if they don't have one in common. + * + * @param oldState + * @param newState + * @return the state model which contains both of the states. + * @throws Message if the state model of one of the states cannot be constructed. + */ + private StateModel findMatchingStateModel(State oldState, State newState) throws Message { + if (oldState.getStateModel().equals(newState.getStateModel())) { + return oldState.getStateModel(); + } + return null; + } + + /** + * Check if the old state is final or if the {@link FORCE_FINAL_STATE} flag is true. + * + * @param oldState + * @throws Message if the state is not final. + */ + private void checkFinalState(State oldState) throws Message { + if (!oldState.isFinal()) { + if (isForceFinal()) { + getUser().checkPermission(PERMISSION_STATE_FORCE_FINAL); + } else { + throw FINAL_STATE_NOT_ALLOWED; + } + } + getUser().checkPermission(PERMISSION_STATE_UNASSIGN + oldState.getStateModelName()); + } + + /** + * Check if the new state is an initial state. + * + * @param newState + * @throws Message if not + */ + private void checkInitialState(State newState) throws Message { + if (!newState.isInitial()) { + throw INITIAL_STATE_NOT_ALLOWED; + } + getUser().checkPermission(PERMISSION_STATE_ASSIGN + newState.getStateModelName()); + } + + private boolean isForceFinal() { + Map<String, String> containerFlags = getTransaction().getContainer().getFlags(); + return (containerFlags != null + && "true".equalsIgnoreCase(containerFlags.get(FLAG_FORCE_FINAL_STATE))) + || "true".equalsIgnoreCase(getEntity().getFlag(FLAG_FORCE_FINAL_STATE)); + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java b/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java index 34e1960961f72118567653a77e4f48fe357dfedc..1dd457143f8cc23c0136f2c51cb40edf8552ba15 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java @@ -28,6 +28,7 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; /** * Check whether the value of an entity is parsable according to the entity's data type. This job @@ -37,7 +38,7 @@ import org.caosdb.server.utils.Observable; * * @author tf */ -public class CheckValueParsable extends EntityJob { +public class CheckValueParsable extends EntityJob implements Observer { @Override public final void run() { @@ -96,7 +97,6 @@ public class CheckValueParsable extends EntityJob { // Therefore, not the whole test has to be run again. if (getEntity().hasDatatype()) { parseValue(); - super.notifyObservers(e); return false; } } diff --git a/src/main/java/org/caosdb/server/jobs/core/DebugCalls.java b/src/main/java/org/caosdb/server/jobs/core/DebugCalls.java index 8527560d02e498bf3812162a8b01b9641aad7260..8e0ae7435693ba1010f023da6c585911825f5a5e 100644 --- a/src/main/java/org/caosdb/server/jobs/core/DebugCalls.java +++ b/src/main/java/org/caosdb/server/jobs/core/DebugCalls.java @@ -25,11 +25,11 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.CaosDBServer; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; @JobAnnotation( flag = "debug", - time = JobExecutionTime.INIT, + stage = TransactionStage.INIT, values = {DebugCalls.ex_null_p}, description = "This job for debugging and is available in debug mode only. Otherwise the flag is ignored. It throws an exception on purpose.", diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java new file mode 100644 index 0000000000000000000000000000000000000000..c20b18b6e4950c07967d2cfcdd776c95c25f4a53 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java @@ -0,0 +1,857 @@ +/* + * ** 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.jobs.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import org.apache.shiro.subject.Subject; +import org.caosdb.server.database.exceptions.EntityDoesNotExistException; +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.ReferenceDatatype2; +import org.caosdb.server.datatype.ReferenceValue; +import org.caosdb.server.datatype.TextDatatype; +import org.caosdb.server.entity.ClientMessage; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.StatementStatus; +import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.entity.xml.ToElementable; +import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.permissions.EntityACI; +import org.caosdb.server.permissions.EntityACL; +import org.caosdb.server.query.Query; +import org.caosdb.server.utils.EntityStatus; +import org.jdom2.Element; + +/** + * The EntityStateJob is the abstract base class for four EntityJobs: + * + * <p>1. The {@link InitEntityState} job reads ClientMessages or StateProperties with tag state and + * converts them into instances of State. This job runs during WriteTransactions. This job runs + * during the INIT Phase and does not perform any checks other than those necessary for the + * conversion. + * + * <p>2. The {@link CheckStateTransition} job checks if the attempted state transition is in + * compliance with the state model. This job runs during the CHECK phase and should do all necessary + * consistency and permission checks. + * + * <p>3. The {@link MakeStateProperty} job constructs an ordinary Property from the State right + * before the entity is being written to the back-end and after any checks run. + * + * <p>4. The {@link MakeStateMessage} job converts a state property (back) into State messages and + * appends them to the entity. + * + * <p>Only the 4th job ({@link MakeStateMessage}) runs during Retrieve transitions. During + * WriteTransactions all four jobs do run. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public abstract class EntityStateJob extends EntityJob { + + protected static final String SERVER_PROPERTY_EXT_ENTITY_STATE = "EXT_ENTITY_STATE"; + + public static final String TO_STATE_PROPERTY_NAME = "to"; + public static final String FROM_STATE_PROPERTY_NAME = "from"; + public static final String FINAL_STATE_PROPERTY_NAME = "final"; + public static final String INITIAL_STATE_PROPERTY_NAME = "initial"; + public static final String STATE_RECORD_TYPE_NAME = "State"; + public static final String STATE_MODEL_RECORD_TYPE_NAME = "StateModel"; + public static final String TRANSITION_RECORD_TYPE_NAME = "Transition"; + public static final String TRANSITION_XML_TAG = "Transition"; + public static final String TRANSITION_ATTRIBUTE_NAME = "name"; + public static final String TRANSITION_ATTRIBUTE_DESCRIPTION = "description"; + public static final String TO_XML_TAG = "ToState"; + public static final String FROM_XML_TAG = "FromState"; + public static final String STATE_XML_TAG = "State"; + public static final String STATE_ATTRIBUTE_MODEL = "model"; + public static final String STATE_ATTRIBUTE_NAME = "name"; + public static final String STATE_ATTRIBUTE_DESCRIPTION = "description"; + public static final String STATE_ATTRIBUTE_ID = "id"; + public static final String ENTITY_STATE_ROLE_MARKER = "?STATE?"; + public static final String PERMISSION_STATE_TRANSION = "STATE:TRANSITION:"; + + public static final Message STATE_MODEL_NOT_FOUND = + new Message(MessageType.Error, "StateModel not found."); + public static final Message STATE_NOT_IN_STATE_MODEL = + new Message(MessageType.Error, "State does not exist in this StateModel."); + public static final Message COULD_NOT_CONSTRUCT_STATE_MESSAGE = + new Message(MessageType.Error, "Could not construct the state message."); + public static final Message COULD_NOT_CONSTRUCT_TRANSITIONS = + new Message(MessageType.Error, "Could not construct the transitions."); + public static final Message STATE_MODEL_NOT_SPECIFIED = + new Message(MessageType.Error, "State model not specified."); + public static final Message STATE_NOT_SPECIFIED = + new Message(MessageType.Error, "State not specified."); + + /** + * Represents a Transition which is identified by a name and the two States from and to which an + * entity is being transitioned. + * + * <p>Currently, only exactly one toState and one fromState can be defined. However, it might be + * allowed in the future to have multiple states here. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ + public class Transition { + + private String name; + private String description; + private State fromState; + private State toState; + private Map<String, String> transitionProperties; + + /** + * @param transition The transition Entity, from which the Transition is created. Relevant + * Properties are "to" and "from" + */ + public Transition(EntityInterface transition) throws Message { + this.name = transition.getName(); + this.description = transition.getDescription(); + this.fromState = getFromState(transition); + this.toState = getToState(transition); + this.transitionProperties = getTransitionProperties(transition); + } + + private Map<String, String> getTransitionProperties(EntityInterface transition) { + Map<String, String> result = new LinkedHashMap<>(); + for (Property p : transition.getProperties()) { + if (p.getDatatype() instanceof TextDatatype) { + result.put(p.getName(), p.getValue().toString()); + } + } + return result; + } + + private State getToState(EntityInterface transition) throws Message { + for (Property p : transition.getProperties()) { + if (p.getName().equals(TO_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + private State getFromState(EntityInterface transition) throws Message { + for (Property p : transition.getProperties()) { + if (p.getName().equals(FROM_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + /** + * @param previousState + * @return true iff the previous state is a fromState of this transition. + */ + public boolean isFromState(State previousState) { + return this.fromState.equals(previousState); + } + + /** + * @param nextState + * @return true iff the next state is a toState of this transition. + */ + public boolean isToState(State nextState) { + return this.toState.equals(nextState); + } + + public State getToState() { + return this.toState; + } + + public State getFromState() { + return this.fromState; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Transition) { + Transition that = (Transition) obj; + return Objects.equals(this.getName(), that.getName()) + && Objects.equals(this.getFromState(), that.getFromState()) + && Objects.equals(this.getToState(), that.getToState()); + } + return false; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return "Transition (name=" + + getName() + + ", from=" + + getFromState().getStateName() + + ", to=" + + getToState().getStateName() + + ")"; + } + + public Element toElement() { + Element result = new Element(TRANSITION_XML_TAG); + if (this.transitionProperties != null) { + this.transitionProperties.forEach( + (String key, String value) -> { + result.setAttribute(key, value); + }); + } + if (this.name != null) { + result.setAttribute(TRANSITION_ATTRIBUTE_NAME, this.name); + } + if (this.description != null) { + result.setAttribute(TRANSITION_ATTRIBUTE_DESCRIPTION, this.description); + } + Element to = new Element(TO_XML_TAG); + to.setAttribute(STATE_ATTRIBUTE_NAME, this.toState.stateName); + if (this.toState.stateDescription != null) { + to.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.toState.stateDescription); + } + Element from = new Element(FROM_XML_TAG); + from.setAttribute(STATE_ATTRIBUTE_NAME, this.fromState.stateName); + return result.addContent(from).addContent(to); + } + + public boolean isPermitted(Subject user) { + return user.isPermitted(PERMISSION_STATE_TRANSION + this.name); + } + } + + /** + * The State instance represents a single entity state. This class is used for concrete states + * (the state of a stateful entity, say a Record) and abstract states (states which are part of a + * {@link StateModel}). + * + * <p>States are identified via their name and the name of the model to which they belong. + * + * <p>States are represented by Records with the state's name as the Record name. They belong to a + * StateModel iff the StateModel RecordType references the State Record. Each State should only + * belong to one StateModel. + * + * <p>Furthermore, States are the start or end point of {@link Transition Transitions} which + * belong to the same StateModel. Each State can be part of several transitions at the same time. + * + * <p>Note: The purpose of this should not be confused with {@link EntityStatus} which is purely + * for internal use. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ + public class State implements ToElementable { + + private String stateModelName = null; + private String stateName = null; + private EntityInterface stateEntity = null; + private EntityInterface stateModelEntity = null; + private StateModel stateModel; + private String stateDescription = null; + private Integer stateId = null; + private EntityACL stateACL = null; + private Map<String, String> stateProperties; + + public State(EntityInterface stateEntity, EntityInterface stateModelEntity) throws Message { + this.stateEntity = stateEntity; + this.stateDescription = stateEntity.getDescription(); + this.stateId = stateEntity.getId(); + this.stateName = stateEntity.getName(); + this.stateModelEntity = stateModelEntity; + this.stateModelName = stateModelEntity.getName(); + this.stateACL = createStateACL(stateEntity.getEntityACL()); + this.stateProperties = createStateProperties(stateEntity); + } + + private Map<String, String> createStateProperties(EntityInterface stateEntity) { + Map<String, String> result = new LinkedHashMap<>(); + for (Property p : stateEntity.getProperties()) { + if (p.getDatatype() instanceof TextDatatype) { + result.put(p.getName(), p.getValue().toString()); + } + } + return result; + } + + private EntityACL createStateACL(EntityACL entityACL) { + LinkedList<EntityACI> rules = new LinkedList<>(); + for (EntityACI aci : entityACL.getRules()) { + if (aci.getResponsibleAgent().toString().startsWith(ENTITY_STATE_ROLE_MARKER)) { + int end = aci.getResponsibleAgent().toString().length() - 1; + String role = aci.getResponsibleAgent().toString().substring(7, end); + rules.add( + new EntityACI(org.caosdb.server.permissions.Role.create(role), aci.getBitSet())); + } + } + return new EntityACL(rules); + } + + public EntityACL getStateACL() { + return this.stateACL; + } + + public String getStateDescription() throws Message { + return this.stateDescription; + } + + public Integer getStateId() throws Message { + return this.stateId; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof State) { + State that = (State) obj; + return Objects.equals(that.getStateName(), this.getStateName()) + && Objects.equals(that.getStateModelName(), this.getStateModelName()); + } + return false; + } + + @Override + public int hashCode() { + return 21364234 + this.getStateName().hashCode() + this.getStateModelName().hashCode(); + } + + /** + * Serialize this State into XML. + * + * <p>The result looks approximately like this: {@code <State name="My name" model="Model's + * name"/>} + */ + @Override + public void addToElement(Element ret) { + Element e = new Element(STATE_XML_TAG); + if (this.stateProperties != null) { + this.stateProperties.forEach( + (String key, String value) -> { + e.setAttribute(key, value); + }); + } + if (this.stateModelName != null) { + e.setAttribute(STATE_ATTRIBUTE_MODEL, this.stateModelName); + } + if (this.stateName != null) { + e.setAttribute(STATE_ATTRIBUTE_NAME, this.stateName); + } + if (this.stateDescription != null) { + e.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.stateDescription); + } + if (this.stateId != null) { + e.setAttribute(STATE_ATTRIBUTE_ID, Integer.toString(this.stateId)); + } + if (this.stateModel != null) { + this.stateModel.transitions.forEach( + (Transition t) -> { + if (t.isFromState(this) && t.isPermitted(getUser())) { + e.addContent(t.toElement()); + } + }); + } + ret.addContent(e); + } + + public String getStateModelName() { + return this.stateModelName; + } + + private String getStateName() { + return this.stateName; + } + + public StateModel getStateModel() throws Message { + if (this.stateModel == null) { + this.stateModel = new StateModel(this.stateModelEntity); + } + return this.stateModel; + } + + /** + * @return true iff this state is an initial state of its StateModel. + * @throws Message + */ + public boolean isInitial() throws Message { + return Objects.equals(this, getStateModel().initialState); + } + + /** + * @return true iff this state is a final state of its StateModel. + * @throws Message + */ + public boolean isFinal() throws Message { + return Objects.equals(this, getStateModel().finalState); + } + + /** + * Create a Property which represents the current entity state of a stateful entity. + * + * @return stateProperty + * @throws Message + */ + public Property createStateProperty() throws Message { + EntityInterface stateRecordType = getStateRecordType(); + Property stateProperty = new Property(stateRecordType); + stateProperty.setDatatype(new ReferenceDatatype2(stateRecordType)); + stateProperty.setValue(new ReferenceValue(getStateEntity(), false)); + stateProperty.setStatementStatus(StatementStatus.FIX); + return stateProperty; + } + + public EntityInterface getStateEntity() { + return this.stateEntity; + } + + public EntityInterface getStateModelEntity() { + return this.stateModelEntity; + } + + @Override + public String toString() { + String isInitial = null; + String isFinal = null; + try { + isInitial = String.valueOf(isInitial()); + } catch (Message e) { + isInitial = "null"; + } + try { + isFinal = String.valueOf(isFinal()); + } catch (Message e) { + isFinal = "null"; + } + return "State (name=" + + getStateName() + + ", model=" + + getStateModelName() + + ", initial=" + + isInitial + + ", final=" + + isFinal + + ")"; + } + } + + /** + * A StateModel is an abstract definition of a Finite State Machine for entities. + * + * <p>It consists of a set of States, a set of transitions, a initial state and a final state. + * + * <p>If the StateModel has no initial state, it cannot be initialized (no entity will ever be in + * any of the StateModel's states) without using the forceInitialState flag. + * + * <p>If the StateModel has not final state, an entity with any of the states from this StateModel + * cannot leave this StateModel (and cannot be deleted either) without using the forceFinalState + * flag. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ + public class StateModel { + + private String name; + private Set<State> states; + private Set<Transition> transitions; + private State initialState; + private State finalState; + + public StateModel(EntityInterface stateModelEntity) throws Message { + this.name = stateModelEntity.getName(); + this.transitions = getTransitions(stateModelEntity); + this.states = getStates(transitions, this); + this.finalState = getFinalState(stateModelEntity); + this.initialState = getInitialState(stateModelEntity); + } + + private State getInitialState(EntityInterface stateModelEntity) throws Message { + // TODO maybe check if there is more than one "initial" Property? + for (Property p : stateModelEntity.getProperties()) { + if (p.getName().equals(INITIAL_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + private State getFinalState(EntityInterface stateModelEntity) throws Message { + // TODO maybe check if there is more than one "final" Property? + for (Property p : stateModelEntity.getProperties()) { + if (p.getName().equals(FINAL_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + /** Transitions are taken from list Property with name="Transition". */ + private Set<Transition> getTransitions(EntityInterface stateModelEntity) throws Message { + for (Property p : stateModelEntity.getProperties()) { + if (p.getName().equals(TRANSITION_RECORD_TYPE_NAME)) { + return createTransitions(p); + } + } + return null; + } + + /** + * Read out the "Transition" property and create Transition instances. + * + * @param p the transition property + * @return a set of transitions + * @throws Message if the transitions could ne be created. + */ + private Set<Transition> createTransitions(Property p) throws Message { + Set<Transition> result = new LinkedHashSet<>(); + try { + if (!(p.getDatatype() instanceof AbstractCollectionDatatype)) { + // FIXME raise an exception instead? + return result; + } + p.parseValue(); + CollectionValue vals = (CollectionValue) p.getValue(); + for (IndexedSingleValue val : vals) { + if (val.getWrapped() instanceof ReferenceValue) { + Integer refid = ((ReferenceValue) val.getWrapped()).getId(); + + String key = "transition" + Integer.toString(refid); + EntityInterface transition = getCached(key); + if (transition == null) { + transition = retrieveValidEntity(refid); + putCache(key, transition); + } + result.add(new Transition(transition)); + } + } + } catch (Exception e) { + throw COULD_NOT_CONSTRUCT_TRANSITIONS; + } + return result; + } + + /** + * Collect all possible states from the set of transitions. + * + * <p>This function does not perform any consistency checks. It only add all toStates and + * fromStates of the transitions to the result. + * + * @param transitions + * @param stateModel + * @return set of states. + * @throws Message + */ + private Set<State> getStates(Set<Transition> transitions, StateModel stateModel) + throws Message { + // TODO Move outside of this class + Iterator<Transition> it = transitions.iterator(); + Set<State> result = new LinkedHashSet<>(); + while (it.hasNext()) { + Transition t = it.next(); + result.add(t.getFromState()); + result.add(t.getToState()); + } + return result; + } + + public String getName() { + return this.name; + } + + public Set<State> getStates() { + return this.states; + } + + public Set<Transition> getTransitions() { + return this.transitions; + } + + public State getFinalState() { + return this.finalState; + } + + public State getInitialState() { + return this.initialState; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StateModel) { + return ((StateModel) obj).getName().equals(this.getName()); + } + return false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("StateModel (name="); + sb.append(this.getName()); + sb.append(", initial="); + sb.append(this.getInitialState().stateName); + sb.append(", final="); + sb.append(this.getFinalState().stateName); + sb.append(", transitions=["); + Iterator<Transition> iterator = this.transitions.iterator(); + while (iterator.hasNext()) { + sb.append(iterator.next().name); + sb.append(" -> "); + sb.append(iterator.next().name); + sb.append(", "); + } + sb.append("])"); + return sb.toString(); + } + } + + private EntityInterface retrieveStateEntity(String stateName) throws Message { + try { + return retrieveValidEntity(retrieveValidIDByName(stateName)); + } catch (EntityDoesNotExistException e) { + throw STATE_NOT_IN_STATE_MODEL; + } + } + + private EntityInterface retrieveStateModelEntity(String stateModel) throws Message { + try { + return retrieveValidEntity(retrieveValidIDByName(stateModel)); + } catch (EntityDoesNotExistException e) { + throw STATE_MODEL_NOT_FOUND; + } + } + + protected EntityInterface getStateRecordType() throws Message { + EntityInterface stateRecordType = getCached(STATE_RECORD_TYPE_NAME); + if (stateRecordType == null) { + stateRecordType = retrieveValidSparseEntityByName(STATE_RECORD_TYPE_NAME); + putCache(STATE_RECORD_TYPE_NAME, stateRecordType); + } + return stateRecordType; + } + + protected State getState() { + return getState(false); + } + + protected State getState(EntityInterface entity) { + return getState(entity, false); + } + + protected State getState(EntityInterface entity, boolean remove) { + Iterator<ToElementable> messages = entity.getMessages().iterator(); + while (messages.hasNext()) { + ToElementable s = messages.next(); + if (s instanceof State) { + if (remove) { + messages.remove(); + } + return (State) s; + } + } + return null; + } + + protected State getState(boolean remove) { + return getState(getEntity(), remove); + } + + /** Return (and possibly remove) the States Properties of `entity`. */ + protected List<Property> getStateProperties(EntityInterface entity, boolean remove) { + Iterator<Property> it = entity.getProperties().iterator(); + List<Property> result = new ArrayList<>(); + while (it.hasNext()) { + Property p = it.next(); + if (Objects.equals(p.getName(), STATE_RECORD_TYPE_NAME)) { + if (!(p.getDatatype() instanceof ReferenceDatatype)) { + continue; + } + if (remove) { + it.remove(); + } + result.add(p); + } + } + return result; + } + + protected List<Property> getStateProperties(boolean remove) { + return getStateProperties(getEntity(), remove); + } + + /** Get the {@code ClientMessage}s which denote a state. */ + protected List<ClientMessage> getStateClientMessages(EntityInterface entity, boolean remove) { + Iterator<ToElementable> stateMessages = entity.getMessages().iterator(); + List<ClientMessage> result = new ArrayList<>(); + while (stateMessages.hasNext()) { + ToElementable s = stateMessages.next(); + if (s instanceof ClientMessage && STATE_XML_TAG.equals(((ClientMessage) s).getType())) { + if (remove) { + stateMessages.remove(); + } + result.add((ClientMessage) s); + } + } + return result; + } + + protected List<ClientMessage> getStateClientMessages(boolean remove) { + return getStateClientMessages(getEntity(), remove); + } + + protected State createState(ClientMessage s) throws Message { + String stateModel = s.getProperty(STATE_ATTRIBUTE_MODEL); + if (stateModel == null) { + throw STATE_MODEL_NOT_SPECIFIED; + } + String stateName = s.getProperty(STATE_ATTRIBUTE_NAME); + if (stateName == null) { + throw STATE_NOT_SPECIFIED; + } + String stateModelKey = "statemodel:" + stateModel; + + EntityInterface stateModelEntity = getCached(stateModelKey); + if (stateModelEntity == null) { + stateModelEntity = retrieveStateModelEntity(stateModel); + putCache(stateModelKey, stateModelEntity); + } + + String stateKey = "namestate:" + stateName; + + EntityInterface stateEntity = getCached(stateKey); + if (stateEntity == null) { + stateEntity = retrieveStateEntity(stateName); + putCache(stateKey, stateEntity); + } + return new State(stateEntity, stateModelEntity); + } + + /** + * Create a State instance from the value of the state property. + * + * <p>This method also retrieves the state entity from the back-end. The StateModel is deduced + * from finding an appropriately referencing StateModel Record. + * + * @param p the entity's state property + * @return The state of the entity + * @throws Message + */ + protected State createState(Property p) throws Message { + try { + p.parseValue(); + ReferenceValue refid = (ReferenceValue) p.getValue(); + String key = "idstate" + Integer.toString(refid.getId()); + + EntityInterface stateEntity = getCached(key); + if (stateEntity == null) { + stateEntity = retrieveValidEntity(refid.getId()); + putCache(key, stateEntity); + } + + EntityInterface stateModelEntity = findStateModel(stateEntity); + return new State(stateEntity, stateModelEntity); + } catch (Message e) { + throw e; + } catch (Exception e) { + throw COULD_NOT_CONSTRUCT_STATE_MESSAGE; + } + } + + private static final Map<String, EntityInterface> cache = new HashMap<>(); + private static final Set<Integer> id_in_cache = new HashSet<>(); + + EntityInterface findStateModel(EntityInterface stateEntity) throws Exception { + boolean cached = true; + String key = "modelof" + Integer.toString(stateEntity.getId()); + + EntityInterface result = getCached(key); + if (result != null && cached) { + return result; + } + // TODO This should throw a meaningful Exception if no matching StateModel can be found. + TransactionContainer c = new TransactionContainer(); + Query query = + new Query( + "FIND RECORD " + + STATE_MODEL_RECORD_TYPE_NAME + + " WHICH REFERENCES " + + TRANSITION_RECORD_TYPE_NAME + + " WHICH REFERENCES " + + Integer.toString(stateEntity.getId()), + getUser(), + c); + query.execute(getTransaction().getAccess()); + result = retrieveValidEntity(c.get(0).getId()); + putCache(key, result); + return result; + } + + private EntityInterface getCached(String key) { + EntityInterface result; + synchronized (cache) { + result = cache.get(key); + } + return result; + } + + private void putCache(String key, EntityInterface value) { + synchronized (cache) { + if (value instanceof DeleteEntity) { + throw new RuntimeException("Delete entity in cache. This is an implementation error."); + } + id_in_cache.add(value.getId()); + cache.put(key, value); + } + } + + protected void removeCached(EntityInterface entity) { + synchronized (cache) { + if (id_in_cache.contains(entity.getId())) { + id_in_cache.remove(entity.getId()); + + List<String> remove = new LinkedList<>(); + for (Entry<String, EntityInterface> entry : cache.entrySet()) { + if (entry.getValue().getId().equals(entity.getId())) { + remove.add(entry.getKey()); + } + } + for (String key : remove) { + cache.remove(key); + } + } + } + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java index b6583f024071e3c1151cf974b387ffe72d1579b7..399039a072d2fd7b5f54566b2f448791169a356b 100644 --- a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java +++ b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java @@ -22,16 +22,17 @@ */ package org.caosdb.server.jobs.core; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.query.Query; import org.caosdb.server.query.Query.ParsingException; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; -@JobAnnotation(flag = "query", time = JobExecutionTime.INIT) +@JobAnnotation(flag = "query", stage = TransactionStage.INIT) public class ExecuteQuery extends FlagJob { @Override @@ -51,5 +52,8 @@ public class ExecuteQuery extends FlagJob { getContainer().addMessage(new Message(e.getMessage())); } getContainer().addMessage(queryInstance); + for (EntityInterface entity : getContainer()) { + getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction())); + } } } 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 5594b5afdb841436dbdd63f96b8e2c44faef51c6..d39a6e1f570400194b3a530c3c323a92d0374b7c 100644 --- a/src/main/java/org/caosdb/server/jobs/core/History.java +++ b/src/main/java/org/caosdb/server/jobs/core/History.java @@ -27,7 +27,7 @@ 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; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -37,7 +37,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author Timm Fitschen (t.fitschen@indiscale.com) */ -@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION, flag = "H") +@JobAnnotation(stage = TransactionStage.POST_TRANSACTION, flag = "H") public class History extends FlagJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java b/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java new file mode 100644 index 0000000000000000000000000000000000000000..39b66e35330a485bbd8eb4c40724ab3b250ccc45 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java @@ -0,0 +1,36 @@ +package org.caosdb.server.jobs.core; + +import java.util.List; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.wrapper.Parent; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; + +@JobAnnotation(stage = TransactionStage.CHECK) +public class InheritInitialState extends EntityStateJob { + + @Override + protected void run() { + try { + State parentState = getFirstParentState(); + if (parentState != null) { + getEntity().addMessage(parentState); + parentState.getStateEntity(); + getEntity().setEntityACL(parentState.getStateACL()); + } + } catch (Message e) { + getEntity().addError(e); + } + } + + private State getFirstParentState() throws Message { + for (Parent par : getEntity().getParents()) { + List<Property> stateProperties = getStateProperties(par, false); + if (stateProperties.size() > 0) { + return createState(stateProperties.get(0)); + } + } + return null; + } +} 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 d49bec09c909c5e884eae80619a5911a50b1f333..107c5768a5c15fbb05ea6d68b0cb956cbb235687 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java +++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java @@ -34,6 +34,7 @@ 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.jobs.ScheduledJob; import org.caosdb.server.utils.EntityStatus; /** @@ -65,7 +66,7 @@ public class Inheritance extends EntityJob { protected void run() { if (getEntity() instanceof InsertEntity || getEntity() instanceof UpdateEntity) { if (getEntity().hasParents()) { - final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>(); + final ArrayList<Property> transfer = new ArrayList<>(); parentLoop: for (final EntityInterface parent : getEntity().getParents()) { try { @@ -100,23 +101,25 @@ public class Inheritance extends EntityJob { // transfer properties if they are not implemented yet outerLoop: - for (final EntityInterface prop : transfer) { - for (final EntityInterface eprop : getEntity().getProperties()) { + for (final Property prop : transfer) { + for (final Property eprop : getEntity().getProperties()) { if (prop.hasId() && eprop.hasId() && prop.getId().equals(eprop.getId())) { continue outerLoop; } } // prop's Datatype might need to be resolved. - this.appendJob(prop, CheckDatatypePresent.class); - getEntity().addProperty(new Property(prop)); + ScheduledJob job = this.appendJob(prop, CheckDatatypePresent.class); + getTransaction().getSchedule().runJob(job); + + getEntity().addProperty(new Property(prop.getWrapped())); } } // implement properties if (getEntity().hasProperties()) { propertyLoop: - for (final EntityInterface property : getEntity().getProperties()) { - final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>(); + for (final Property property : getEntity().getProperties()) { + final ArrayList<Property> transfer = new ArrayList<>(); try { if (property.getFlags().get("inheritance") == null) { break propertyLoop; @@ -156,15 +159,17 @@ public class Inheritance extends EntityJob { // transfer properties if they are not implemented yet outerLoop: - for (final EntityInterface prop : transfer) { - for (final EntityInterface eprop : property.getProperties()) { + for (final Property prop : transfer) { + for (final Property eprop : property.getProperties()) { if (prop.hasId() && eprop.hasId() && prop.getId() == eprop.getId()) { continue outerLoop; } } // prop's Datatype might need to be resolved. - this.appendJob(prop, CheckDatatypePresent.class); - property.addProperty(new Property(prop)); + ScheduledJob job = this.appendJob(prop, CheckDatatypePresent.class); + getTransaction().getSchedule().runJob(job); + + property.addProperty(new Property(prop.getWrapped())); } } } @@ -182,9 +187,9 @@ public class Inheritance extends EntityJob { * @param inheritance */ private void collectInheritedProperties( - List<EntityInterface> transfer, EntityInterface from, INHERITANCE_MODE inheritance) { + List<Property> transfer, EntityInterface from, INHERITANCE_MODE inheritance) { if (from.hasProperties()) { - for (final EntityInterface propProperty : from.getProperties()) { + for (final Property propProperty : from.getProperties()) { switch (inheritance) { // the following cases are ordered according to their importance level and use a // fall-through. diff --git a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java new file mode 100644 index 0000000000000000000000000000000000000000..d3a1fc636761298f1b0586a4036be09ced835021 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java @@ -0,0 +1,200 @@ +/* + * ** 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.jobs.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.caosdb.server.CaosDBServer; +import org.caosdb.server.entity.ClientMessage; +import org.caosdb.server.entity.DeleteEntity; +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.Role; +import org.caosdb.server.entity.UpdateEntity; +import org.caosdb.server.entity.WritableEntity; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; +import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.utils.EntityStatus; +import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; + +/** + * Initialize the other entity jobs by converting the client message with type "State" or + * StateProperties into {@link State} instances. + * + * <p>This job also needs to initialize the other jobs even if the current entity version does not + * have a state anymore but the previous version had, because it has to be checked if the stateModel + * allows to leave in this state. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@JobAnnotation( + loadAlways = true, + stage = TransactionStage.INIT, + transaction = WriteTransaction.class) +public class InitEntityStateJobs extends EntityStateJob implements Observer { + + @Override + protected void run() { + if ("ENABLED".equals(CaosDBServer.getServerProperty(SERVER_PROPERTY_EXT_ENTITY_STATE))) { + State newState = handleNewState(); + State oldState = handleOldState(newState); + if (newState != null || oldState != null) { + if (!(getEntity() instanceof DeleteEntity)) { + + appendJob(MakeStateProperty.class); + } + appendJob(CheckStateTransition.class); + appendJob(MakeStateMessage.class); + } else if (newState == null + && getEntity().getRole() == Role.Record + && getEntity() instanceof InsertEntity) { + appendJob(InheritInitialState.class); + appendJob(CheckStateTransition.class); + appendJob(MakeStateProperty.class); + appendJob(MakeStateMessage.class); + } + if (getEntity() instanceof WritableEntity || getEntity() instanceof DeleteEntity) { + removeCached(getEntity()); + } + } + } + + /** + * Converts the state property of the original entity into a state message (only needed for + * updates). + * + * <p>Also, this method adds an observer to the entity state which handles a corner case where the + * entity changes the state, but no other property changes. In this case Update.deriveUpdate + * cannot detect any changes and will mark this entity as "to-be-skipped". The observer waits for + * that to happen and changes the {@EntityStatus} back to normal. + * + * @param newState + * @return The old state or null. + */ + private State handleOldState(State newState) { + State oldState = null; + try { + if (getEntity() instanceof UpdateEntity) { + List<State> states = initStateMessage(((UpdateEntity) getEntity()).getOriginal()); + oldState = null; + if (states.size() == 1) { + oldState = states.get(0); + if (newState != null) { + ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL()); + } + } + if (!Objects.equals(newState, oldState)) { + getEntity().acceptObserver(this); + } + } + } catch (Message m) { + getEntity().addWarning(STATE_ERROR_IN_ORIGINAL_ENTITY(m)); + } + return oldState; + } + + /** + * Converts the state property of this entity into a state message. + * + * @return The new state or null. + */ + private State handleNewState() { + State newState = null; + try { + List<State> states = initStateMessage(getEntity()); + if (states.size() > 1) { + throw new Message( + MessageType.Error, "Currently, each entity can only have one state at a time."); + } else if (states.size() == 1) { + newState = states.get(0); + if (getEntity().getRole() == Role.Record) { + transferEntityACL(getEntity(), newState); + } + } + } catch (Message m) { + getEntity().addError(m); + } + + return newState; + } + + private void transferEntityACL(EntityInterface entity, State newState) throws Message { + newState.getStateEntity(); + entity.setEntityACL(newState.getStateACL()); + } + + private static final Message STATE_ERROR_IN_ORIGINAL_ENTITY(Message m) { + return new Message( + MessageType.Warning, "State error in previous entity version\n" + m.getDescription()); + } + + /** + * Return a list of states from their representations as properties or client messages in the + * entity. + * + * @param entity + * @return list of state instances for the entity. + * @throws Message + */ + private List<State> initStateMessage(EntityInterface entity) throws Message { + List<ClientMessage> stateClientMessages = getStateClientMessages(entity, true); + List<State> result = new ArrayList<>(); + if (stateClientMessages != null) { + for (ClientMessage s : stateClientMessages) { + State stateMessage = createState(s); + entity.addMessage(stateMessage); + result.add(stateMessage); + } + } + List<Property> stateProperties = getStateProperties(entity, true); + if (stateProperties != null) { + for (Property p : stateProperties) { + State stateMessage = createState(p); + entity.addMessage(stateMessage); + result.add(stateMessage); + } + } + return result; + } + + @Override + public boolean notifyObserver(String e, Observable o) { + if (e == Entity.ENTITY_STATUS_CHANGED_EVENT) { + if (o == getEntity() && getEntity().getEntityStatus() == EntityStatus.VALID) { + // The Update.deriveUpdate method didn't recognize that the state is changing and set the + // entity to "VALID" + getEntity().setEntityStatus(EntityStatus.QUALIFIED); + return false; + } + } + return true; + } +} 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 35ce49297291accd41a7c293fc909cc0a27f9317..8133e4659d55caf29faa56eeb80ced194cd26cfc 100644 --- a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java +++ b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java @@ -46,7 +46,7 @@ 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.jobs.TransactionStage; import org.caosdb.server.transaction.Retrieve; import org.caosdb.server.transaction.WriteTransactionInterface; import org.caosdb.server.utils.EntityStatus; @@ -57,7 +57,7 @@ import org.caosdb.server.utils.Utils; @JobAnnotation( flag = "InsertFilesInDir", loadOnDefault = false, - time = JobExecutionTime.INIT, + stage = TransactionStage.INIT, description = "For expert users only! Risk of creating spam records!\nValue of this flag might be any directory on the servers local file system which is part of the server's back-end file storage. This job will insert every readable, nonhidden file in said directory into the database and link the file with a symlink. This is useful to add a huge amount of files without actully copying them to the back-end file storage. If you call this job on a directory more than once every file that was recently added to the source directory is inserted. Every yet known file is left untouched. \nOptional parameter -e EXCLUDE: A regular expression of files which are to be ignored. \n Optional parameter -i INCLUDE: a regular expression of files which are to be included. By default, all files are included. The -e takes precedence. \nOptional parameter -p PREFIX: Stores all new files into the directory PREFIX in the server's file system.\nOptional parameter --force-allow-symlinks: Simlinks in your data are a source of problems for the database. Therefore, simlinks are ignored by default. This option allows symlinks (but still generates simlink warnings). \nPrepend/Dry run: Call this flag with a retrieve transaction (HTTP GET) and it will only count all files and list them without actually inserting them.") public class InsertFilesInDir extends FlagJob { diff --git a/src/main/java/org/caosdb/server/jobs/core/LoadContainerFlagJobs.java b/src/main/java/org/caosdb/server/jobs/core/LoadContainerFlagJobs.java index 89f381d648144e274325c5c71a57c9f4daf04895..ef8df214c804fb1d55c3ed088c5f7f6e998aca07 100644 --- a/src/main/java/org/caosdb/server/jobs/core/LoadContainerFlagJobs.java +++ b/src/main/java/org/caosdb/server/jobs/core/LoadContainerFlagJobs.java @@ -27,9 +27,9 @@ import org.caosdb.server.jobs.ContainerJob; 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.jobs.TransactionStage; -@JobAnnotation(time = JobExecutionTime.INIT) +@JobAnnotation(stage = TransactionStage.INIT) public class LoadContainerFlagJobs extends ContainerJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..855d6a139aa80fa0b67cda652708b3e56ef5eb2b --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java @@ -0,0 +1,108 @@ +/* + * ** 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.jobs.core; + +import java.util.List; +import java.util.Map; +import org.caosdb.server.CaosDBServer; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.entity.xml.ToElementable; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; +import org.caosdb.server.transaction.Retrieve; +import org.jdom2.Element; + +/** + * Remove the state property from the entity and, iff necessary, convert it into a State instance + * which is then being appended to the entity's messages. + * + * <p>If this job belongs to a Write transaction there is already a State instance present and the + * conversion is not necessary. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@JobAnnotation( + loadAlways = true, + transaction = Retrieve.class, + stage = TransactionStage.POST_TRANSACTION) +public class MakeStateMessage extends EntityStateJob { + + public static final String SPARSE_FLAG = "sparseState"; + + @Override + protected void run() { + + if ("ENABLED".equals(CaosDBServer.getServerProperty(SERVER_PROPERTY_EXT_ENTITY_STATE))) { + try { + // fetch all state properties and remove them from the entity (indicated by "true") + List<Property> stateProperties = getStateProperties(true); + State stateMessage = getState(false); + + if (stateMessage != null) { + // trigger retrieval of state model because when the XML Writer calls the addToElement + // method, it is to late. + stateMessage.getStateModel(); + } else if (stateProperties != null && stateProperties.size() > 0) { + for (Property s : stateProperties) { + getEntity().addMessage(getMessage(s, isSparse())); + } + } + } catch (Message e) { + getEntity().addError(e); + } + } + } + + private ToElementable getMessage(Property s, boolean sparse) throws Message { + if (sparse) { + return getSparseStateMessage(s); + } + State stateMessage = createState(s); + + // trigger retrieval of state model because when the XML Writer calls the addToElement method, + // it is to late. + stateMessage.getStateModel(); + return stateMessage; + } + + private ToElementable getSparseStateMessage(Property s) { + return new ToElementable() { + @Override + public void addToElement(Element ret) { + Element state = new Element(STATE_XML_TAG); + state.setAttribute(STATE_ATTRIBUTE_ID, s.getValue().toString()); + ret.addContent(state); + } + }; + } + + private boolean isSparse() { + Map<String, String> flags = getTransaction().getContainer().getFlags(); + if (flags != null) { + return "true".equals(flags.getOrDefault(SPARSE_FLAG, "false")); + } + return false; + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java new file mode 100644 index 0000000000000000000000000000000000000000..c2aebc681b38a9902bede69883cce4025bc8afe6 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java @@ -0,0 +1,53 @@ +/* + * ** 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.jobs.core; + +import org.caosdb.server.entity.Message; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; +import org.caosdb.server.transaction.WriteTransaction; + +/** + * This job constructs an ordinary Property from the State right before the entity is being written + * to the back-end and after any checks run. + */ +@JobAnnotation(transaction = WriteTransaction.class, stage = TransactionStage.PRE_TRANSACTION) +public class MakeStateProperty extends EntityStateJob { + + @Override + protected void run() { + State s = getState(); + if (s != null) { + try { + addStateProperty(s); + } catch (Message e) { + getEntity().addError(e); + } + } + } + + private void addStateProperty(State stateEntity) throws Message { + getEntity().addProperty(stateEntity.createStateProperty()); + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/NoCache.java b/src/main/java/org/caosdb/server/jobs/core/NoCache.java index a4f639392ac63a97f2f025dfe6717a664114c561..e408330ee2639d1c5d7dc5f145848e292daa9b3b 100644 --- a/src/main/java/org/caosdb/server/jobs/core/NoCache.java +++ b/src/main/java/org/caosdb/server/jobs/core/NoCache.java @@ -24,7 +24,7 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; @JobAnnotation( flag = "cache", @@ -32,7 +32,7 @@ import org.caosdb.server.jobs.JobExecutionTime; description = "Depending on the configuraion of the server some transactions with the backend database might be cached internally for performance reasons. Disable the internal caching for this transaction with 'false'. Note: You cannot enable the caching with 'true', if the server is not configured to use caching.", values = {"true", "false"}, - time = JobExecutionTime.INIT) + stage = TransactionStage.INIT) public class NoCache extends FlagJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/Paging.java b/src/main/java/org/caosdb/server/jobs/core/Paging.java index 43683639f75c08be394a38ce03e91282405f333d..5f2a6ed62f48f20d09c34c8ee34190231903ffe7 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Paging.java +++ b/src/main/java/org/caosdb/server/jobs/core/Paging.java @@ -27,11 +27,11 @@ import java.util.regex.Pattern; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.transaction.Retrieve; import org.caosdb.server.utils.EntityStatus; -@JobAnnotation(flag = "P", time = JobExecutionTime.PRE_TRANSACTION) +@JobAnnotation(flag = "P", stage = TransactionStage.PRE_TRANSACTION) public class Paging extends FlagJob { public static final int DEFAULT_LENGTH = 100; diff --git a/src/main/java/org/caosdb/server/jobs/core/PickUp.java b/src/main/java/org/caosdb/server/jobs/core/PickUp.java index dff574a4ad0aa5199fe3ccdfdd9c72d2a7b2bb67..94a80836c863ea226a88e035201a2e2fb9fd1039 100644 --- a/src/main/java/org/caosdb/server/jobs/core/PickUp.java +++ b/src/main/java/org/caosdb/server/jobs/core/PickUp.java @@ -30,12 +30,13 @@ import org.caosdb.server.entity.FileProperties; import org.caosdb.server.entity.Message; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; -@JobAnnotation(time = JobExecutionTime.INIT) -public class PickUp extends EntityJob { +@JobAnnotation(stage = TransactionStage.INIT) +public class PickUp extends EntityJob implements Observer { @Override protected void run() { diff --git a/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java b/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java index 950c61d36255b68f52e3066616b1dee5d9f259c3..1cc12c6102f66dda224360554e4ac5c515ae418d 100644 --- a/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java +++ b/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java @@ -34,14 +34,17 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; /** - * To be called after CheckPropValid. + * To be called after CheckPropValid and Inheritance. * * @author tf */ +@JobAnnotation(stage = TransactionStage.PRE_TRANSACTION) public class ProcessNameProperties extends EntityJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/RetrieveACL.java b/src/main/java/org/caosdb/server/jobs/core/RetrieveACL.java index f59b2c1ec94de846714575537516c41d18bf4629..4664c01b88d361485c342a8fcddca2ab082f0dee 100644 --- a/src/main/java/org/caosdb/server/jobs/core/RetrieveACL.java +++ b/src/main/java/org/caosdb/server/jobs/core/RetrieveACL.java @@ -27,7 +27,7 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.xml.ToElementable; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.transaction.Retrieve; import org.caosdb.server.utils.EntityStatus; @@ -35,7 +35,7 @@ import org.caosdb.server.utils.ServerMessages; import org.jdom2.Element; @JobAnnotation( - time = JobExecutionTime.POST_TRANSACTION, + stage = TransactionStage.POST_TRANSACTION, flag = "ACL", description = "Adds an XML representation of the EntityACL to the output of this entity.") public class RetrieveACL extends FlagJob { diff --git a/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java b/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java index 1ca43cec45413d80c307df2f4d09c6b8d34cfb49..7cd7791e0b27f1ec1d5e67e047e7f1cbe177948c 100644 --- a/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java +++ b/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java @@ -23,11 +23,12 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.database.backend.transaction.RetrieveAll; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; -@JobAnnotation(flag = "all", time = JobExecutionTime.INIT) +@JobAnnotation(flag = "all", stage = TransactionStage.INIT) public class RetrieveAllJob extends FlagJob { @Override @@ -37,6 +38,9 @@ public class RetrieveAllJob extends FlagJob { value = "ENTITY"; } execute(new RetrieveAll(getContainer(), value)); + for (EntityInterface entity : getContainer()) { + getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction())); + } } } } diff --git a/src/main/java/org/caosdb/server/jobs/core/RetrieveIdOnlyFlag.java b/src/main/java/org/caosdb/server/jobs/core/RetrieveIdOnlyFlag.java index 1712950dff9403df54f26457d0b6a763d7ce4de8..844468c5473b4d2dff6e59777f925f2701505749 100644 --- a/src/main/java/org/caosdb/server/jobs/core/RetrieveIdOnlyFlag.java +++ b/src/main/java/org/caosdb/server/jobs/core/RetrieveIdOnlyFlag.java @@ -25,10 +25,10 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.utils.EntityStatus; -@JobAnnotation(flag = "IdOnly", time = JobExecutionTime.PRE_TRANSACTION) +@JobAnnotation(flag = "IdOnly", stage = TransactionStage.PRE_TRANSACTION) public class RetrieveIdOnlyFlag extends FlagJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/RetrieveOwner.java b/src/main/java/org/caosdb/server/jobs/core/RetrieveOwner.java index 7377805805ec55e2c3c3928cc42b0c053d8a9c9d..8b6412cee5d6e800a78509741d4f3680403008ce 100644 --- a/src/main/java/org/caosdb/server/jobs/core/RetrieveOwner.java +++ b/src/main/java/org/caosdb/server/jobs/core/RetrieveOwner.java @@ -28,7 +28,7 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.xml.ToElementable; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.permissions.ResponsibleAgent; import org.caosdb.server.transaction.Retrieve; @@ -37,7 +37,7 @@ import org.caosdb.server.utils.ServerMessages; import org.jdom2.Element; @JobAnnotation( - time = JobExecutionTime.POST_TRANSACTION, + stage = TransactionStage.POST_TRANSACTION, flag = "owner", description = "Adds an XML representation of the owner(s) to the output of this entity.") public class RetrieveOwner extends FlagJob { diff --git a/src/main/java/org/caosdb/server/jobs/core/RetriveAllNames.java b/src/main/java/org/caosdb/server/jobs/core/RetriveAllNames.java index dfd69a5cb5a40a6e6be35e345f17e38f3dbb7e2e..b5787246cbafba0df31056d2da15adb330ef0c50 100644 --- a/src/main/java/org/caosdb/server/jobs/core/RetriveAllNames.java +++ b/src/main/java/org/caosdb/server/jobs/core/RetriveAllNames.java @@ -4,7 +4,7 @@ import org.caosdb.server.database.backend.transaction.GetAllNames; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; @@ -15,7 +15,7 @@ import org.caosdb.server.utils.EntityStatus; * <p>The entites' status are set to VALID because the entities parents and properties do not have * to be retrieved. */ -@JobAnnotation(flag = "names", time = JobExecutionTime.INIT) +@JobAnnotation(flag = "names", stage = TransactionStage.INIT) public class RetriveAllNames extends FlagJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/Strict.java b/src/main/java/org/caosdb/server/jobs/core/Strict.java index aebbecf86a89476339dbdef8892b074aebc8196d..bb69063c116e5c14aa9fad2b0bbf881d2481b394 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Strict.java +++ b/src/main/java/org/caosdb/server/jobs/core/Strict.java @@ -27,11 +27,11 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.xml.ToElementable; import org.caosdb.server.jobs.ContainerJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; -@JobAnnotation(time = JobExecutionTime.POST_CHECK) +@JobAnnotation(stage = TransactionStage.POST_CHECK) public class Strict extends ContainerJob { // TODO load strict job via unified flag interface diff --git a/src/main/java/org/caosdb/server/jobs/core/UpdateUnitConverters.java b/src/main/java/org/caosdb/server/jobs/core/UpdateUnitConverters.java index b3ef80adb64fcf3c1007a33a64bde691c9ff9c1d..0b71d2102c98188d875d52d3bbc3e0f79a23043b 100644 --- a/src/main/java/org/caosdb/server/jobs/core/UpdateUnitConverters.java +++ b/src/main/java/org/caosdb/server/jobs/core/UpdateUnitConverters.java @@ -25,12 +25,12 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.database.backend.transaction.InsertLinCon; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.jobs.JobAnnotation; -import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.unit.Converter; import org.caosdb.unit.LinearConverter; import org.caosdb.unit.Unit; -@JobAnnotation(time = JobExecutionTime.PRE_TRANSACTION) +@JobAnnotation(stage = TransactionStage.PRE_TRANSACTION) public class UpdateUnitConverters extends EntityJob { @Override diff --git a/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java b/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java index 3a3d03d18bc7b0913a522e2edd6c235f9a7d39a7..c8cd93e4a1d12125235819b4a2ba69fa12bd0256 100644 --- a/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java +++ b/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -168,32 +169,75 @@ public abstract class AbstractEntityACLFactory<T extends EntityACL> { return create(acis); } + /** + * Normalize the permission rules. + * + * <p>This means that rules which are overriden by other rules are removed. E.g. a granting rule + * for the permission X and the agent P would be removed if there is a denial of X (for P) with + * the same or a higher priority. Likewise, A denial of Y for agent Q would be removed if there is + * a granting rule of Y (for Q) with a higher priority. + */ private void normalize() { - for (final Entry<ResponsibleAgent, Long> set : this.priorityDenials.entrySet()) { - if (this.priorityGrants.containsKey(set.getKey())) { - this.priorityGrants.put( - set.getKey(), this.priorityGrants.get(set.getKey()) & ~set.getValue()); + // 1. run through all prioritized denials and remove overriden rules + // (priority grants, normal grants and normal denials) + Iterator<Entry<ResponsibleAgent, Long>> iterator = this.priorityDenials.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + final ResponsibleAgent agent = next.getKey(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); + continue; } - if (this.normalDenials.containsKey(set.getKey())) { - this.normalDenials.put( - set.getKey(), this.normalDenials.get(set.getKey()) & ~set.getValue()); + if (this.priorityGrants.containsKey(agent)) { + this.priorityGrants.put(agent, this.priorityGrants.get(agent) & ~bitset); } - if (this.normalGrants.containsKey(set.getKey())) { - this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue()); + if (this.normalDenials.containsKey(agent)) { + this.normalDenials.put(agent, this.normalDenials.get(agent) & ~bitset); + } + if (this.normalGrants.containsKey(agent)) { + this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset); + } + } + // 2. run through all prioritized grants and remove overriden rules (normal + // denials and grants) + iterator = this.priorityGrants.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + final ResponsibleAgent agent = next.getKey(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); + continue; + } + if (this.normalDenials.containsKey(agent)) { + this.normalDenials.put(agent, this.normalDenials.get(agent) & ~bitset); + } + if (this.normalGrants.containsKey(agent)) { + this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset); } } - for (final Entry<ResponsibleAgent, Long> set : this.priorityGrants.entrySet()) { - if (this.normalDenials.containsKey(set.getKey())) { - this.normalDenials.put( - set.getKey(), this.normalDenials.get(set.getKey()) & ~set.getValue()); + // 3. run through all normal denials and remove overriden rules (normal grants) + iterator = this.normalDenials.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + final ResponsibleAgent agent = next.getKey(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); + continue; } - if (this.normalGrants.containsKey(set.getKey())) { - this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue()); + if (this.normalGrants.containsKey(agent)) { + this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset); } } - for (final Entry<ResponsibleAgent, Long> set : this.normalDenials.entrySet()) { - if (this.normalGrants.containsKey(set.getKey())) { - this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue()); + // finally, remove all remaining empty grants + iterator = this.normalGrants.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); } } } @@ -206,4 +250,63 @@ public abstract class AbstractEntityACLFactory<T extends EntityACL> { } protected abstract T create(Collection<EntityACI> acis); + + /** + * Remove all rules of the `other` EntityACL from this factory. + * + * <p>This is mainly used for removing all rules which belong to the global entity ACL from this + * ACL before storing it to the backend. + * + * @param other + * @return the same object with changed rule set. + */ + public AbstractEntityACLFactory<T> remove(EntityACL other) { + if (other != null) { + normalize(); + for (EntityACI aci : other.getRules()) { + if (EntityACL.isAllowance(aci.getBitSet())) { + if (EntityACL.isPriorityBitSet(aci.getBitSet())) { + Long bitset = this.priorityGrants.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.priorityGrants.put(aci.getResponsibleAgent(), bitset); + } else { + Long bitset = this.normalGrants.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.normalGrants.put(aci.getResponsibleAgent(), bitset); + } + } else { + if (EntityACL.isPriorityBitSet(aci.getBitSet())) { + Long bitset = this.priorityDenials.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.priorityDenials.put(aci.getResponsibleAgent(), bitset); + } else { + Long bitset = this.normalDenials.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.normalDenials.put(aci.getResponsibleAgent(), bitset); + } + } + } + } + return this; + } } diff --git a/src/main/java/org/caosdb/server/permissions/CaosPermission.java b/src/main/java/org/caosdb/server/permissions/CaosPermission.java index 1b793bb27f6a14bcf7fd0c5f828c20b73f3d11e5..bfbb7eb2e8515f71bb2d611d4fd75916cfae50e7 100644 --- a/src/main/java/org/caosdb/server/permissions/CaosPermission.java +++ b/src/main/java/org/caosdb/server/permissions/CaosPermission.java @@ -24,7 +24,9 @@ package org.caosdb.server.permissions; import java.util.HashSet; import java.util.Map; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.Permission; +import org.apache.shiro.subject.Subject; import org.eclipse.jetty.util.ajax.JSON; public class CaosPermission extends HashSet<PermissionRule> implements Permission { @@ -52,9 +54,10 @@ public class CaosPermission extends HashSet<PermissionRule> implements Permissio boolean grant = false; boolean deny = false; boolean grant_priority = false; + Subject subject = SecurityUtils.getSubject(); for (final PermissionRule r : this) { - if (r.getPermission().implies(p)) { + if (r.getPermission(subject).implies(p)) { if (r.isGrant()) { if (r.isPriority()) { grant_priority = true; diff --git a/src/main/java/org/caosdb/server/permissions/EntityACL.java b/src/main/java/org/caosdb/server/permissions/EntityACL.java index df1915bd460079eb52dfc6d4f3bbf4fe42795918..cfa436d59ae25971a08d4314a7f668a70cf75bbf 100644 --- a/src/main/java/org/caosdb/server/permissions/EntityACL.java +++ b/src/main/java/org/caosdb/server/permissions/EntityACL.java @@ -253,7 +253,9 @@ public class EntityACL { public final Element toElement() { final Element ret = new Element("EntityACL"); - for (final EntityACI aci : this.acl) { + final List<EntityACI> localAcl = new ArrayList<>(this.acl); + localAcl.addAll(GLOBAL_PERMISSIONS.acl); + for (final EntityACI aci : localAcl) { final boolean isDenial = isDenial(aci.getBitSet()); final boolean isPriority = isPriorityBitSet(aci.getBitSet()); final Element e = new Element(isDenial ? "Deny" : "Grant"); @@ -323,7 +325,7 @@ public class EntityACL { } } } - return factory.create(); + return factory.remove(GLOBAL_PERMISSIONS).create(); } public static BitSet convert(final long value) { @@ -379,7 +381,9 @@ public class EntityACL { public Element getPermissionsFor(final Subject subject) { final Element ret = new Element("Permissions"); - final Set<EntityPermission> permissionsFor = getPermissionsFor(subject, this.acl); + final List<EntityACI> localAcl = new ArrayList<>(this.acl); + localAcl.addAll(GLOBAL_PERMISSIONS.acl); + final Set<EntityPermission> permissionsFor = getPermissionsFor(subject, localAcl); for (final EntityPermission p : permissionsFor) { ret.addContent(p.toElement()); } diff --git a/src/main/java/org/caosdb/server/permissions/PermissionRule.java b/src/main/java/org/caosdb/server/permissions/PermissionRule.java index b6ee915156771e1164f3a0ee221d3e14ab964833..85d3b62834a67a4fc46ea9b3c38d19e4b8261d74 100644 --- a/src/main/java/org/caosdb/server/permissions/PermissionRule.java +++ b/src/main/java/org/caosdb/server/permissions/PermissionRule.java @@ -26,23 +26,21 @@ import java.util.HashMap; import java.util.Map; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.permission.WildcardPermission; +import org.apache.shiro.subject.Subject; +import org.caosdb.server.accessControl.Principal; import org.jdom2.Element; public class PermissionRule { - private final WildcardPermission permission; + private final String permission; private final boolean priority; private final boolean grant; public PermissionRule(final String grant, final String priority, final String permission) { - this( - Boolean.parseBoolean(grant), - Boolean.parseBoolean(priority), - new WildcardPermission(permission)); + this(Boolean.parseBoolean(grant), Boolean.parseBoolean(priority), permission); } - public PermissionRule( - final boolean grant, final boolean priority, final WildcardPermission permission) { + public PermissionRule(final boolean grant, final boolean priority, final String permission) { this.grant = grant; this.priority = priority; this.permission = permission; @@ -56,8 +54,9 @@ public class PermissionRule { return this.priority; } - public Permission getPermission() { - return this.permission; + public Permission getPermission(String realm, String username) { + return new WildcardPermission( + permission.replaceAll("\\?REALM\\?", realm).replaceAll("\\?USERNAME\\?", username)); } public static PermissionRule parse(final Map<String, String> rule) { @@ -69,7 +68,7 @@ public class PermissionRule { if (isPriority()) { ret.setAttribute("priority", Boolean.toString(true)); } - ret.setAttribute("permission", getPermission().toString()); + ret.setAttribute("permission", permission); return ret; } @@ -77,14 +76,19 @@ public class PermissionRule { return new PermissionRule( e.getName().equalsIgnoreCase("Grant"), e.getAttribute("priority") != null && Boolean.parseBoolean(e.getAttributeValue("priority")), - new WildcardPermission(e.getAttributeValue("permission"))); + e.getAttributeValue("permission")); } public Map<String, String> getMap() { final HashMap<String, String> ret = new HashMap<String, String>(); ret.put("priority", Boolean.toString(isPriority())); ret.put("grant", Boolean.toString(isGrant())); - ret.put("permission", getPermission().toString()); + ret.put("permission", permission); return ret; } + + public Permission getPermission(Subject subject) { + Principal principal = (Principal) subject.getPrincipal(); + return getPermission(principal.getRealm(), principal.getUsername()); + } } diff --git a/src/main/java/org/caosdb/server/query/CQLLexer.g4 b/src/main/java/org/caosdb/server/query/CQLLexer.g4 index 85061d30cacb222dd8c866d84a7abfb525a592a9..71b41d480bc171aa02af3b2e61eadd16345b3c6d 100644 --- a/src/main/java/org/caosdb/server/query/CQLLexer.g4 +++ b/src/main/java/org/caosdb/server/query/CQLLexer.g4 @@ -77,6 +77,22 @@ IN: [Ii][Nn] WHITE_SPACE_f? ; +AFTER: + [Aa][Ff][Tt][Ee][Rr] WHITE_SPACE_f? +; + +BEFORE: + [Bb][Ee][Ff][Oo][Rr][Ee] WHITE_SPACE_f? +; + +UNTIL: + [Uu][Nn][Tt][Ii][Ll] WHITE_SPACE_f? +; + +SINCE: + [Ss][Ii][Nn][Cc][Ee] WHITE_SPACE_f? +; + IS_STORED_AT: (IS_f WHITE_SPACE_f?)? [Ss][Tt][Oo][Rr][Ee][Dd] (WHITE_SPACE_f? AT)? WHITE_SPACE_f? ; @@ -486,7 +502,7 @@ mode DOUBLE_QUOTE_MODE; mode SELECT_MODE; FROM: - [Ff][Rr][Oo][Mm]([ \t\n\r])? -> 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 452321c670d4101bc603e657023143025f12c22f..6aea8c55f006cd9127d68f0c09e1654d9f9842a9 100644 --- a/src/main/java/org/caosdb/server/query/CQLParser.g4 +++ b/src/main/java/org/caosdb/server/query/CQLParser.g4 @@ -153,14 +153,15 @@ idfilter returns [IDFilter filter] locals [String o, String v, String a] )? ; -transaction returns [TransactionFilter filter] locals [String type, TransactionFilter.Transactor user, String time] +transaction returns [TransactionFilter filter] locals [String type, TransactionFilter.Transactor user, String time, String time_op] @init{ $time = null; $user = null; $type = null; + $time_op = null; } @after{ - $filter = new TransactionFilter($type,$user,$time); + $filter = new TransactionFilter($type,$user,$time,$time_op); } : ( @@ -169,8 +170,8 @@ transaction returns [TransactionFilter filter] locals [String type, TransactionF ) ( - transactor (transaction_time {$time = $transaction_time.tqp;})? {$user = $transactor.t;} - | transaction_time (transactor {$user = $transactor.t;})? {$time = $transaction_time.tqp;} + transactor (transaction_time {$time = $transaction_time.tqp; $time_op = $transaction_time.op;})? {$user = $transactor.t;} + | transaction_time (transactor {$user = $transactor.t;})? {$time = $transaction_time.tqp; $time_op = $transaction_time.op;} ) ; @@ -199,12 +200,25 @@ username returns [Query.Pattern ep] locals [int type] ( STAR {$type = Query.Pattern.TYPE_LIKE;} | ~(STAR | WHITE_SPACE) )+ ; -transaction_time returns [String tqp] +transaction_time returns [String tqp, String op] +@init { + $op = "("; +} : + ( + AT {$op = "=";} + | (ON | IN) + | ( + BEFORE {$op = "<";} + | UNTIL {$op = "<=";} + | AFTER {$op = ">";} + | SINCE {$op = ">=";} + ) + )? ( - (ON | IN) - (value {$tqp = $value.text;}) - ) | TODAY {$tqp = TransactionFilter.TODAY;} + TODAY {$tqp = TransactionFilter.TODAY;} + | value {$tqp = $value.text;} + ) ; /* @@ -481,8 +495,10 @@ number_with_unit unit : - TXT - | NUM SLASH TXT + (~(WHITE_SPACE | WHICH | HAS_A | WITH | WHERE | DOT | AND | OR )) + (~(WHITE_SPACE))* + | + NUM SLASH (~(WHITE_SPACE))+ ; location returns [String str] diff --git a/src/main/java/org/caosdb/server/query/POV.java b/src/main/java/org/caosdb/server/query/POV.java index a1008bd6a67a8712baf2c9b52ab164f619236263..b1a457529a0199edcc5061110ee97e416a264fff 100644 --- a/src/main/java/org/caosdb/server/query/POV.java +++ b/src/main/java/org/caosdb/server/query/POV.java @@ -70,8 +70,8 @@ public class POV implements EntityFilterInterface { private String propertiesTable = null; private String refIdsTable = null; private final HashMap<String, String> statistics = new HashMap<>(); - private Logger logger = LoggerFactory.getLogger(getClass()); - private Stack<String> prefix = new Stack<>(); + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Stack<String> prefix = new Stack<>(); private Unit getUnit(final String s) throws ParserException { return CaosDBSystemOfUnits.getUnit(s); @@ -116,7 +116,7 @@ public class POV implements EntityFilterInterface { // try and parse as integer try { - final Pattern dp = Pattern.compile("^(-?[0-9]++)([^(\\.[0-9])-][^-]*)?$"); + final Pattern dp = Pattern.compile("^(-?[0-9]++)\\s*([^(\\.[0-9])-][^-]*)?$"); final Matcher m = dp.matcher(value); if (!m.matches()) { throw new NumberFormatException(); @@ -133,7 +133,7 @@ public class POV implements EntityFilterInterface { this.vDouble = (double) this.vInt; } else { try { - final Pattern dp = Pattern.compile("^(-?[0-9]+(?:\\.[0-9]+))([^-]*)$"); + final Pattern dp = Pattern.compile("^(-?[0-9]+(?:\\.[0-9]+))\\s*([^-]*)$"); final Matcher m = dp.matcher(value); if (!m.matches()) { throw new NumberFormatException(); @@ -142,7 +142,7 @@ public class POV implements EntityFilterInterface { unitStr = m.group(2); this.vDouble = Double.parseDouble(vDoubleStr); - if ((this.vDouble % 1) == 0) { + if (this.vDouble % 1 == 0) { this.vInt = (int) Math.floor(this.vDouble); } } catch (final NumberFormatException e) { @@ -509,14 +509,16 @@ public class POV implements EntityFilterInterface { return this.aggregate; } - private String measurement(String m) { + private String measurement(final String m) { return String.join("", prefix) + m; } @Override public String getCacheKey() { - StringBuilder sb = new StringBuilder(); - if (this.getAggregate() != null) sb.append(this.aggregate); + final StringBuilder sb = new StringBuilder(); + if (this.getAggregate() != null) { + sb.append(this.aggregate); + } sb.append(toString()); if (this.hasSubProperty()) { sb.append(getSubProperty().getCacheKey()); diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java index cd323bd972b21a261c84bfa6c16c08ea54558fd0..058fc619e021b96e1ac3d83750d7ed84c5acc2c0 100644 --- a/src/main/java/org/caosdb/server/query/Query.java +++ b/src/main/java/org/caosdb/server/query/Query.java @@ -206,7 +206,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } - private static boolean filterEntitiesWithoutRetrievePermisions = + private boolean filterEntitiesWithoutRetrievePermisions = !CaosDBServer.getServerProperty( ServerProperties.KEY_QUERY_FILTER_ENTITIES_WITHOUT_RETRIEVE_PERMISSIONS) .equalsIgnoreCase("FALSE"); @@ -663,13 +663,15 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac if (this.container != null && this.type == Type.FIND) { for (final IdVersionPair p : this.resultSet) { - final Entity e = new RetrieveEntity(p.id, p.version); + if (p.id > 99) { + 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); + // if query has select-clause: + if (this.selections != null && !this.selections.isEmpty()) { + e.addSelections(this.selections); + } + this.container.add(e); } - this.container.add(e); } } return this; diff --git a/src/main/java/org/caosdb/server/query/TransactionFilter.java b/src/main/java/org/caosdb/server/query/TransactionFilter.java index fed4a7156048f1bd43f7e4000df80f29a38187a0..2099639b0fc862edb2fc0bceecfa17bad6dad167 100644 --- a/src/main/java/org/caosdb/server/query/TransactionFilter.java +++ b/src/main/java/org/caosdb/server/query/TransactionFilter.java @@ -29,7 +29,6 @@ import java.sql.Types; import org.caosdb.datetime.Date; import org.caosdb.datetime.DateTimeFactory2; import org.caosdb.datetime.Interval; -import org.caosdb.datetime.SemiCompleteDateTime; import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.accessControl.Principal; import org.caosdb.server.accessControl.UserSources; @@ -96,14 +95,20 @@ public class TransactionFilter implements EntityFilterInterface { } } - public TransactionFilter(final String type, final Transactor transactor, final String time) { + public TransactionFilter( + final String type, + final Transactor transactor, + final String time, + final String timeOperator) { this.transactor = transactor; this.transactionTime = time; this.transactionType = type; + this.transactionTimeOperator = timeOperator; } private final Transactor transactor; private final String transactionTime; + private final String transactionTimeOperator; private final String transactionType; @Override @@ -123,7 +128,7 @@ public class TransactionFilter implements EntityFilterInterface { } else { try { - dt = (SemiCompleteDateTime) DateTimeFactory2.valueOf(this.transactionTime); + dt = (Interval) DateTimeFactory2.valueOf(this.transactionTime); } catch (final ClassCastException e) { throw new QueryException("Transaction time must be a SemiCompleteDateTime."); } catch (final IllegalArgumentException e) { @@ -201,14 +206,13 @@ public class TransactionFilter implements EntityFilterInterface { } else { prepareCall.setNull(10, Types.INTEGER); } - prepareCall.setString(11, "("); // '(' means 'is in the // interval' } else { prepareCall.setNull(9, Types.BIGINT); prepareCall.setNull(10, Types.INTEGER); - prepareCall.setNull(11, Types.CHAR); } + prepareCall.setString(11, transactionTimeOperator); } else { // ilb_sec, ilb_nanos, eub_sec, eub_nanos, operator_t prepareCall.setNull(7, Types.BIGINT); @@ -251,6 +255,8 @@ public class TransactionFilter implements EntityFilterInterface { return "TRANS(" + this.transactionType + "," + + this.transactionTimeOperator + + "," + this.transactionTime + "," + this.transactor diff --git a/src/main/java/org/caosdb/server/resource/JdomRepresentation.java b/src/main/java/org/caosdb/server/resource/JdomRepresentation.java index 1af634b0ae784ccaf4aff4aac399771cb4774f60..4be6743d9ff959c278f80322c92fe2ee9f42d7cd 100644 --- a/src/main/java/org/caosdb/server/resource/JdomRepresentation.java +++ b/src/main/java/org/caosdb/server/resource/JdomRepresentation.java @@ -28,6 +28,7 @@ import java.io.Writer; import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.jdom2.Document; +import org.jdom2.Element; import org.jdom2.ProcessingInstruction; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; @@ -68,6 +69,12 @@ public class JdomRepresentation extends WriterRepresentation { super(MediaType.TEXT_XML); this.indent = indent; this.document = document; + Element noscript = new Element("noscript"); + Element div = new Element("h1"); + + div.addContent("Please enable JavaScript!"); + noscript.addContent(div); + document.getRootElement().addContent(0, noscript); if (xslPath != null && document != null) { addStyleSheet(document, xslPath); } diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java index fd7bae8a82e95554d527135f76124d397c23f0c4..19f840e92a1a6812e2ae2610ed3ace23b7e3cee7 100644 --- a/src/main/java/org/caosdb/server/transaction/Retrieve.java +++ b/src/main/java/org/caosdb/server/transaction/Retrieve.java @@ -30,6 +30,7 @@ import org.caosdb.server.entity.container.RetrieveContainer; import org.caosdb.server.entity.xml.SetFieldStrategy; import org.caosdb.server.entity.xml.ToElementStrategy; import org.caosdb.server.entity.xml.ToElementable; +import org.caosdb.server.jobs.ScheduledJob; import org.caosdb.server.jobs.core.Mode; import org.caosdb.server.jobs.core.RemoveDuplicates; import org.caosdb.server.jobs.core.ResolveNames; @@ -50,15 +51,19 @@ public class Retrieve extends Transaction<RetrieveContainer> { setAccess(getAccessManager().acquireReadAccess(this)); // resolve names - final ResolveNames r = new ResolveNames(); - r.init(Mode.SHOULD, null, this); - getSchedule().add(r); - getSchedule().runJob(r); - - final RemoveDuplicates job = new RemoveDuplicates(); - job.init(Mode.MUST, null, this); - getSchedule().add(job); - getSchedule().runJob(job); + { + final ResolveNames r = new ResolveNames(); + r.init(Mode.SHOULD, null, this); + ScheduledJob scheduledJob = getSchedule().add(r); + getSchedule().runJob(scheduledJob); + } + + { + final RemoveDuplicates job = new RemoveDuplicates(); + job.init(Mode.MUST, null, this); + ScheduledJob scheduledJob = getSchedule().add(job); + getSchedule().runJob(scheduledJob); + } // make schedule for all parsed entities makeSchedule(); diff --git a/src/main/java/org/caosdb/server/transaction/Transaction.java b/src/main/java/org/caosdb/server/transaction/Transaction.java index c2bd7e0feb62fa31dadd5580990350b829ff4591..99c305fa17b37878cc5f3277c93623a915be4ea3 100644 --- a/src/main/java/org/caosdb/server/transaction/Transaction.java +++ b/src/main/java/org/caosdb/server/transaction/Transaction.java @@ -39,13 +39,15 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Message.MessageType; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.jobs.Job; -import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.jobs.Schedule; +import org.caosdb.server.jobs.ScheduledJob; +import org.caosdb.server.jobs.TransactionStage; import org.caosdb.server.jobs.core.AccessControl; import org.caosdb.server.jobs.core.CheckDatatypePresent; import org.caosdb.server.jobs.core.CheckEntityACLRoles; import org.caosdb.server.jobs.core.Mode; import org.caosdb.server.jobs.core.PickUp; +import org.caosdb.server.permissions.EntityACL; import org.caosdb.server.utils.AbstractObservable; import org.caosdb.server.utils.Info; import org.caosdb.server.utils.Observer; @@ -72,9 +74,11 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra this(container, Info.getInstance()); } - protected Transaction(C container, Observer o) { + protected Transaction(final C container, final Observer o) { this.container = container; - if (o != null) acceptObserver(o); + if (o != null) { + acceptObserver(o); + } } public static DatabaseAccessManager getAccessManager() { @@ -85,11 +89,16 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra return this.container; } + /** + * Implementation note: Not called in this class, but may be used by subclasses. + * + * <p>E.g. in {@link Retrieve} and {@link WriteTransaction}. + */ protected void makeSchedule() throws Exception { // load flag jobs final Job loadContainerFlags = Job.getJob("LoadContainerFlagJobs", Mode.MUST, null, this); - this.schedule.add(loadContainerFlags); - this.schedule.runJob(loadContainerFlags); + final ScheduledJob scheduledJob = this.schedule.add(loadContainerFlags); + this.schedule.runJob(scheduledJob); // AccessControl this.schedule.add(Job.getJob(AccessControl.class.getSimpleName(), Mode.MUST, null, this)); @@ -104,17 +113,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra // additionally load datatype job if (e.hasValue()) { - boolean found = false; - for (final Job j : loadJobs) { - - if (CheckDatatypePresent.class.isInstance(j) - && ((CheckDatatypePresent) j).getEntity() == e) { - found = true; - } - } - if (!found) { - this.schedule.add(new CheckDatatypePresent().init(Mode.MUST, e, this)); - } + this.schedule.add(new CheckDatatypePresent().init(Mode.MUST, e, this)); } // load pickup job if necessary @@ -124,12 +123,42 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra } } + /** + * The main transaction execution method. + * + * <p>This method calls the following other internal methods and scheduled jobs stored in the + * {@link getSchedule() internal Schedule object}: + * + * <ol> + * <li>{@link init} - Make {@link Schedule}, resolve names to ids, aquire read access. + * <li>{@link Schedule.runJobs(INIT)} - See {@link TransactionStage#INIT}. + * <li>{@link preCheck} - Load/generate {@link EntityACL}s, check if any updates are to be + * processed. + * <li>{@link Schedule.runJobs(PRE_CHECK)} - See {@link TransactionStage#PRE_CHECK}. + * <li>{@link check} - only run the jobs in the CHECK stage, see {@link TransactionStage#CHECK}. + * <li>{@link Schedule.runJobs(POST_CHECK)} - See {@link TransactionStage#POST_CHECK}. + * <li>{@link postCheck} - currently, nothing happens here (just there for consistency). + * <li>{@link preTransaction} - acquire write access (if necessary) + * <li>{@link Schedule.runJobs(PRE_TRANSACTION)} - See {@link TransactionStage#PRE_TRANSACTION}. + * <li>{@link transaction}: This is typically the main method of a Transaction. + * <li>{@link Schedule.runJobs(POST_TRANSACTION)} - See {@link + * TransactionStage#POST_TRANSACTION}. + * <li>{@link postTransaction} - Add success messages + * <li>{@link writeHistory} - write the transaction history logs + * <li>{@link commit} - commit the changes + * <li>{@link rollBack}: Only in the case of errors - rollback any changes (also file-system + * changes). + * <li>{@link cleanUp}: Always - cleanup the transaction (e.g. remove temporary files). + * <li>{@link notifyObservers(CLEAN_UP)}: Also always - for any jobs that do their own clean-up. + * + * @see {@link TransactionStage}. + */ @Override public final void execute() throws Exception { long t1 = System.currentTimeMillis(); try { init(); - this.schedule.runJobs(JobExecutionTime.INIT); + this.schedule.runJobs(TransactionStage.INIT); getContainer() .getTransactionBenchmark() .addMeasurement( @@ -137,7 +166,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra t1 = System.currentTimeMillis(); preCheck(); - this.schedule.runJobs(JobExecutionTime.PRE_CHECK); + this.schedule.runJobs(TransactionStage.PRE_CHECK); getContainer() .getTransactionBenchmark() .addMeasurement( @@ -151,7 +180,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra this.getClass().getSimpleName() + ".check", System.currentTimeMillis() - t1); t1 = System.currentTimeMillis(); - this.schedule.runJobs(JobExecutionTime.POST_CHECK); + this.schedule.runJobs(TransactionStage.POST_CHECK); postCheck(); getContainer() .getTransactionBenchmark() @@ -160,7 +189,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra t1 = System.currentTimeMillis(); preTransaction(); - this.schedule.runJobs(JobExecutionTime.PRE_TRANSACTION); + this.schedule.runJobs(TransactionStage.PRE_TRANSACTION); getContainer() .getTransactionBenchmark() .addMeasurement( @@ -175,7 +204,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra this.getClass().getSimpleName() + ".transaction", System.currentTimeMillis() - t1); t1 = System.currentTimeMillis(); - this.schedule.runJobs(JobExecutionTime.POST_TRANSACTION); + this.schedule.runJobs(TransactionStage.POST_TRANSACTION); postTransaction(); getContainer() .getTransactionBenchmark() @@ -211,6 +240,12 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra } } + /** + * Return the internal {@link Schedule} object. + * + * <p>The Schedule stores jobs which are also triggered by this transaction (see {@link execute()} + * for details). + */ public Schedule getSchedule() { return this.schedule; } @@ -219,6 +254,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra return getContainer().getOwner(); } + /** Return true iff this transaction should be logged in the transaction history logs. */ public abstract boolean logHistory(); public UTCDateTime getTimestamp() { @@ -228,36 +264,46 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra // TODO move to post-transaction job private void writeHistory() throws TransactionException, Message { if (logHistory()) { - String realm = ((Principal) getTransactor().getPrincipal()).getRealm(); - String username = ((Principal) getTransactor().getPrincipal()).getUsername(); + final String realm = ((Principal) getTransactor().getPrincipal()).getRealm(); + final String username = ((Principal) getTransactor().getPrincipal()).getUsername(); execute( new InsertTransactionHistory(getContainer(), realm, username, getTimestamp()), getAccess()); } } + /** @see {@link #execute()} */ protected void rollBack() { - this.schedule.runJobs(JobExecutionTime.ROLL_BACK); + this.schedule.runJobs(TransactionStage.ROLL_BACK); } + /** @see {@link #execute()} */ protected abstract void init() throws Exception; + /** @see {@link #execute()} */ protected abstract void preCheck() throws InterruptedException, Exception; + /** @see {@link #execute()} */ protected final void check() { - this.schedule.runJobs(JobExecutionTime.CHECK); + this.schedule.runJobs(TransactionStage.CHECK); } + /** @see {@link #execute()} */ protected abstract void postCheck(); + /** @see {@link #execute()} */ protected abstract void preTransaction() throws InterruptedException; + /** @see {@link #execute()} */ protected abstract void transaction() throws Exception; + /** @see {@link #execute()} */ protected abstract void postTransaction() throws Exception; + /** @see {@link #execute()} */ protected abstract void cleanUp(); + /** @see {@link #execute()} */ protected void commit() throws Exception {} public boolean useCache() { diff --git a/src/main/java/org/caosdb/server/transaction/TransactionInterface.java b/src/main/java/org/caosdb/server/transaction/TransactionInterface.java index 1aa41b2bbc31063b2978aa9c141e75c98799f124..d56407ca854c756a956fe11d9bc98a72f05a4b50 100644 --- a/src/main/java/org/caosdb/server/transaction/TransactionInterface.java +++ b/src/main/java/org/caosdb/server/transaction/TransactionInterface.java @@ -35,6 +35,10 @@ public interface TransactionInterface { return TransactionBenchmark.getRootInstance().getBenchmark(getClass()); } + /** + * Append the BackendTransaction t to a RollBackHandler before basically calling {@code + * t.execute()}. Except for benchmarking, this method does not interact directly with this object. + */ public default <K extends BackendTransaction> K execute(K t, Access access) { final RollBackHandler handler = (RollBackHandler) access.getHelper("RollBack"); handler.append(t); diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java index abda78b6feeb0de7104de5365877b28d474058fe..670ff8bd6f4e78c6f20500dc057fc0e432872255 100644 --- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java @@ -76,8 +76,7 @@ public class WriteTransaction extends Transaction<WritableContainer> @Override protected final void preTransaction() throws InterruptedException { - // acquire strong access. No other thread can have access until - // it this strong access is released. + // Acquire strong access. No other thread can have access until this strong access is released. setAccess(getAccessManager().acquireWriteAccess(this)); } @@ -215,13 +214,7 @@ public class WriteTransaction extends Transaction<WritableContainer> .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()); - } + ((UpdateEntity) entity).setOriginal(oldEntity); } break innerLoop; } @@ -290,6 +283,18 @@ public class WriteTransaction extends Transaction<WritableContainer> @Override protected void preCheck() throws InterruptedException, Exception { for (final EntityInterface entity : getContainer()) { + try { + if (entity.getEntityStatus() == EntityStatus.QUALIFIED) { + checkPermissions(entity, deriveUpdate(entity, ((UpdateEntity) entity).getOriginal())); + } + } catch (final AuthorizationException exc) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addError(ServerMessages.AUTHORIZATION_ERROR); + entity.addInfo(exc.getMessage()); + } catch (ClassCastException exc) { + // not an update entity. ignore. + } + // set default EntityACL if none present if (entity.getEntityACL() == null) { entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject())); diff --git a/src/main/java/org/caosdb/server/utils/AbstractObservable.java b/src/main/java/org/caosdb/server/utils/AbstractObservable.java index a19fff9d44ce7339a9cdc6712e0806ef42bdfd40..abc7f0a0ae1cdcbeff613c2778637de97a507c57 100644 --- a/src/main/java/org/caosdb/server/utils/AbstractObservable.java +++ b/src/main/java/org/caosdb/server/utils/AbstractObservable.java @@ -37,6 +37,7 @@ public abstract class AbstractObservable implements Observable { return this.observers.add(o); } + /** @param e A String denoting the notification event. */ @Override public void notifyObservers(final String e) { if (this.observers != null) { diff --git a/src/main/java/org/caosdb/server/utils/FlagInfo.java b/src/main/java/org/caosdb/server/utils/FlagInfo.java index 164c6aabe9c4fafff26f93db30d791fa91389eee..60697cb90df0bcf0da0588c580c49664ec0b0f52 100644 --- a/src/main/java/org/caosdb/server/utils/FlagInfo.java +++ b/src/main/java/org/caosdb/server/utils/FlagInfo.java @@ -55,7 +55,7 @@ public class FlagInfo { for (final JobAnnotation a : as) { final Element aElem = new Element(a.flag()); aElem.setAttribute("description", a.description()); - aElem.setAttribute("time", a.time().toString()); + aElem.setAttribute("stage", a.stage().toString()); if (!a.transaction().equals(TransactionInterface.class)) { aElem.setAttribute("transaction", a.transaction().getSimpleName()); } diff --git a/src/main/java/org/caosdb/server/utils/Observer.java b/src/main/java/org/caosdb/server/utils/Observer.java index be89dff4b882adf24bcfd250c3dea1387997360d..b4c8f5444b957a1d18efc7f864c67b3daff8ca33 100644 --- a/src/main/java/org/caosdb/server/utils/Observer.java +++ b/src/main/java/org/caosdb/server/utils/Observer.java @@ -25,6 +25,8 @@ package org.caosdb.server.utils; public interface Observer { /** + * Notify this observer that an event {@code e} has happened to the {@code sender}. + * * @param e * @param sender * @return true, iff the Observable has to keep it in the list of observers. diff --git a/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java b/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java deleted file mode 100644 index ad643a0aade5e8285fa0aea7a92026995b9ccd61..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java +++ /dev/null @@ -1,39 +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.utils.fsm; - -public class ActionNotAllowedException extends RuntimeException { - - private final String action; - - public ActionNotAllowedException(final String action) { - this.action = action; - } - - private static final long serialVersionUID = -4324962066954446942L; - - @Override - public String getMessage() { - return "The action `" + this.action + "` is not allowed in the current state."; - } -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java b/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java deleted file mode 100644 index f8bdccac726bfe25d7644f6dc689b54113f981c7..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java +++ /dev/null @@ -1,88 +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.utils.fsm; - -import java.util.List; -import java.util.Map; - -public abstract class FiniteStateMachine<S extends State, T extends Transition> { - - public FiniteStateMachine(final S initial, final Map<S, Map<T, S>> transitions) - throws StateNotReachableException { - this.currentState = initial; - this.transitions = transitions; - checkEveryStateReachable(); - } - - private void checkEveryStateReachable() throws StateNotReachableException { - for (final State s : getAllStates()) { - if (!s.equals(this.currentState) && !stateIsReachable(s)) { - throw new StateNotReachableException(s); - } - } - } - - private boolean stateIsReachable(final State s) { - for (final Map<T, S> map : this.transitions.values()) { - if (map.containsValue(s)) { - return true; - } - } - return false; - } - - private final Map<S, Map<T, S>> transitions; - private S currentState = null; - - public void trigger(final T t) throws TransitionNotAllowedException { - final S old = this.currentState; - this.currentState = getNextState(t); - onAfterTransition(old, t, this.currentState); - } - - S getNextState(final T t) throws TransitionNotAllowedException { - final Map<T, S> map = this.transitions.get(this.currentState); - if (map != null && map.containsKey(t)) { - return map.get(t); - } - throw new TransitionNotAllowedException(this.getCurrentState(), t); - } - - public S getCurrentState() { - return this.currentState; - } - - public List<? extends State> getAllStates() { - return this.currentState.getAllStates(); - } - - /** - * Override this method in subclasses. The method is called immediately after a transition - * finished. - * - * @param from - * @param transition - * @param to - */ - protected void onAfterTransition(final S from, final T transition, final S to) {} -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java b/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java deleted file mode 100644 index 8282e5dd7e7bea4368acf2c7e77b536fab301ad6..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.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.utils.fsm; - -public class MissingImplementationException extends Exception { - - public MissingImplementationException(final State s) { - super("The state `" + s.toString() + "` has no implementation."); - } - - private static final long serialVersionUID = -1138551658177420875L; -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/State.java b/src/main/java/org/caosdb/server/utils/fsm/State.java deleted file mode 100644 index 81b76aa599a90de2c8b3c8ddd295e62ce47dae12..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/State.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.utils.fsm; - -import java.util.List; - -public interface State { - - public List<State> getAllStates(); -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java b/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java deleted file mode 100644 index 3970f60e5cf6e86096b0992419b14350c43b42ee..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.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.utils.fsm; - -public class StateNotReachableException extends Exception { - - public StateNotReachableException(final State s) { - super("The state `" + s.toString() + "` is not reachable."); - } - - private static final long serialVersionUID = -1826791324169513493L; -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java b/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java deleted file mode 100644 index d37d83783bd7f4aaa1db573f76315d0b51bd5fbb..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java +++ /dev/null @@ -1,60 +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.utils.fsm; - -import java.util.Map; - -public class StrategyFiniteStateMachine<S extends State, T extends Transition, I> - extends FiniteStateMachine<S, T> { - - public StrategyFiniteStateMachine( - final S initial, final Map<S, I> stateImplementations, final Map<S, Map<T, S>> transitions) - throws MissingImplementationException, StateNotReachableException { - super(initial, transitions); - this.stateImplementations = stateImplementations; - checkImplementationsComplete(); - } - - /** - * Check if every state has it's implementation. - * - * @throws MissingImplementationException - */ - private void checkImplementationsComplete() throws MissingImplementationException { - for (final State s : getAllStates()) { - if (!this.stateImplementations.containsKey(s)) { - throw new MissingImplementationException(s); - } - } - } - - private final Map<S, I> stateImplementations; - - public I getImplementation() { - return getImplementation(getCurrentState()); - } - - public I getImplementation(final S state) { - return this.stateImplementations.get(state); - } -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/Transition.java b/src/main/java/org/caosdb/server/utils/fsm/Transition.java deleted file mode 100644 index 41921b76cfc7ceed6e396551807e4b199486f0cf..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/Transition.java +++ /dev/null @@ -1,25 +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.utils.fsm; - -public interface Transition {} diff --git a/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java b/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java deleted file mode 100644 index f0547704e250db5c5088c5019a85974c2f4e6254..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java +++ /dev/null @@ -1,37 +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.utils.fsm; - -public class TransitionNotAllowedException extends Exception { - - private static final long serialVersionUID = -7236981582249457939L; - - public TransitionNotAllowedException(final State state, final Transition transition) { - super( - "The transition `" - + transition.toString() - + "` is not allowed in state `" - + state.toString() - + "."); - } -} diff --git a/src/test/docker/Dockerfile b/src/test/docker/Dockerfile index 08a2a0884d1f154bb941388075b2bdd915125f99..f3c8b98fdab34400cccfc11f0c9211eeca37f845 100644 --- a/src/test/docker/Dockerfile +++ b/src/test/docker/Dockerfile @@ -2,10 +2,11 @@ FROM debian:buster RUN apt-get update && \ apt-get install -y \ git make mariadb-server maven openjdk-11-jdk-headless \ + plantuml \ python3-pip screen libpam0g-dev unzip curl shunit2 \ python3-sphinx \ && \ - pip3 install javasphinx recommonmark sphinx-rtd-theme + pip3 install javasphinx recommonmark sphinx-rtd-theme sphinxcontrib-plantuml # Alternative, if javasphinx fails because python3-sphinx is too recent: # (`_l` not found): diff --git a/src/test/java/org/caosdb/server/jobs/ScheduleTest.java b/src/test/java/org/caosdb/server/jobs/ScheduleTest.java index 7ad1d6112e8b7bd3a29042d5f6ca82709ae4100a..cef0ec3e076f38893ada35bbad881ccfe8a02331 100644 --- a/src/test/java/org/caosdb/server/jobs/ScheduleTest.java +++ b/src/test/java/org/caosdb/server/jobs/ScheduleTest.java @@ -46,6 +46,6 @@ public class ScheduleTest { } }); - schedule.runJobs(JobExecutionTime.ROLL_BACK); + schedule.runJobs(TransactionStage.ROLL_BACK); } } diff --git a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java index 0e0e3ee121b09332b6cbb7d86a22458bb5fbf17f..1787c902f48124d692f8c53e4a73ed04564dfe8f 100644 --- a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java +++ b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java @@ -22,6 +22,7 @@ */ package org.caosdb.server.permissions; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -307,7 +308,7 @@ public class EntityACLTest { @Test public void testFactory() { - final EntityACLFactory f = new EntityACLFactory(); + final AbstractEntityACLFactory<EntityACL> f = new EntityACLFactory(); org.caosdb.server.permissions.Role role1 = org.caosdb.server.permissions.Role.create("role1"); Config config1 = new Config(); @@ -349,60 +350,49 @@ public class EntityACLTest { Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME))); } - // @Test - // public void niceFactoryStuff() { - // final EntityACLFactory f = new EntityACLFactory(); - // f.grant("user1", "*"); - // final EntityACL acl1 = f.create(); - // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL)); - // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.RETRIEVE_ENTITY)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.UPDATE_DATA_TYPE)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.USE_AS_PROPERTY)); - // - // f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*", - // "USE:*"); - // f.grant("user2", "EDIT:ACL"); - // final EntityACL acl2 = f.create(); - // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL)); - // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.RETRIEVE_ENTITY)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.UPDATE_DATA_TYPE)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.USE_AS_PROPERTY)); - // - // } - // - // @Test - // public void testDeny() { - // EntityACLFactory f = new EntityACLFactory(); - // f.deny("test", "DELETE"); - // Assert.assertFalse(f.create().isPermitted("test", - // EntityPermission.DELETE)); - // - // System.out.println(Utils.element2String(f.create().toElement())); - // - // System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement())); - // - // f.grant("test", "USE:*"); - // Assert.assertFalse(f.create().isPermitted("test", - // EntityPermission.DELETE)); - // - // System.out.println(Utils.element2String(f.create().toElement())); - // - // f = new EntityACLFactory(); - // f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*"); - // f.deny(EntityACL.OTHER_ROLE, "DELETE"); - // final EntityACL a = f.create(); - // - // System.out.println(Utils.element2String(a.toElement())); - // - // System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement())); - // } + @Test + public void testRemove() { + EntityACLFactory f = new EntityACLFactory(); + f.grant(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE); + f.deny(org.caosdb.server.permissions.Role.create("role2"), false, EntityPermission.EDIT_ACL); + f.grant( + org.caosdb.server.permissions.Role.create("role3"), true, EntityPermission.RETRIEVE_ACL); + f.deny( + org.caosdb.server.permissions.Role.create("role4"), true, EntityPermission.RETRIEVE_ENTITY); + + EntityACL other = f.create(); + + f.grant(org.caosdb.server.permissions.Role.create("role2"), false, EntityPermission.EDIT_ACL); + f.grant( + org.caosdb.server.permissions.Role.create("role5"), false, EntityPermission.RETRIEVE_FILE); + + f.remove(other); // normalize and remove "other" + + EntityACL tester = f.create(); + assertEquals( + "only the very last rule survived, the others have been overriden or removed", + 1, + tester.getRules().size()); + for (EntityACI aci : tester.getRules()) { + assertEquals(aci.getResponsibleAgent(), org.caosdb.server.permissions.Role.create("role5")); + } + } + @Test + public void testNormalize() { + EntityACLFactory f = new EntityACLFactory(); + f.grant(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE); + f.deny(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE); + f.grant(org.caosdb.server.permissions.Role.create("role1"), true, EntityPermission.DELETE); + f.deny(org.caosdb.server.permissions.Role.create("role1"), true, EntityPermission.DELETE); + + // priority denail overrides everything else + EntityACL denyDelete = f.create(); + assertEquals(1, denyDelete.getRules().size()); + for (EntityACI aci : denyDelete.getRules()) { + assertEquals(org.caosdb.server.permissions.Role.create("role1"), aci.getResponsibleAgent()); + assertTrue(EntityACL.isDenial(aci.getBitSet())); + assertTrue(EntityACL.isPriorityBitSet(aci.getBitSet())); + } + } } diff --git a/src/test/java/org/caosdb/server/query/TestCQL.java b/src/test/java/org/caosdb/server/query/TestCQL.java index 2c28d7f59c018f90ce77e9f7691a362027655466..154977a909a9d34ac2034c1ef352b81e7b2a3cb7 100644 --- a/src/test/java/org/caosdb/server/query/TestCQL.java +++ b/src/test/java/org/caosdb/server/query/TestCQL.java @@ -237,6 +237,10 @@ public class TestCQL { String queryIssue31 = "FIND FILE WHICH IS STORED AT /data/in0.foo"; String queryIssue116 = "FIND *"; + String queryIssue132a = "FIND ENTITY WHICH HAS BEEN INSERTED AFTER TODAY"; + String queryIssue132b = "FIND ENTITY WHICH HAS BEEN CREATED TODAY BY ME"; + String queryIssue134 = "SELECT pname FROM ename"; + String queryIssue131 = "FIND ENTITY WITH pname = 13 €"; // File paths /////////////////////////////////////////////////////////////// String filepath_verb01 = "/foo/"; @@ -5692,7 +5696,7 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); assertTrue(sfq.filter instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(some%,=))", sfq.filter.toString()); + assertEquals("TRANS(Insert,null,null,Transactor(some%,=))", sfq.filter.toString()); } /** String ticket242 = "FIND RECORD WHICH HAS been created by some.user"; */ @@ -5707,7 +5711,7 @@ public class TestCQL { System.out.println(sfq.toStringTree(parser)); - assertEquals("TRANS(Insert,null,Transactor(some.user,=))", sfq.filter.toString()); + assertEquals("TRANS(Insert,null,null,Transactor(some.user,=))", sfq.filter.toString()); assertTrue(sfq.filter instanceof TransactionFilter); } @@ -5781,7 +5785,7 @@ public class TestCQL { assertEquals("@(null,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** String ticket262e = "COUNT FILE WHICH IS NOT REFERENCED AND WAS created by me"; */ @@ -5804,7 +5808,7 @@ public class TestCQL { assertEquals("@(null,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** String ticket262f = "COUNT FILE WHICH IS NOT REFERENCED BY entity AND WAS created by me"; */ @@ -5827,7 +5831,7 @@ public class TestCQL { assertEquals("@(entity,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** @@ -5853,7 +5857,7 @@ public class TestCQL { assertEquals("@(entity,null)", n.getFilter().toString()); assertTrue(f2.getLast() instanceof TransactionFilter); - assertEquals("TRANS(Insert,null,Transactor(null,=))", f2.getLast().toString()); + assertEquals("TRANS(Insert,null,null,Transactor(null,=))", f2.getLast().toString()); } /** String ticket262h = "COUNT FILE WHICH IS NOT REFERENCED BY entity WHICH WAS created by me"; */ @@ -5876,7 +5880,7 @@ public class TestCQL { assertNotNull(((Backreference) backref).getSubProperty()); assertEquals( - "TRANS(Insert,null,Transactor(null,=))", + "TRANS(Insert,null,null,Transactor(null,=))", ((Backreference) backref).getSubProperty().getFilter().toString()); } @@ -5917,7 +5921,7 @@ public class TestCQL { assertNotNull(((Backreference) backref).getSubProperty()); assertEquals( - "TRANS(Insert,null,Transactor(null,=))", + "TRANS(Insert,null,null,Transactor(null,=))", ((Backreference) backref).getSubProperty().getFilter().toString()); } @@ -6686,4 +6690,55 @@ public class TestCQL { assertEquals("POV(pname,=,with)", sfq.filter.toString()); assertNull(((POV) sfq.filter).getSubProperty()); } + + @Test + /** String queryIssue132a = "FIND ENTITY WHICH HAS BEEN INSERTED AFTER TODAY"; */ + public void testIssue132a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.queryIssue132a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("TRANS(Insert,>,Today,null)", sfq.filter.toString()); + } + + @Test + /** String queryIssue132b = "FIND ENTITY WHICH HAS BEEN CREATED TODAY BY ME"; */ + public void testIssue132b() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.queryIssue132b)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertEquals("TRANS(Insert,(,Today,Transactor(null,=))", sfq.filter.toString()); + } + + /** + * Multiple white space chars after `FROM`. + * + * <p>String queryIssue134 = "SELECT pname FROM ename"; + */ + @Test + public void testIssue134() { + // must not throw ParsingException + new Query(this.queryIssue134).parse(); + } + /** + * Space before special character unit + * + * <p>String queryIssue131= "FIND ENTITY WITH pname = 13 €"; + */ + @Test + public void testIssue131() { + // must not throw ParsingException + new Query(this.queryIssue131).parse(); + } } diff --git a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java index 845f589a9700a9d3d25eb8da2878c13ee114cd7d..7f7434528678dbd2e1886fade2116d0c9f766740 100644 --- a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java +++ b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java @@ -29,7 +29,6 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authz.permission.WildcardPermission; import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBServer; import org.caosdb.server.accessControl.AnonymousAuthenticationToken; @@ -95,9 +94,7 @@ public class TestScriptingResource { HashSet<PermissionRule> result = new HashSet<>(); result.add( new PermissionRule( - true, - false, - new WildcardPermission(ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok")))); + true, false, ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok"))); return result; } diff --git a/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java b/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java deleted file mode 100644 index 2a7b726b84464f3a7d287434b2767a34baaf4b50..0000000000000000000000000000000000000000 --- a/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java +++ /dev/null @@ -1,90 +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.utils.fsm; - -import static org.junit.Assert.assertEquals; - -import com.google.common.collect.Lists; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -class SimpleFiniteStateMachine extends FiniteStateMachine<State, Transition> { - - public SimpleFiniteStateMachine( - final State initial, final Map<State, Map<Transition, State>> transitions) - throws StateNotReachableException { - super(initial, transitions); - } -} - -enum TestState implements State { - State1, - State2, - State3; - - @Override - public List<State> getAllStates() { - return Lists.newArrayList(values()); - } -} - -enum TestTransition implements Transition { - toState2, - toState3 -} - -public class TestFiniteStateMachine { - - @Rule public ExpectedException exc = ExpectedException.none(); - - @Test - public void testTransitionNotAllowedException() - throws StateNotReachableException, TransitionNotAllowedException { - final Map<State, Map<Transition, State>> map = new HashMap<>(); - final HashMap<Transition, State> from1 = new HashMap<>(); - from1.put(TestTransition.toState2, TestState.State2); - from1.put(TestTransition.toState3, TestState.State3); - map.put(TestState.State1, from1); - - final SimpleFiniteStateMachine fsm = new SimpleFiniteStateMachine(TestState.State1, map); - assertEquals(TestState.State1, fsm.getCurrentState()); - fsm.trigger(TestTransition.toState2); - assertEquals(TestState.State2, fsm.getCurrentState()); - - // only 1->2 and from 1->3 is allowed. not 2->3 - this.exc.expect(TransitionNotAllowedException.class); - fsm.trigger(TestTransition.toState3); - } - - @Test - public void testStateNotReachable() throws StateNotReachableException { - final Map<State, Map<Transition, State>> empty = new HashMap<>(); - - this.exc.expect(StateNotReachableException.class); - new SimpleFiniteStateMachine(TestState.State1, empty); - } -} diff --git a/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java b/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java deleted file mode 100644 index ebf719b1eaa4b56f3b64bdd9371f121231b4ed68..0000000000000000000000000000000000000000 --- a/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.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.utils.fsm; - -import java.util.HashMap; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -public class TestStrategyFiniteStateMachine { - - @Rule public ExpectedException exc = ExpectedException.none(); - - @Test - public void testStateHasNoImplementation() - throws MissingImplementationException, StateNotReachableException { - final Map<State, Map<Transition, State>> map = new HashMap<>(); - final HashMap<Transition, State> from1 = new HashMap<>(); - from1.put(TestTransition.toState2, TestState.State2); - from1.put(TestTransition.toState3, TestState.State3); - map.put(TestState.State1, from1); - - final Map<State, Object> stateImplementations = new HashMap<>(); - - this.exc.expect(MissingImplementationException.class); - new StrategyFiniteStateMachine<State, Transition, Object>( - TestState.State1, stateImplementations, map); - } -}