diff --git a/CHANGELOG.md b/CHANGELOG.md index ea88e321261634711f9e084f318adf497672d66c..8077b4075f814ed34577e68cba4de2fdeed0b192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Basic caching for queries. The caching is enabled by default and can be + controlled by the usual "cache" flag. + +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Security + +## [0.3.0] - 2021-02-10 + +### Added + * New version history feature. The "H" container flag retrieves the full version history during a transaction (e.g. during Retrievals) and constructs a tree of successors and predecessors of the requested entity version. @@ -36,6 +53,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +* Text user interface (CaosDBTerminal). + ### Fixed * Bug: When the user password is updated the user is deactivated. @@ -52,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -## [0.2] - 2020-09-02 +## [0.2.0] - 2020-09-02 ### Added @@ -95,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Missing handling of list of reference properties in SELECT queries. * #51 - name queries (e.g. `FIND ENTITY WITH name = ...`) - #27 - star matches slashes (e.g. for `FIND ... STORED AT /*.dat`). - #30 - file path cannot be in quotes diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000000000000000000000000000000000000..3744c7b7d654c5f2379b396c5894f8c054907ee4 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,4 @@ +* caosdb-mysqlbackend == 4.0.0 +* Java 11 +* Apache Maven >= 3.6.0 +* make >= 4.2.0 diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index 234ff24f40e9c281bfeb23703864c39369aaea05..5d89ceec00bbd8ba228bb497964f0eeb437891f7 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -18,6 +18,9 @@ guidelines of the CaosDB Project 2. Check all general prerequisites. +3. Update the version property in [pom.xml](./pom.xml) (probably this means to + remove the `-SNAPSHOT`) and in `src/doc/conf.py`. + 4. Merge the release branch into the master branch. 5. Tag the latest commit of the master branch with `v<VERSION>`. @@ -25,3 +28,6 @@ guidelines of the CaosDB Project 6. Delete the release branch. 7. Merge the master branch back into the dev branch. + +8. Update the version property in [pom.xml](./pom.xml) for the next + developlement round (with a `-SNAPSHOT` suffix). diff --git a/caosdb-webui b/caosdb-webui index 82315e1199bd5adcf584f61bfde167bd05624172..8c59cc861d646cbdba0ec749ba052656f67fd58d 160000 --- a/caosdb-webui +++ b/caosdb-webui @@ -1 +1 @@ -Subproject commit 82315e1199bd5adcf584f61bfde167bd05624172 +Subproject commit 8c59cc861d646cbdba0ec749ba052656f67fd58d diff --git a/conf/core/cache.ccf b/conf/core/cache.ccf index b6a50bee08fdd8598dac3c9f3d7aa70f43190127..9cd12694874dfd5e7739f7b23098602155436c8e 100644 --- a/conf/core/cache.ccf +++ b/conf/core/cache.ccf @@ -36,6 +36,9 @@ jcs.region.BACKEND_SparseEntities.cacheattributes.MaxObjects=1002 jcs.region.BACKEND_RetrieveVersionHistory jcs.region.BACKEND_RetrieveVersionHistory.cacheattributes.MaxObjects=1006 +jcs.region.HIGH_LEVEL_QUERY_CACHE +jcs.region.HIGH_LEVEL_QUERY_CACHE.cacheattributes.MaxObjects=1000 + # PAM UserSource Caching: Cached Items expire after 60 seconds if they are not requested (idle) and after 600 seconds max. # PAM_UnixUserGroups jcs.region.PAM_UnixUserGroups diff --git a/conf/core/server.conf b/conf/core/server.conf index 039284de44da219837e522b6f9282327562eb1f0..8922d19c8a41a0d54de3357e4ac3eecffdcbf482 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -68,7 +68,7 @@ MYSQL_USER_NAME=caosdb # Password for the user MYSQL_USER_PASSWORD=caosdb # Schema of mysql procedures and tables which is required by this CaosDB instance -MYSQL_SCHEMA_VERSION=v4.0.0-rc2 +MYSQL_SCHEMA_VERSION=v4.0.0 # -------------------------------------------------- @@ -112,8 +112,8 @@ CERTIFICATES_KEY_STORE_PASSWORD= # -------------------------------------------------- # The session timeout after which the cookie expires. -# 10 min -SESSION_TIMEOUT_MS=600000 +# 60 min +SESSION_TIMEOUT_MS=3600000 # Time after which one-time tokens expire. # This is only a default value. The actual timeout of tokens can be diff --git a/pom.xml b/pom.xml index b617ba7083ab1504913172e36079034c455af8c1..0e01855b2bec1d9f83ed7e16c0d85d1ca4433c9f 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>org.caosdb</groupId> <artifactId>caosdb-server</artifactId> - <version>0.3-SNAPSHOT</version> + <version>0.4.0-SNAPSHOT</version> <packaging>jar</packaging> <name>CaosDB Server</name> <properties> @@ -119,11 +119,6 @@ <artifactId>reflections</artifactId> <version>0.9.11</version> </dependency> - <dependency> - <groupId>com.googlecode.lanterna</groupId> - <artifactId>lanterna</artifactId> - <version>2.1.9</version> - </dependency> <dependency> <groupId>org.antlr</groupId> <artifactId>antlr4</artifactId> diff --git a/src/doc/Data-Model.md b/src/doc/Data-Model.md new file mode 100644 index 0000000000000000000000000000000000000000..3fa64ec2fdab693552b742572d05b1e3be88d11b --- /dev/null +++ b/src/doc/Data-Model.md @@ -0,0 +1,40 @@ + +# CaosDB Data Model + +The data structure is build from some basic building blocks which are shown in the following picture: + + + +It has a base object called **Entity**. Entities are either **Record Types**, **Records**, or **Abstract +Properties** (and Record like objects for files). +What *kind of data* is stored is defined by the Record Types. Actual data is then stored in CaosDB as Records which are of some Record Type. Those Records can have Properties that contain information about the Record. The following is a more detailed explanation (also see this [paper](https://www.mdpi.com/2306-5729/4/2/83)). + +> Record Types and Abstract Properties are used to define the ontology for a particular +> domain in which the RDMS is used. Records are used to store the actual data and therefore +> represent individuals or particular things, e.g. a particular experiment, a particular time series, etc. + +**Record Types** define classes or types of things, e.g. persons, experiments, timeseries, etc. Records +can be viewed as members of the class defined by its Record Type. These classes can contain +Abstract Properties which define key-value relationships for properties of the things along +with the expected data type and possibly the default unit, a default value, or a range of permitted +values. As files on the back-end file system are a major focus of this database management system, +there is a special entity File that encapsulates typical file properties like path, size and checksum. + +**Entities** can be related via binary, directed, transitive is-a relations which model both subtyping +and instantiation, depending on the relata. These relations construct a directed graph of the +Entities. If A is-a B we call A the child of B and B the parent of A. No adamant restrictions are +imposed on the relate of the is-a relation and thus, Entities can be children of multiple Entities. + +Each Entity has a list of Entity Properties, or in short just **Properties**. An Entity Property is not an Entity of its own, but a triple of an Abstract Property, a value or Null, and +an Importance. The values can be numericals, strings, dates, any other valid value that fits into +one of several builtin data types, or, most notably, references to other Entities. The importance +is either obligatory, recommended, suggested, or fix. A valid child of an Entity implicitly +inherits its parent’s Properties according to their Importance, which means that it is obliged, +recommended or only suggested to have a Property with the same Abstract Property (or +any subtype thereof). + +As opposed to Properties with other priorities, **Fixed Properties** have no effect on the Entity’s children. During the creation or update of Entities, the importances of the parents are +being checked by the Server. Missing obligatory Properties invalidate the transaction and +result in an error, by default. Missing Properties, when they are recommended, result in a +warning, but the transaction is considered valid. Entities with missing suggested Properties +are silently accepted as valid. diff --git a/src/doc/Glossary.md b/src/doc/Glossary.md new file mode 100644 index 0000000000000000000000000000000000000000..080c2aea4ff393d349553f7d7211d35518ff7c11 --- /dev/null +++ b/src/doc/Glossary.md @@ -0,0 +1,17 @@ +# Glossary + +## Valid Unique Existing Name + +A name of an exiting entity which is unique among the names of all existing entities. + +## Valid Unique Prospective Name + +A name of a to-be-inserted/updated entity _e_ which is unique among the names of all other entities that are to be inserted or updated along with the entity _e_. + +## Valid ID + +The ID of an existing entity. It is by definition unique among the IDs of all existing entities and is a positive integer." + +## Valid Unique Temporary ID + +The negative integer ID of a to-be-inserted entity _e_ which is unique among the ids of all other entities that are to be inserted along with the entity _e_. diff --git a/src/doc/Permissions.rst b/src/doc/Permissions.rst new file mode 100644 index 0000000000000000000000000000000000000000..c624092aff4f479bc8ed48f439109530cc5295aa --- /dev/null +++ b/src/doc/Permissions.rst @@ -0,0 +1,96 @@ +#Permissions + +CaosDB has a fine grained role based permission system. Each interaction +with the server is governed by the current rules of the user, by default +this is the ``anonymous`` role. The permissions for an action which +involves one or more objects are set either manually or via default +permissions which can be configured. For more detailed information, +there is separate +:doc:``documentation of the permission system<permissions>``. + +Permissions are needed to perform particular elementary *actions* during +any interaction with the the server. E.g. retrieving an Entity requires +the requesting user to have the ``RETRIEVE:ENTITY`` permission for that +entity, and ``DELETE:ENTITY`` to delete the entity. + +The permissions of every user are calculated from a set of *Permission +Rules*. These rules have several sources. Some are global defaults, some +are defined by administrators or the owners of an entity. + +As a side note on the implementation: The server uses `Apache +Shiro <https://shiro.apache.org/documentation.html>`__ for its +permission system. + +What is a Permission Rule? +-------------------------- + +A Permission Rule consists of: + +- A type: Permission Rules can be of ``Grant`` or ``Deny`` type, either + granting or denying specific permissions. +- A `role <manuals/general/roles>`__ (or user): For which users the + permission shall be granted or denied. +- A permission action: Which action shall be permitted or forbidden, + for example all retrieval, or modifications of a specific entity. +- An optional priority: May be ``true`` or ``false``. Permissions with + priority = ``true`` override those without, see the calculation rules + below. + +Permission calculation +---------------------- + +For each action a user tries to perform, the server tests, in the +following order, which rules apply: + +1. *Grant* rules, without priority. +2. *Deny* rules, without priority. +3. *Grant* rules, with priority. +4. *Deny* rules, with priority. + +If at the end the user’s permission is *granted*, the action may take +place. Otherwise (the result is *denied* or the permission still +undefined), the action can not take place. In other words, if you have +not been given the permission explicitly at some point, you don’t have +it. + +Possible actions +---------------- + +Until it is completely added to this documentation, a detailed +description of the actions governed by these permissions can be found +`in the +sources <https://gitlab.com/caosdb/caosdb-server/blob/dev/src/main/java/caosdb/server/permissions/EntityPermission.java#L119>`__. + +Typical permissions are: + +- ``RETRIEVE:ENTITY`` :: To retrieve the full entity (name, + description, data type, …) with all parents and properties (unless + prohibited by another rule on the property level). +- ``RETRIEVE:ACL`` :: To retrieve the full and final ACL of this + entity. +- ``RETRIEVE:ENTITY:1234`` :: To retrieve the entity ``1234``. +- ``RETRIEVE:*:1234`` :: For all “retrieve” actions concerning the + entity ``1234``. + +How to set permissions +---------------------- + +- Config file :: Some default permissions are typically set in the + ``global_entity_permissions.xml`` file, see also the `documentation + for that + file </manuals/server/conf/global_entity_permissions.xml>`__. +- API :: The REST API allows to set the permissions. (*to be documented + here*) +- The Python library :: The permissions can also conveniently be set + via the Python library. Currently the best documentation is inside + various files which use the permission API: + + - The `example + file <https://gitlab.com/caosdb/caosdb-pylib/blob/HEAD/examples/set_permissions.py>`__ + - The ```caosdb_admin.py`` utility + script <https://gitlab.com/caosdb/caosdb-pylib/blob/HEAD/src/caosdb/utils/caosdb_admin.py>`__ + - The `integration + tests <https://gitlab.com/caosdb/caosdb-pyinttest/blob/HEAD/tests/test_permissions.py>`__ + also cover quite a bit of the permission API. + +- WebUI :: Not implemented (or documented?) yet. diff --git a/src/doc/administration.rst b/src/doc/administration.rst new file mode 100644 index 0000000000000000000000000000000000000000..819ce5a59fcfbeccc5e145cf773d807197a42d0f --- /dev/null +++ b/src/doc/administration.rst @@ -0,0 +1,10 @@ +Administration +============== + +.. toctree:: + :maxdepth: 1 + :glob: + + administration/* + + diff --git a/src/doc/administration/configuration.rst b/src/doc/administration/configuration.rst index 4170fd0ed31c7a0ec5c1b4e589ff919267cf7de9..bbafcf22bd8576e141cb9d7e8388212ccf24d934 100644 --- a/src/doc/administration/configuration.rst +++ b/src/doc/administration/configuration.rst @@ -23,6 +23,7 @@ In each of these directories, the server looks for the following files: ``global_entity_permissions.xml`` :ref:`Permissions <concepts:Permissions>` which are automatically set, based on user roles. + See the `default file <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/conf/core/global_entity_permissions.xml>`_. ``usersources.ini`` This file defines possible sources which are checked when a user tries to authenticate. Each diff --git a/src/doc/administration/curl-access.rst b/src/doc/administration/curl-access.rst new file mode 100644 index 0000000000000000000000000000000000000000..79b10a712056959bf03a5ee6a41671b9cb3c3119 --- /dev/null +++ b/src/doc/administration/curl-access.rst @@ -0,0 +1,265 @@ +Curl Access for CaosDB +====================== + +As the API that is used to communicate with the CaosDB server is XML +over HTTP also simple HTTP clients, such as +`cURL <https://curl.haxx.se/>`__ can be used. cURL is an old and +established command line program for transferring files over networks +that implements various protocols including HTTP/HTTPS. It is installed +by default on many Linux distributions and can therefore be very useful +for testing and debugging of CaosDB. + +This small manual also gives some practical insights about the CaosDB +protocol itself. + +Doing a simple retrieve +----------------------- + +So, let’s start right away with a few basic examples. + +Let’s do a query on our demo instance: + +.. code:: bash + + curl "https://demo.indiscale.com/Entity/?query=FIND%20Experiment" + +By default cURL sends an HTTP GET request which is needed for doing +queries for CaosDB. CaosDB requires sending them to ``/Entity``. The +query itself is specified after the HTTP query string ``?query=``. +``%20`` is specific to URL encoding and corresponds to a space (see +https://en.wikipedia.org/wiki/Percent-encoding for details). So the +actual query we were doing was ``FIND Experiment`` which should return +all entities with name or with parent ``Experiment``. + +The response should look like: + +.. code:: xml + + <?xml version="1.0" encoding="UTF-8"?> + <?xml-stylesheet type="text/xsl" href="https://demo.indiscale.com/webinterface/webcaosdb.xsl" ?> + <Response srid="b56e3d1a442460c46dde924a54e8afba" timestamp="1561453363382" baseuri="https://demo.indiscale.com" count="3"> + <UserInfo> + <Roles> + <Role>anonymous</Role> + </Roles> + </UserInfo> + <Query string="FIND Experiment" results="3"> + <ParseTree>(cq FIND (entity Experiment) <EOF>)</ParseTree> + <Role /> + <Entity>Experiment</Entity> + <TransactionBenchmark since="Tue Jun 25 11:02:43 CEST 2019" /> + </Query> + <RecordType id="212" name="Experiment"> + <Permissions /> + </RecordType> + <RecordType id="215" name="UnicornExperiment"> + <Permissions /> + <Parent id="212" name="Experiment" /> + </RecordType> + <Record id="230"> + <Permissions /> + <Parent id="215" name="UnicornExperiment" /> + <Property id="216" name="date" datatype="DATETIME" importance="FIX"> + 2025-10-11 + <Permissions /> + </Property> + <Property id="217" name="species" datatype="TEXT" importance="FIX"> + Unicorn + <Permissions /> + </Property> + <Property id="214" name="Conductor" datatype="TEXT" importance="FIX"> + Anton Beta + <Permissions /> + </Property> + <Property id="221" name="Photo" datatype="Photo" importance="FIX"> + 226 + <Permissions /> + </Property> + <Property id="220" name="LabNotes" datatype="LabNotes" importance="FIX"> + 227 + <Permissions /> + </Property> + <Property id="218" name="UnicornVideo" datatype="UnicornVideo" importance="FIX"> + 228 + <Permissions /> + </Property> + <Property id="219" name="UnicornECG" datatype="UnicornECG" importance="FIX"> + 229 + <Permissions /> + </Property> + </Record> + </Response> + +We can see, that in fact two ``RecordTypes`` and one ``Record`` are +returned. Furthermore the response contains some additional information: + +- Attributes in the ``Response`` tag: + + - ``srid="b56e3d1a442460c46dde924a54e8afba"`` A unique identifier + for this request. + - ``timestamp="1561453363382"`` The `UNIX + timestamp <https://en.wikipedia.org/wiki/Unix_time>`__ for this + request. + - ``baseuri="https://demo.indiscale.com"`` The base URI of the + instance of CaosDB that performed the request. + - ``count="3"`` The number of results (2 ``RecordTypes`` and 1 + ``Record``) + +- Information about the user in ``UserInfo``. In this case we have not + logged in, so we are anonymous. +- Detailled information about the query. This includes for example the + parse tree and can be used for debugging and testing. Depending on + the settings of the server instance, this tag includes more or less + detail, for example a more detailled transaction benchmark. + +More details about the retrieve +------------------------------- + +The cURL statement used in the previous section made use of a lot of +default settings for cURL. Let’s have a closer look behind the options. +(I assigned the above URL to a shell variable, to make the statement +more readable.) + +.. code:: bash + + URL="https://demo.indiscale.com/Entity/?query=FIND%20Experiment" + + curl -X GET -b cookie.txt -D head.txt $URL + +This command specifies three more options: + +- ``-X GET`` Do a GET request. This can of course be replaced by POST, + PUT, DELETE or any other HTTP operation. +- ``-b cookie.txt`` This instructs cURL to use cookies from the file + ``cookie.txt`` (which we don’t have yet, see below) +- ``-D head.txt`` Tell cURL to store the received header in the file + ``head.txt``. + +Running this command will give us a similar response than in the +previous section, but additionally a file ``head.txt``: + +:: + + HTTP/1.1 200 OK + Content-Type: text/xml; charset=UTF-8 + Date: Tue, 25 Jun 2019 09:17:23 GMT + Accept-Ranges: bytes + Server: Restlet-Framework/2.3.12 + Vary: Accept-Charset, Accept-Encoding, Accept-Language, Accept + Transfer-Encoding: chunked + +There is nothing special in that header. Most importantly the request +has lead to a response without HTTP error. + +Logging in +---------- + +You might have asked yourself, what we need the cookie for. The simple +answer is: authentication. For many operations done on the CaosDB server +we have to log in first. The demo instance is configured to allow for +anonymous read access by default. But depending on the instance you are +accessing, even this might be disallowed. + +You can log in to the server using cURL with the following command: + +.. code:: bash + + URL="https://demo.indiscale.com/login" + + curl -X POST -c cookie.txt -D head.txt -H "Content-Type: application/x-www-form-urlencoded" -d username=salexan -d password=$PW $URL + +So here we are doing a ``POST`` request instead of a ``GET``. We are +instructing cURL to use the ``cookie.txt`` file to **store** the cookies +it recieves. This is done using the ``-c`` option (instead of the ``-b`` +option above). This time we are explicitely specifying the content type +of the content we are sending (using the ``POST`` request) with the +``-H`` option. The actual content sent are the two fields specified +using the ``-d`` options. This boils down to the two key-value-pairs +“username=” and “password=”. + +This time we are sending the information to a different context +indentified by ``/login``. + +If you don’t want to supply your passowrd in plain text you can for +example use a password manager (like I do), in my case +`pass <https://www.passwordstore.org/>`__ as follows to store your +password in the variable ``$PW``: + +.. code:: bash + + PW=$(pass your/passowrd/identifier) + +Custom Certificates +------------------- + +If you are running your own CaosDB instance it might be necessary to use +a custom SSL certificate. + +You can specify this using the option ``--cacert``, e.g.: + +:: + + --cacert "/path/to/certificate/root.cert.pem" + +Uploading files +--------------- + +According to the specification, a file upload is a POST with multipart +form data. This can be achieved using CURL with the following simple +command line: + +.. code:: bash + + curl -b cookie.txt \ + -F "FileRepresentation=<file.xml" \ + -F "testfile.bla=@testfile.bla" \ + $URL + +Here I am using a previously stored cookie from cookie.txt. + +There are two prerequisites to executing the above command: - The file +representation ``file.xml`` which is in this case stored in a file. - +The actual file to be uploaded with name ``testfile.bla``. The left hand +side of the assignment is very important as the identifier given here +(which in this case is ``testfile.bla``) will be used in the XML for +identifying the file that is described in the corresponding XML tag. + +Let’s have a look at the contents of file.xml: + +.. code:: xml + + <Post> + <File upload="testfile.bla" destination="/test/testfile.bla" description="bla" + checksum="672f8ff4ae8530de295f9dd963724947841e6277edec3b21820b5e44d0a64baef90fb04e22048028453d715f79357acc5bd2d566fe6ede65f981ba3dda06bae4" + size="3"/> + </Post> + +The attributes have the following meaning: - ``upload=testfile.bla`` The +filename given here is actually no filename, but an identifier to find +the multipart data chunk that contains the file. I called it +``testfile.bla`` for simplicity here. - +``destination="/test/testfile.bla"`` The destination path on the CaosDB +server file system. + +Before looking at the other attributes let’s have a look at the file +``testfile.bla`` itself: + +:: + + ok + +The file has size “3” which can be verified on linux using a: + +.. code:: bash + + stat testfile.bla + +It’s hashsum is important for checking the integrity after the transfer +to the server. It can be computed on linux using: + +.. code:: bash + + sha512sum testfile.bla + +These information has to be supplied as the remaining attributes to the +XML. diff --git a/src/doc/administration/server_side_scripting.rst b/src/doc/administration/server_side_scripting.rst index 59bce98682c6d1190fc90681fc8e14fd483e891e..b187d1d566fdcac004649506d036807276cbc4ae 100644 --- a/src/doc/administration/server_side_scripting.rst +++ b/src/doc/administration/server_side_scripting.rst @@ -81,5 +81,5 @@ An invocation via a button in javascript could look like: return scripting_form[0]; } -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>` diff --git a/src/doc/concepts.rst b/src/doc/concepts.rst index ec93719548a7d7c577ac62c6615b3fcf24111b42..0db6186302302767c49735bcdfa81364685573f9 100644 --- a/src/doc/concepts.rst +++ b/src/doc/concepts.rst @@ -1,29 +1,15 @@ -=================================== + Basic concepts of the CaosDB server =================================== +.. toctree:: + :hidden: + :glob: + + Data Model <Data-Model> + Permissions + roles + The CaosDB server provides the HTTP API resources to users and client libraries. It uses a plain MariaDB/MySQL database as backend for data storage, raw files are stored separately on the file system. - - -Configuration -------------- - -Administrators may configure the server through :doc:`configuration -files<administration/configuration>`. Additionally, configurations may be set via the API if the -server is in debug mode. - - -Permissions ------------ - -CaosDB has a fine grained role based permission system. Each interaction with the server is -governed by the current rules of the user, by default this is the ``anonymous`` role. The -permissions for an action which involves one or more objects are set either manually or via default -permissions which can be configured. For more detailed information, there is separate -:doc:`documentation of the permission system<permissions>`. - - - - diff --git a/src/doc/conf.py b/src/doc/conf.py index 84e676394d19e8ec35f241f1cf3cc202dafa0d4d..fb3d6264cc4add018b83d08378813aab9d10964d 100644 --- a/src/doc/conf.py +++ b/src/doc/conf.py @@ -25,9 +25,9 @@ copyright = '2020, IndiScale GmbH' author = 'Daniel Hornung' # The short X.Y version -version = '0.2' +version = '0.3' # The full version, including alpha/beta/rc tags -release = '0.2' +release = '0.3' # -- General configuration --------------------------------------------------- diff --git a/src/doc/entities.png b/src/doc/entities.png new file mode 100644 index 0000000000000000000000000000000000000000..ba3e111a78ddec275636ff0b7e8b324edb13f7be Binary files /dev/null and b/src/doc/entities.png differ diff --git a/src/doc/index.rst b/src/doc/index.rst index e41d318a2dbf7323e5cb327be1c7a3fe0b5d61ad..6a9013c51720f13b4f473d9213bb64b8d4d27eec 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -11,8 +11,10 @@ Welcome to caosdb-server's documentation! Getting started <README_SETUP> Concepts <concepts> Query Language <CaosDB-Query-Language> - administration/* + administration development/* + specification/index.rst + Glossary API documentation<_apidoc/packages> Welcome to the CaosDB, the flexible semantic data management toolkit! diff --git a/src/doc/roles.md b/src/doc/roles.md new file mode 100644 index 0000000000000000000000000000000000000000..80729fb11f8113c64da7e9bf26b04de68fdd6681 --- /dev/null +++ b/src/doc/roles.md @@ -0,0 +1,35 @@ +# Roles # + +## Users and roles ## + +Interaction with CaosDB happens either as an authenticated *user* or without +authentication. In CaosDB, users can have zero, one or more *roles*, several +users may have the same role, and there may be roles without any users. + +## What are users and roles good for? ## + +The user and their roles are always returned by the server in answers to requests +and can thus be interpreted and used by clients. The most important use though +is [permission](manuals/general/permissions) checking in the server: Access and +modification of +entities can be controlled via roles, so that users of a given role are allowed +or denied certain actions. Incidentally, the permission to edit the permissions +of an entity is seen as defining the ownership of an object: Being able to +change the permissions is equivalent to being the owner. + +## Special roles ## + +There are some special roles, which are automatically assigned to users: + +- `anonymous` :: If requests are sent to the server without authentication, so + that no user is defined, the request always has the role `anonymous`. +- *User names* :: An authenticated user implicitly has a role with the same name + as the user name. +- `?OWNER?` :: If a user has the permission to edit the permissions of an + entity, the user automatically has the `?OWNER?` roler for that entity. +- `?OTHER?` :: The `?OTHER?` role is the contrary to the `?OWNER?` role: A user + is either the owner of an entity, or has the role `?OTHER?`. + +Except for the `anonymous` role, these special roles are not returned by the +server, but can nevertheless be used to define +[permissions](manuals/general/permissions). diff --git a/src/doc/specification/AbstractProperty.md b/src/doc/specification/AbstractProperty.md new file mode 100644 index 0000000000000000000000000000000000000000..d594f81967cacd708699ceaeccd9188685a7184d --- /dev/null +++ b/src/doc/specification/AbstractProperty.md @@ -0,0 +1,331 @@ +# AbstractProperty Specification + +## Introduction +An `AbstractProperty` is one of the basal [objects of HeartDB](./HeartDBObject). +An `AbstractProperty` MUST have the following _qualities_ (shortcut in brackets): +* a persistent id (`id`) +* an unique name (`name`) +* a description (`description`) +* a type (`type`) +* a generator `generator`) +* a creator (`creator`) +* a timestamp of it's date of creation (`created`) +* a set of owners and owning groups (`owner`) +* a set rules which controls the access to that `AbstractProperty` (`permission`) +Depending on the `AbstractProperty's` type it MUST have one of the following _qualities_: +* a unit (`unit`) +* a RecordType which is referenced by any instantiating ConcreteProperty. (`reference`) +This is described below. + +## Property Types +An `AbstractProperty` MUST have one of the following 7 types: +* `text` +* `integer` +* `double` +* `datetime` +* `reference` +* `file` + +An `AbstractProperty` of type `text`, `datetime`, or `file` MUST NOT have any unit or referenced RecordType. A `double`, `integer` AbstractProperty MUST have a unit but MUST NOT have a referenced RecordType. A `reference` `AbstractProperty` MUST have referenced RecordType und MUST NOT have a unit. + +## XML Representation of AbstractProperty Objects + +An `AbstractProperty` is represented in xml by a `<Property/>` tag. It's _qualities_ except for `owner` and `permission` are represented by an xml attribute of the same name. E.g `<Property name="length"/> denotes an AbstractProperty which {{{name` is _length_. The `owner` and `permission` qualities are not in use, yet. +Depending on the purpose of the xml document (shall it represent an object in the database or an object _to be posted _to the database?) the `<Property/>` tag may actually have just a few of the mentioned "quality-attributes". + +### GET AbstractProperty +Any xml representation of an `AbstractProperty` that is retrieved from the HeartDB Server MUST have exactly ONE of the following forms, depending on the `AbstractProperty's` type: +#### text + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="text" /> +#### integer + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="integer" unit="$unit" /> +#### double + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="double" unit="$unit" /> +#### datetime + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="datetime" /> +#### reference + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="reference" reference="$reference" /> +#### file + + <Property id="$id" name="$name" description="$description" generator="$generator" creator="$creator" created="$created" type="file" /> +'''General Notes: +* If the called Property does not exist or if the Property called without permission, the HeartDB Server will return an [Error](./Errorcodes). + +### POST AbstractProperty +Any xml representation of an `AbstractProperty` that is to be posted to the HeartDB server MUST have exactly ONE of the following forms, depending on the `AbstractProperty's` type: +#### text + + <Property name="$name" description="$description" generator="$generator" type="text" /> +#### integer + + <Property name="$name" description="$description" generator="$generator" type="integer" unit="$unit" /> +#### double + + <Property name="$name" description="$description" generator="$generator" type="double" unit="$unit" /> +#### datetime + + <Property name="$name" description="$description" generator="$generator" type="datetime" /> +#### reference + + <Property name="$name" description="$description" generator="$generator" type="reference" reference="$reference" /> +#### file + + <Property name="$name" description="$description" generator="$generator" type="file" /> +*General Notes:* +* The `AbstractProperty's` `id` and timestamp (`created`) will be generated by the HeartDB Server. +* The `AbstractProperty's` creator will be determined by the HeartDB Server depending on it's policy configuration. +* Any given attribute beyond these will be *ignored*. +* If the `<Property/>` tag isn't compliant with these the HeartDB Server will return an [Error](./Errorcodes). + +---- +## Examples +### GET Requests +#### Single-Get +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/1 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="1" name="explanation" type="text" description="explains the thing" generator="Timm (manually)"/> + </Response> +---- +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/explanation + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="1" name="explanation" type="text" description="explains the thing" /> + </Response> +---- +#### Multi-Get +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/explanation&2&3 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="1" name="explanation" type="text" description="explains the thing" /> + <Property id="2" name="comment" type="text" description="A comment" generator="Timm (manually)"/> + <Property id="3" name="name" type="text" description="Name" generator="Timm (manually)" /> + </Response> +---- +#### Get all +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/ + GET http://localhost:8122/mpidsserver/AbstractProperty + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response /> +[View Ticket #2](http://dev.bmp.ds.mpg.de/heartdb/ticket/2) +#### Erroneous Requests +##### Non-existing +*Request:* + + GET http://localhost:8122/mpidsserver/AbstractProperty/123456 +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="123456"> + <Error code="201" description="Property does not exist. " /> + </Property> + </Response> +---- +### POST Requests + +*Request is to be sent to:* + + POST http://localhost:8122/mpidsserver/AbstractProperty/ + POST http://localhost:8122/mpidsserver/AbstractProperty +---- +#### Single-Post +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty1" type="integer" description="This is TimmsIntegerProperty1" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="39" name="TimmsIntegerProperty1" type="integer" unit="kg" description="This is TimmsIntegerProperty1" generator="Timm (manually)"/> + </Response> +---- +*Request body (with a reference to Property 12345):* + + <Post> + <Property name="Timms reference property 1" type="reference" reference="12345" description="This is Timms reference property 1"/> + </Post> +---- +#### Multi-Post +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty7" type="integer" description="This is TimmsIntegerProperty7" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty8" type="integer" description="This is TimmsIntegerProperty8" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty9" type="integer" description="This is TimmsIntegerProperty9" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + ... + <Property id="41" name="TimmsIntegerProperty9" type="integer" unit="kg" description="This is TimmsIntegerProperty9" generator="Timm (manually)"/> + </Response> + +---- +#### Erroneous Requests +##### No Description +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" generator="Timm (manually)"/> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg"> + <Error code="202" description="Property has no Description. " /> + </Property> + </Response> +---- +##### No generator +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" description="This is TimmsIntegerProperty4" /> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" > + <Error code="202" description="Property has no generator. " /> + </Property> + </Response> +---- +##### No Type +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty4" description="This is TimmsIntegerProperty4" unit="kg" generator="Timm (manually)"/> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty4" unit="kg" description="This is TimmsIntegerProperty4" generator="Timm (manually)" > + <Error code="252" description="Property has no PropertyType" /> + </Property> + </Response> +---- +##### No Unit +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty5" type="integer" description="This is TimmsIntegerProperty5" generator="Timm (manually)"/> + </Post> +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property name="TimmsIntegerProperty5" type="integer" description="This is TimmsIntegerProperty5" generator="Timm (manually)"> + <Error code="202" description="Property has no Unit. " /> + </Property> + </Response> +---- +##### No Name +*Request body:* + + <Post> + <Property type="integer" description="This is TimmsIntegerProperty6" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property type="integer" unit="kg" description="This is TimmsIntegerProperty6" generator="Timm (manually)"> + <Error code="202" description="Property has no Name. " /> + </Property> + </Response> +---- +##### Invalid Name +*Request body:* + + <Post> + <Property id="39" name="TimmsIntegerProperty7" type="integer" description="This is TimmsIntegerProperty7" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property id="39" name="TimmsIntegerProperty7" type="integer" unit="kg" description="This is TimmsIntegerProperty7" generator="Timm (manually)"> + <Error code="204" description="Property has an invalid Name. " /> + </Property> + </Response> +---- +##### Mixed +*Request body:* + + <Post> + <Property name="TimmsIntegerProperty2" type="integer" description="This is TimmsIntegerProperty2" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty3" type="integer" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty4" description="This is TimmsIntegerProperty4" unit="kg" generator="Timm (manually)"/> + <Property name="TimmsIntegerProperty5" type="integer" description="This is TimmsIntegerProperty5" generator="Timm (manually)"/> + <Property type="integer" description="This is TimmsIntegerProperty6" unit="kg" generator="Timm (manually)"/> + <Property id="39" name="TimmsIntegerProperty7" type="integer" description="This is TimmsIntegerProperty7" unit="kg" generator="Timm (manually)"/> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <Property type="integer" unit="kg" description="This is TimmsIntegerProperty6" generator="Timm (manually)"> + <Error code="202" description="Property has no Name. " /> + </Property> + <Property id="39" name="TimmsIntegerProperty7" type="integer" unit="kg" description="This is TimmsIntegerProperty7" generator="Timm (manually)"> + <Error code="204" description="Property has an invalid Name. " /> + </Property> + ... + </Response> + += DELETE AbstractProperties = + +HTTP-DELETE-requests are to be send to `http://${host}:${port}/mpidsserver/AbstractProperty/...`. Default port is 8123. + +*Example* +{{{ +DELETE http://${host}:${port}/mpidsserver/AbstractProperty/1&2&3&4 +}}} + + +## TODO +### UPDATE AbstractProperties +Notes: +* ids are persistent. They cannot be changed. +* Maybe we should take the PropertyTypes as persistent, too or just allow a few changes: Double <-> Integer? +Roadmap: +1) Specify and implement changing the name, description and unit of AbstractProperties. +2) Specify and implement changing the reference of an abstract reference property. +3) Anything else... diff --git a/src/doc/specification/Authentication.md b/src/doc/specification/Authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..9ec5bcd74ffb68ee33084f739d6d547a3e7a9bb8 --- /dev/null +++ b/src/doc/specification/Authentication.md @@ -0,0 +1,82 @@ +# Authentication + Some features of HeartDB are available to registered users only. Making any changes to the data stock via HTTP requires authentication by `username` _plus_ `password`. They are to be send as a HTTP header, while the password is to be hashed by the sha512 algorithm: + +| `username:` | `$username` | +|-------------|-------------|- +| `password:` | `$SHA512ed_password` | + + +## Sessions + +### Login + +#### Request Challenge + + * `GET http://host:port/mpidsserver/login?username=$username` + * `GET http://host:port/mpidsserver/login` with `username` header + +*no password required to be sent over http* + +The request returns an AuthToken with a login challenge as a cookie. The AuthToken is a dictionary of the following form: + + + {scope=$scope; + mode=LOGIN; + offerer=$offerer; + auth=$auth + expires=$expires; + date=$date; + hash=$hash; + session=$session; + } + + $scope:: A uri pattern string. Example: ` {**/*} ` + $mode:: `ONETIME`, `SESSION`, or `LOGIN` + $offerer:: A valid username + $auth:: A valid username + $expires:: A `YYYY-MM-DD HH:mm:ss[.nnnn]` date string + $date:: A `YYYY-MM-DD HH:mm:ss[.nnnn]` date string + $hash:: A string + $session:: A string + +The challenge is solved by concatenating the `$hash` string and the user's `$password` string and calculating the sha512 hash of both. Pseudo code: + + + $solution = sha512($hash + sha512($password)) + +#### Send Solution + +The old $hash string in the cookie has to be replaces by $solution and the cookie is to be send with the next request: + +`PUT http://host:port/mpidsserver/login` + +The server will return the user's entity in the HTTP body, e.g. + + + <Response ...> + <User name="$username" ...> + ... + </User> + </Response> + +and a new AuthToken with `$mode=SESSION` and a new expiration date and so on. This AuthToken cookie is to be send with every request. + +### Logout + +Send + +`PUT http://host:port/mpidsserver/logout` + +with a valid AuthToken cookie. No new AuthToken will be returned and no AuthToken with that `$session` will be accepted anymore. + +### Commandline solution with `curl` ## + +To use curl for talking with the server, first save your password into a variable: +`PW=$(cat)` + +The create a cookie in `cookie.txt` like this (note that this makes your password visible for a short time to everyone on your system: +`curl -X POST -c cookie.txt -D head.txt -H "Content-Type: application/x-www-form-urlencoded" -d username=<USERNAME> -d password="$PW" --insecure "https://<SERVER>/login` + +To use the cookie, pass it on with later requests: +`curl -X GET -b cookie.txt --insecure "https://<SERVER>/Entity/12345"` + diff --git a/src/doc/specification/Datatype.md b/src/doc/specification/Datatype.md new file mode 100644 index 0000000000000000000000000000000000000000..ffc9794ea36cfe748e8c11cee2bcd32adc8ebe0a --- /dev/null +++ b/src/doc/specification/Datatype.md @@ -0,0 +1,95 @@ +# Datatype + +## TEXT +* Description: TEXT stores stores any text values. +* Range: Any [utf-8](https://en.wikipedia.org/wiki/UTF-8) encodable sequence of characters with maximal 65,535 bytes. (Simply put: In most cases, any text with less than 65,535 letters and spaces will work. But if you use special characters like `à`, `€` or non-latin letters then the number of bytes, which are needed to store it, increases. Then the effective maximal length is smaller than 65,535. A bad case scenario would be a text in Chinese. Chinese characters need about three times the space of letters from the latin alphabet. Therefore, only 21845 Chinese characters can be stored within this datatype. Which is still quite a lot I guess :D) +* Examples: + * `Am Faßberg 17, D-37077 Göttingen, Germany` + * `Experiment went well until the problem with the voltmeter occured. Don't use the results after that.` + * `someone@email.org` + * `Abstract: bla bla bla ...` + * `Head of Group` + * `http://www.bmp.ds.mpg.de` + * + + A. Schlemmer, S. Berg, TK Shajahan, S. Luther, U. Parlitz, + Quantifying Spatiotemporal Complexity of Cardiac Dynamics using Ordinal Patterns, + 37th Annual International Conference of the IEEE Engineering in Medicine and Biology Society (EMBC), 2015, doi: 10.1109/EMBC.2015.7319283 + +---- + +## BOOLEAN +* Description: BOOLEAN stores boolean `TRUE` or `FALSE`. It is therefore suitable for any variable that represents that something is the case or not. +* Accepted Values: `TRUE` or `FALSE`, case insensitive (i.e. it doesn't matter if you use capitals or small letters). +* Note: You could also use a TEXT datatype to represent booleans (or even INTEGER or DOUBLE). But it makes a lot of sense to use this special datatype as it ensures that only the two possible values, `TRUE` or `FALSE` are inserted into the database. Every other input would be rejected. This helps to keep the database understandable and to avoid mistakes. + +---- + +## INTEGER +* Description: INTEGER stores integer numbers. If you need floating point variables, take a look at DOUBLE. +* Range: `-2147483648` to `2147483647`, `-0` is interpreted and stored as `0`. +* Note: This rather limited range is just provisional. It can be extended with low effort as soon as requested. + +---- + +## DOUBLE +* Description: DOUBLE stores floating point numbers with a double precision as defined by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +* Range: + * From `2.2250738585072014E-308` to `1.7976931348623157E308` (negative and positive) with a precision of 15 decimals. + * Any other decimal number _might work_ but it is not guaranteed. + * `-0`, `0`, `NaN`, `-inf` and `inf` +* Note: The server generates a warning when the precision of the submitted DOUBLE value is to high to be preserved. + +---- + +## DATETIME +The DateTime data type exists in (currently) three flavors which are dynamically chosen during parsing on the the serverside. The flavors have different ranges, support of time zones and intended use cases. Only the first two flavors are actually implemented for storage and queries. The third one is implemented for queries exclusively. + +### UTCDateTime +* Description: This DATETIME flavor stores values which represent a single point of time according to [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) with the format specified by [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) (Combined date and time). It does support [UTC Leap Seconds](https://en.wikipedia.org/wiki/Leap_second) and time zones. +* Range: From `-9999-01-01T00:00:00.0UTC` to `9999-12-31T23:59:59.999999999UTC` with nanosecond precision. +* Examples: + * `2016-01-01T13:23:00.0CEST` which means _January 1, 2016, 1:23 PM, Central European Summer Time_. + * `-800-01-01T13:23:00.0` which means _January 1, 800 BC, 1:23 PM, UTC_. +* Note: + * It is allowed to ommit the nanosecond part of a UTCDateTime (`2016-01-01T13:23:00CEST`). This indicates a precision of seconds for a UTCDateTime value. + +### Date + Description:: This DATETIME flavor stores values which represent a single date, month or year according to the [gregorian calendar](https://en.wikipedia.org/wiki/Gregorian_Calendar). A month/year is conceived as a single date with the presion of a month/year. This concept is useful if you try to understand the query semantics which are explained [elsewhere](./QueryLanguage#POVDateTime). + Format:: `Y[YYY][-MM[-dd]]` (where square brackets mean that the expression is optional). + Range:: Any valid date according to the gregorian calendar from `-9999-01-01` to `9999-12-31` (and respective dates with lower precision. E.g. the year `-9999`). There is no year `0`. +* Note: Date is a specialization of [#SemiCompleteDateTime]. + +### SemiCompleteDateTime +* Description: A generalization of the _Date_ and _UTCDateTime_ flavors. In general, there is no time zone support. Although this flavor is not yet storable in general, it is implemented for search queries yet. I.e. you might search for `FIND ... date>2015-04-03T20:15` yet. +* Format: `Y[YYY]['-MM[-dd[Thh:[mm[:ss[.ns]]]]]]]`. +* Special Properties: For every SemiCompleteDateTime _d_ there exists a _Inclusive Lower Bound_ (`d.ILB`) and a _Exclusive Upper Bound_ (`d.EUB`). That means, a SemiCompleteDateTime can be interpreted as an interval of time. E.g. `2015-01` is the half-open interval `[2015-01-01T00:00:00.0, 2016-01-01T00:00:00.0)`. ILB and EUB are UTCDateTimes respectively. These properties are important for the semantics of the the query language, especialy the [operators](./QueryLanguage#POVDateTime). + +### Future Flavors +Please file a new feature request as soon as you need them. +* Time:: For a time of the day (without the date). Supports time zones. +* FragmentaryDateTime:: For any fragmentary DateTime. That is an arbitrary combination of year, month, day of week, day of month, day of year, hour of day, minute, seconds (and nanoseconds). This flavor is useful for recurrent events like a bus schedule (_Saturday, 7:30_) or the time of a standing order for money transfer (_third day of the month_). + +---- + +## REFERENCE +* Description: REFERENCE values store the [Valid ID](./Glossary#valid-id) of an existing entity. The are useful to establish links between two entities. +* Accepted Values: Any [Valid ID](./Glossary#valid-id) or [Valid Unique Existing Name](./Glossary#valid-unique-existing-name) or [Valid Unique Temporary ID](./Glossary#valid-unique-temporary-id) or [Valid Unique Prospective Name](./Glossary#valid-unique-prospective-pame). +* Note: + * After beeing processed successfully by the server the REFERENCE value is normalized to a [Valid ID](./Glossary#valid-id). I.e. it is guaranteed that a REFERENCE value of a valid property is a positive integer. + +### FILE +* Description: A FILE is a special REFERENCE. It only allows entity IDS which belong to a File. + +### RecordType as a data type +* Furthermore, any RecordType can be used as a data type. This is a variant of the REFERENCE data type where any entity is a valid value which is a child of the RecordType in question. +* Example: + * Let `Person` be a RecordType, `Bertrand Russel` be a child of `Person`. Then `Bertrand Russel` is a valid value for a property with a `Person` data type. + +## LIST +* Description: A LIST is always a list of something which has another data type. E.g. A LIST of TEXT values, a LIST of REFERENCES value, etc. Here we call TEXT resp. REFERENCE the **Element Data Type**. The LIST data type allows you to store an arbitrary empty or non-empty ordered set (with duplicates) of values of the *same* data type into one property. Each value must be a valid value of the Element Data Type. +* Example: + * LIST of INTEGER: ```[0, 2, 4, 5, 8, 2, 3, 6, 7]``` + * LIST of Person, while `Person` is a RecordType: ```['Bertrand Russel', 'Mahatma Ghandi', 'Mother Therese']``` + + diff --git a/src/doc/specification/Fileserver.md b/src/doc/specification/Fileserver.md new file mode 100644 index 0000000000000000000000000000000000000000..fda2ec183c909e494a407f74eb1f4d03a5a23625 --- /dev/null +++ b/src/doc/specification/Fileserver.md @@ -0,0 +1,142 @@ +# Fileserver + +## Info +There are several ways to utilize the file server component of HeartDB. It is possible to upload a file or a whole folder including subfolders via HTTP and the _drop off box_. It is possible to download a file via HTTP identified by its ID or by its path in the internal file system. Furthermore, it is possible to get the files metadata via HTTP as an xml. + +## File upload +### Drop off box + +The drop off box is a directory on the HeartDB server's local file system, specified in the `server.conf` file in the server's basepath (something like `~/HeartDB/server/server.conf`). The key in the `server.conf` is called `dropoffbox`. Since the drop off box directory is writable for all, users can push their files or complete folders via a `mv` or a `cp` (recommended!) in that folder. The server deletes files older than their maximum lifetime (24 hours by default, specified `in server.conf`). But within their lifetime a user can prompt the server to pick up the file (or folder) from the drop off box in order to transfer it to the internal file system. + +Now, the user may send a pick up request to `POST http://host:port/mpidsserver/FilesDropOff` with a similar body: + + <Post> + <File pickup="$path_dropoffbox" destination="$path_filesystem" description="$description" generator="$generator"/> + ... + </Post> + +where +* $path_dropoffbox is the actual relative path of the dropped file or folder in the DropOffBox, +* $path_filesystem is the designated relative path of that object in the internal file system, +* $description is a description of the file to be uploaded, +* $generator is the tool or client used for pushing this file. + +After a successful pick up the server will return: + + <Response> + <File description="$description" path="$path" id="$id" checksum="$checksum" size="$size" /> + ... + </Response> + +where +* $id is the new generated id of that file and +* $path is the path of the submitted file or folder relative to the file system's root. + +### HTTP upload stream +#### Files + +There is an example on file upload using cURL described in detail in [the curl section of this wiki](manuals/curl/curl-access). + +File upload via HTTP is implemented in a [rfc1867](http://www.ietf.org/rfc/rfc1867.txt) consistent way. This is a de-facto standard that defines a file upload as a part of an HTML form submission. This concept shall not be amplified here. But it has to be noticed that this protocol is not designed for uploads of complete structured folders. Therefore the HeartDB file components have to impose that structure on the upload protocol. + +HeartDB's file upload resource does exclusively accept POST requests of MIME media type `multipart/form-data`. The first part of each POST body is expected to be a form-data text field, containing information about the files to be uploaded. It has to meet the following requirements: +* `Content-type: text/plain; charset=UTF-8` +* `Content-disposition: form-data; name="FileRepresentation"` + +If the content type of the first part is not `text/plain; charset=UTF-8` the server will return error 418. If the body is not actually encoded in UTF-8 the servers behaviour is not defined. If the field name of the first part is not `FileRepresentation` the server will return error 419. + +The body of that first part is to be an xml document of the following form: + + + <Post> + <File upload="$temporary_identifier" destination="$path_filesystem" description="$description" checksum="$checksum" size="$size"/> + ... + </Post> + +where +* $temporary_identifier is simply a arbitrary name, which will be used to identify this `<File>` tag with a uploaded file in the other form-data parts. +* $path_filesystem is the designated relative path of that object in the internal file system, +* $description is a description of the file to be uploaded, +* $size is the files size in bytes, +* $checksum is a SHA-512 Hash of the file. + +The other parts (which must be at least one) may have any appropriate media type. `application/octet-stream` is a good choice for it is the default for any upload file according to [rfc1867](http://www.ietf.org/rfc/rfc1867.txt). Their field name may be any name meeting the requirements of [rfc1867](http://www.ietf.org/rfc/rfc1867.txt) (most notably they must be unique within this POST). But in order to identify the corresponding xml file representation of each file the `filename` parameter of the content-disposition header has to be set to the proper $temporary_identifier. The Content-disposition type must be `form-data`: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier"` + +Finally the body of these parts have to contain the file encoded in the proper `Content-Transfer-Encoding`. + + +If a file part has a `filename` parameter which doesn't occur in the xml file representation the server will return error 420. The file will not be stored anywhere. If an xml file representation has no corresponding file to be uploaded (i.e. there is no part with the same `filename`) the server will return error 421. Some other error might occur if the checksum, the size, the destination etc. are somehow corrupted. + +#### Folders + +Uploading folders works in a similar way. The first part of the `multipart/form-data` document is to be the representation of the folders: + + + <Post> + <File upload="$temporary_identifier" destination="$path_filesystem" description="$description" checksum="$checksum" size="$size"/> + ... + </Post> + +The root folder is represented by a part which has a header of the form: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier/"` +The slash at the end of the `filename` indicates that this is a folder, not a file. Consequently, the body of this part will be ignored and should be empty. +Any file with the name `$filename` in the root folder is represented by a part which has a header of the form: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier/$filename"` +Any sub folder with the name `$subfolder` is represented by a part which has a header of the form: +* `Content-disposition: form-data; name="$any_name"; filename="$temporary_identifier/$subfolder/"` + +Likewise, a complete directory tree can be transfered by appending the structure to the `filename` header field. + +**Example**: +Given the structure + + rootfolder/ + rootfolder/file1 + rootfolder/subfolder/ + rootfolder/subfolder/file2 + +an upload document would have the following form: + + ... (HTTP Header) + Content-type: multipart/form-data, boundary=AaB03x + + --AaB03x + content-disposition: form-data; name="FileRepresentation" + + <Post> + <File upload="tmp1234" destination="$path_filesystem" description="$description" checksum="$checksum" size="$size"/> + </Post> + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/" + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/file1" + + Hello, world! This is file1. + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/subfolder/" + + --AaB03x + content-disposition: form-data; name="random_name1"; filename="temp1234/subfolder/file2" + + Hello, world! This is file2. + + --AaB03x-- + + +(**Timm 2014-06-17: to be continued**) + +## Consistency checks +To start a consistency check on either the complete file system or a subdirectory, add the `fileStorageConsistency` flag to a retrieve query. In a `GET` request, simply add `...?fileStorageConsistency=<OPTIONS>` to the URL. Possible options are (currently, only one of them?): + +- `-t <TIMEOUT>` :: The timeout for the query (in seconds?) +- `-c <TESTCASE>` :: To trigger internal test cases. +- `<PATH>` :: The path in the file system where searching files should start. If omitted or `\`, the full file system will be checked. + +One example, using curl and an existing cookie: +`curl -X GET -G -b cookie.txt -d "fileStorageConsistency=Analysis/VideoAnalysis/masks/" --insecure "https://<SERVER>/Entity/12345"` + + diff --git a/src/doc/specification/Paging.md b/src/doc/specification/Paging.md new file mode 100644 index 0000000000000000000000000000000000000000..fb994639204704618af2b1c7a8ccb32301013af3 --- /dev/null +++ b/src/doc/specification/Paging.md @@ -0,0 +1,25 @@ +# Paging +The Paging flag splits the retrieval of a (possibly huge) number entities into pages. + +## Syntax + + + flag = name, [":", value]; + name = "P"; + value = [ index ], ["L", length]]; + index = ? any positive integer ?; + length = ? any positive integer ?; + +## Semantics + +The `index` (starting with zero) denotes the index of the first entity to be retrieved. The `length` is the number of entities on that page. If `length` is omitted, the default number of entities is returned (as configured by a server contant called ...). If only the `name` is given the paging behaves as if the `index` has been zero. + +## Examples + +`http://localhost:8123/mpidsserver/Entities/all?flags=P:24L50` returns 50 entities starting with the 25th entity which would be retrieved without paging. + +`http://localhost:8123/mpidsserver/Entities/all?flags=P:24` returns the default number of entities starting with the 25th entity which would be retrieved without paging. + +`http://localhost:8123/mpidsserver/Entities/all?flags=P:L50` returns 50 entities starting with the first entity which would be retrieved without paging. + +`http://localhost:8123/mpidsserver/Entities/all?flags=P` returns the default number of entities starting with the first entity which would be retrieved without paging. diff --git a/src/doc/specification/Record.md b/src/doc/specification/Record.md new file mode 100644 index 0000000000000000000000000000000000000000..1e43e372f754d884da2ff15cfb7b71f6813f1785 --- /dev/null +++ b/src/doc/specification/Record.md @@ -0,0 +1,367 @@ +# Record + + += GET records = + +HTTP-GET-requests are to be send to `http://${host}:${port}/mpidsserver/Record/...`. Default port is 8123. + +### Get single record + +{{{ +GET http://${host}:${port}/mpidsserver/Record/1 +}}} + +returns in case of success: + + + <Response id="1353783822304" generated="2012-11-23 11:10:50.507" items="1"> + <Record id="1" generator="X23" timestamp="2012-09-22 00:56:03.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended">100111327</Property> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">Here comes the explanation</Property> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2010-01-20 20:15:00.0</Property> + </Record> + </Response> + + +If a requested record didn't exist, it returns: + + + <Response id="1354256823059" generated="2012-11-23 11:17:06.754" items="1"> + <Record id="1"> + <Error code="101" description="Record does not exist. "/> + </Record> + </Response> + +### Get multiple records + +{{{ +GET http://${host}:${port}/mpidsserver/Record/1&2 +}}} + +returns in case of success: + + + <Response id="1354570488723" generated="2012-11-23 11:12:33.666" items="2"> + <Record id="1" generator="X23" timestamp="2012-09-22 00:56:03.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended">100111327</Property> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">Here comes the explanation</Property> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2010-01-20 20:15:00.0</Property> + </Record> + <Record id="2" generator="G22" timestamp="2012-09-22 00:56:03.0" recordtypename="Heart" recordtypeid="6"> + <Property id="9" name="weightg" type="double" unit="g" description="weight in g" exponent="0" importance="obligatory">1000.0</Property> + </Record> + </Response> + +Same behavior as above if one of these didn't exist. + +## POST records + +The POST-requests are to be send to `http://${host}:${port}/mpidsserver/Record`. Default port is 8123. + +### Post single record + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="6">2012-12-24 18:00:00</Property> + <Property id="1">We investigate if Santa Clause does fit down the chimney</Property> + </Record> + </Post> + +| Remarks | +|---------| +* The obligatory root tag is `<Post>` +* A record has to have a `generator="..."` attribute and a _recordtype_ (i.e. a `recordtypeid="..."` or `recordtypename="..."` attribute). +* Depending on the _recordtype_ of the posted _record_ there are several _concrete properties_ that must be definded (they are _obligatory_). + +In case of success the response will look like: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1353708657159" generated="2012-11-23 11:55:41.223" items="1"> + <Record id="272" generator="Timm" timestamp="2012-11-23 10:55:43.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2012-12-24 18:00:00.0</Property> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">We investigate if Santa Clause does fit down the chimney</Property> + </Record> + </Response> + +| Remarks | +|---------| +* The responces contains the whole record as it lies in the database now. That entails it's final `id`, a timestamp (`generated` attribute) and a full description of the properties with name, type, descpription and so on. +* For further specifications of the _concrete properties_ see [wiki:ConcreteProperty]. + +#### Erroneous properies + +Now, there are a couple of errors that can occur (cf. [wiki:errorcodes]). The errors that occur due to corrupt concrete properies are described in [wiki:ConcreteProperty]. If any Errors occur on the level of properties, the whole record is handled as unqualified to be posted. Suppose the post body reads + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="6">Christmas Eve</Property> + <Property id="1">Does Santa Clause fit down the chimney</Property> + </Record> + </Post> + +The first property cannot be parsed to a datetime format. Therefore, the response will be + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354575364020" generated="2012-11-23 12:37:20.045" items="1"> + <Record generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="103" description="Record has unqualified Properties. " /> + ... + </Record> + </Response> + +| Remarks | +|---------| +* The error "Record has unqualified Properties" occures whenever the posted concrete properties of the record are corrupt (i.e. any `<Error>` tag is child of a `<Property>` tag). +* Erroneous records are not stored to the database. + +#### Erroneous record tags + +1) If the recordtype hasn't been indicated, the server returns + + + ... + <Error code="105" description="Record has no RecordType. " /> + ... + +2) The indicated recordtype does not exist: + + + ... + <Error code="105" description="Record's RecordType does not exist. " /> + ... +view Tickets #16 #17 + +3) The generator has not been indicated: + + + ... + <Error code="105" description="Record has no Generator. " /> + ... + +4) The record's _recordtype_ demands for a property that isn't present: + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="6">2012-12-24 18:00:00</Property> + </Record> + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354701618532" generated="2012-11-23 13:25:41.59" items="1"> + <Record generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="102" description="Obligatory Property 1 (explanation) is missing. " /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory">2012-12-24 18:00:00.0</Property> + </Record></Response> + + +5) Record does'nt entail any properties: + + + ... + <Error code="105" description="Record has no Properties. " /> + ... + +6) The indicated ID and name of a _recordtype_ are mismatching (at least one of them exists but they don't correspond to each other). + + + ... + <Error code="105" description="Record has invalid RecordType (Name/ID mismatch). " /> + ... + + +### Post multiple records + +It is possible to post several records in one go. Here we go through a few scenarios with independent sets of records and closely intertwined ones. + +#### Independent records + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + ... (properties) ... + </Record> + <Record generator="Timm" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1356209136586" generated="2012-11-30 11:33:39.897" items="2"> + <Record id="274" generator="Timm" timestamp="2012-11-30 10:33:41.0" recordtypename="Experiment" recordtypeid="1"> + ... (properties) ... + </Record> + <Record id="275" generator="Timm" timestamp="2012-11-30 10:33:41.0" recordtypename="Experiment" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Response> + +#### Non-circular interdependent records: + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="10">-1</Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354608915619" generated="2012-11-30 11:40:35.369" items="2"> + <Record id="277" generator="Timm" timestamp="2012-11-30 10:40:37.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">276</Property> + ... (more properties) ... + </Record> + <Record id="276" generator="Timm" timestamp="2012-11-30 10:40:37.0" recordtypename="Experiment" recordtypeid="1"> + ... (properties) ... + </Record> + ... (more records) ... + </Response> + +| Remarks | +|---------| +* The record that is to be referenced by another gets an id less than zero (id="-1"). This id is provisional and will be replaced by the final id, which is the one the record is stored with in the database, in the response (id="276"). + +#### Circular dependent records + +*HTTP body:* + + + <Post> + <Record id="-2" generator="Timm" recordtypeid="1"> + <Property id="10">-1</Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypeid="1"> + <Property id="10">-2</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354698071005" generated="2012-11-30 11:52:28.789" items="2"> + <Record id="278" generator="Timm" timestamp="2012-11-30 10:52:30.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">279</Property> + ... (more properties) ... + </Record><Record id="279" generator="Timm" timestamp="2012-11-30 10:52:30.0" recordtypename="Experiment" recordtypeid="1"> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">278</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Response> + +| Remarks | +|---------| +* Likewise,records that are to be referenced by each other get ids less than zero (id="-1" and id="-2"). These are provisional and will be replaced by the final ids in the response (id="278" and id="279"). + + +#### Erroneous multi-posts + +1) If any error occurs in a single record, all correct records are nevertheless stored (In future this behavior should be controlled better by an _atomic flag_. See #18). + +2) If a referenced record is corrupt, the referencing record is marked with an error tag equally. + +*HTTP body:* + + + <Post> + <Record generator="Timm" recordtypeid="1"> + <Property id="10">-1</Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypeid="1"> + <Property id="1">Explanation2</Property> + <!-- obligatory property 6 is missing --> + </Record> + ... (more records) ... + </Post> + +returns: + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1356334296695" generated="2012-11-30 12:14:37.246" items="2"> + <Record id="-2" generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="103" description="Record has unqualified Properties. " /> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested"> + <Error code="204" description="Referenced Record is unqualified. " /> + -1 + </Property> + ... (more properties) ... + </Record> + <Record id="-1" generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="102" description="Obligatory Property 6 (startDate) is missing. " /> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory">Explanation2</Property> + </Record> + ... (more records) ... + </Response> + +3) Self-referencing is not allowed. A self-referencing record will return + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1355236745833" generated="2012-11-30 12:18:45.066" items="1"> + <Record id="-2" generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="104" description="Self-referencing is not allowed. " /> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">-2</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Response> + +4) Referencing a non-existing provisional id will return (cf. #19): + + + <?xml version="1.0" encoding="UTF-8"?> + <Response id="1354990914009" generated="2012-11-30 13:07:00.45" items="1"> + <Record generator="Timm" recordtypename="Experiment" recordtypeid="1"> + <Error code="104" description="Referenced record does not exist. " /> + <Property id="10" name="experiment" type="reference" reference="1" description="reference to experiment" importance="suggested">-2</Property> + ... (more properties) ... + </Record> + ... (more records) ... + </Response> + += DELETE records = + +HTTP-DELETE-requests are to be send to `http://${host}:${port}/mpidsserver/Record/...`. Default port is 8123. + +*Example* +{{{ +DELETE http://${host}:${port}/mpidsserver/Record/1&2&3&4 +}}} + + + + + + diff --git a/src/doc/specification/RecordType.md b/src/doc/specification/RecordType.md new file mode 100644 index 0000000000000000000000000000000000000000..068c433d8c5390882176c0ff8783576127a2da2e --- /dev/null +++ b/src/doc/specification/RecordType.md @@ -0,0 +1,164 @@ +# RecordType +---- +## Overview +RecordTypes function as templates for [[Record|Records]], they provide a description for a type of Record and define which [[Property|Properties]] should be present. Properties come with an _importance_ attribute which tells the user or client program how strongly necessary the Property is. (As all other entities,) RecordTypes can be inherited from other RecordTypes (or any Entities). When RecordTypes inherit from other RecordTypes, the _inheritance_ flag tells which properties shall be inherited. + +### Importance + +Importances are more of a recommendation, Records with omitted Properties can technically still be committed. + +| *Importance* | *Meaning* | *Consequence when omitted* | +|--------------|----------------------|----------------------------| +| `OBLIGATORY` | Property must be present | *FIXME* Can be forced to be committed the server, but usually an error will be returned. | +| `RECOMMENDED` | Property should be present (makes sense for most users) | *FIXME* Usually an error will be returned? | +| `SUGGESTED` (*FIXME* the default?) | Property may be present (may make sense for some users) | *FIXME* No negative consequence? | + +*FIXME* Did I get recommended and suggested right? + +### Inheritance + +The _inheritance_ flag decides which [[Property|Properties]] are inherited from parents. + +| *Inheritance* | *Meaning* | +|----------------|-----------| +| _None_ (default) | Nothing is being inherited | +| `OBLIGATORY` | *FIXME* Properteis of importance `OBLIGATORY` and above? | +| `SUGGESTED` | *FIXME* Properteis of importance `SIGGESTED` and above, so the same effect as `ALL`? | +| `ALL` | Copy everything from the parent. | +| `FIX` | *FIXME* in `tests/test_inheritance.py` of pyint_test this is used, what does it mean???| + + +## Examples +### GET Requests +#### Single-Get +*Request:* + + GET http://localhost:8122/mpidsserver/RecordType/1 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="1" name="Experiment" description="Description Experiment"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="suggested" /> + </RecordType> + </Response> +---- +#### Multi-Get +*Request:* + + GET http://localhost:8122/mpidsserver/RecordType/1&2&3 + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="1" name="Experiment" description="Description Experiment"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="suggested" /> + </RecordType> + <RecordType id="2" name="Measurement" description="Description Measurement"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="obligatory" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="obligatory" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="suggested" /> + </RecordType> + <RecordType id="3" name="Video" description="Description Video"> + <Property id="1" name="explanation" type="text" description="explains the thing" importance="recommended" /> + <Property id="6" name="startDate" type="datetime" description="start" importance="recommended" /> + <Property id="12" name="file" type="file" description="file" importance="obligatory" /> + </RecordType> + </Response> +---- +#### Get all +*Request:* + + GET http://localhost:8122/mpidsserver/RecordType/ + GET http://localhost:8122/mpidsserver/RecordType + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response /> +[View Ticket #2](http://dev.bmp.ds.mpg.de/heartdb/ticket/2) + +---- +### POST Requests + +*Request is to be send to:* + + POST http://localhost:8122/mpidsserver/RecordType/ + POST http://localhost:8122/mpidsserver/RecordType/ +---- +#### Single-Post +*HTTP Body:* + + <Post> + <RecordType name="TimmsRecordType11" > + <Property id="3" importance="recommended"/> + <Property id="4" importance="obligatory"/> + <Property name="age" /> + </RecordType> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="40" name="TimmsRecordType11"> + <Property id="3" name="name" type="text" description="Name" importance="recommended" /> + <Property id="4" name="id" type="integer" description="identifier" importance="suggested" /> + </RecordType> + </Response> +---- +#### Multi-Post +*HTTP Body:* + + <Post> + <RecordType name="TimmsRecordType11" > + <Property id="3" importance="recommended"/> + <Property id="4" importance="obligatory"/> + <Property name="age" /> + </RecordType> + <RecordType name="TimmsRecordType12" > + <Property id="6" importance="recommended"/> + <Property id="7" importance="obligatory"/> + <Property id="15" /> + </RecordType> + </Post> + +*Response:* + + <?xml version="1.0" encoding="UTF-8"?> + <Response> + <RecordType id="40" name="TimmsRecordType11" description="null"> + <Property id="3" name="name" type="text" description="Name" importance="recommended" /> + <Property id="4" name="id" type="integer" description="identifier" importance="suggested" /> + <Property id="3" name="name" type="text" description="Name" importance="recommended" /> + <Property id="4" name="id" type="integer" description="identifier" exponent="0" importance="suggested" /> + </RecordType> + <RecordType id="41" name="TimmsRecordType12"> + <Property id="6" name="startDate" type="datetime" description="start" importance="recommended" /> + <Property id="7" name="stopDate" type="datetime" description="stop" importance="suggested" /> + <Property id="15" name="TimmsIntProperty" type="integer" unit="kg" description="This is TimmsIntProperty" importance="suggested" /> + </RecordType> + </Response> + += DELETE RecordTypes = + +HTTP-DELETE-requests are to be send to `http://${host}:${port}/mpidsserver/RecordType/...`. Default port is 8123. + +*Example* +{{{ +DELETE http://${host}:${port}/mpidsserver/RecordType/1&2&3&4 +}}} + diff --git a/src/doc/specs/Server-side-scripting.md b/src/doc/specification/Server-side-scripting.md similarity index 86% rename from src/doc/specs/Server-side-scripting.md rename to src/doc/specification/Server-side-scripting.md index 49f56af66a6b65855e36de12144955b2925a4f51..18dfe9eab0201fdd322bb2f61cbc87370ced99d5 100644 --- a/src/doc/specs/Server-side-scripting.md +++ b/src/doc/specification/Server-side-scripting.md @@ -1,21 +1,24 @@ # Server-Side Scripting API (v0.1) -The CaosDB Server can execute scripts (bash/python/perl) and compiled executables. The scripts can be invoked by a remote-procedure-call (RPC) protocol. Both, the requirements for the scripts and the RPC are described in this document. +The CaosDB Server can execute scripts (bash/python/perl) and +compiled executables. The scripts can be invoked by a remote-procedure-call +(RPC) protocol. Both, the requirements for the scripts and the +RPC are described in this document. ## Configuration of the Server -* The CaosDB Server has two relevant properties: - 1. `SERVER_SIDE_SCRIPTING_BIN_DIR` is the directory where executable scripts are to be placed. The server will not execute scripts which are out side of this directory. This directory must be readable and executable for the server. But it should not be readable or executable for anyone else. - 2. `SERVER_SIDE_SCRIPTING_WORKING_DIR` is the directory under which the server creates temporary working directories. The server needs writing, reading and executing permissions. The temporary working directories are deleted after the scripts have finished and the server has collected the results of the scripts. +The CaosDB Server has two relevant properties: -## Installing a Server-Side Script (SSS) +### `SERVER_SIDE_SCRIPTING_BIN_DIR` +is the directory where executable scripts are to be placed. The server will not execute scripts which are out side of this directory. This directory must be readable and executable for the server. But it should not be readable or executable for anyone else. +Executable files in this directory or its subdirectories are called `SSS`. -* Put your script into the `SERVER_SIDE_SCRIPTING_BIN_DIR` or in any subdirectory and make it executable. Executable files below this directory are called `SSS`. -* All other files in that directory MUST be ignored by the server, i.e. the server will never call them directly. +All other files in that directory MUST be ignored by the server, i.e. the server will never call them directly. However, they MAY contain additional data, different implementations, libraries etc. -* A symlink pointing to an executable MUST be treated as SSS, too. -### Example SERVER\_SIDE\_SCRIPTING\_BIN\_DIR +A symlink pointing to an executable MUST be treated as SSS, too. + +#### Example SERVER\_SIDE\_SCRIPTING\_BIN\_DIR ``` / @@ -34,6 +37,10 @@ The files `scripts1.py`, `scripts2.sh`, `scripts3.pl` and `another_script.py` ar Also the `script4` can be called which would result in calling `another_script.py` The script5 points to an executable which is not stored in the `SSD_DIR`. +### `SERVER_SIDE_SCRIPTING_WORKING_DIR` +is the directory under which the server creates temporary working directories. The server needs writing, reading and executing permissions. The temporary working directories are deleted after the scripts have finished and the server has collected the results of the scripts. + + ## Calling a Server-Side Script Via Remote Procedure Call * Users can invoke scripts via HTTP under the uri `https://$HOST:$HTTPS_PORT/$CONTEXT_ROOT/scripting`. The server accepts POST requests with content types `application/x-www-form-urlencoded` and `multipart/form-data`. @@ -54,7 +61,7 @@ The script5 points to an executable which is not stored in the `SSD_DIR`. * If a file form field has a field name which begins with either `-p` or `-O` the file name with prefix `.upload_files/` is passed as value of an option or as a positional argument to the script. Thus it is possible to distinguish several uploaded files from one another. * If there is a "auth-token" field present, another command line options `--auth-token=...` is appended. The value is either the string which was submitted with the POST request, or, if the value was "generate", a refreshed, valid AuthToken which authenticates as the user of the request (why? see below). -### Example HTML Form +## Example HTML Form ```html <form action="/scripting" method="post" enctype="multipart/form-data"> @@ -73,6 +80,7 @@ where the user uploads `my.conf` as `-Oconfig-file` and `my.input.tsv` as `-p1`, ``` $SERVER_SIDE_SCRIPTING_BIN_DIR/my/script.py --config-file=.upload_files/my.conf --algorithm=fast analyze .upload_files/my.input.tsv ``` + ## CaosDB Server Response The CaosDB Server responds with an xml document. The root element is the usual `/Response`. If no errors occurred (which would be represented with `/Response/Error` elements) the result of the script execution is represented as a `/Response/script/` element. diff --git a/src/doc/specification/Specification-of-the-Entity-API.md b/src/doc/specification/Specification-of-the-Entity-API.md new file mode 100644 index 0000000000000000000000000000000000000000..93b39f5333b9f80c9f4485fc00c2f827ad60792a --- /dev/null +++ b/src/doc/specification/Specification-of-the-Entity-API.md @@ -0,0 +1,191 @@ +# Specification of the Entity API +Version: 0.1.0r1 + +Author: Timm Fitschen + +Email: timm.fitschen@ds.mpg.de + +Date: 2017-12-17 + +## Introduction + +CaosDB is a database management system that stores it's data into `Entities`. An `Entity` can be thought of as the equivalent to tables, rows, columns and the tuples that fill the tables of a traditional RDBMS. Entities are not only used to store the data they also define the structure of the data. + +## Formal Definition + +An `Entity` may have + +* a `domain` +* an `id` +* a `role` +* a `name` +* a `data type` +* a `Set of Values` +* a `Set of Properties` +* a `Set of Parents` + +A `domain` contains an `Entity`. + +An `id` is an arbitrary string. + +A `role` is an arbitrary string. Especially, it may be one of the following strings: + +* `RecordType` +* `Record` +* `Relation` +* `Property` +* `File` +* `QueryTemplate` +* `Domain` +* `Unit` +* `Rule` +* `DataType` +* `Remote` + +A `name` is an arbitrary string. + +A `data type` contains an `Entity`. Note: this is not necessarily a `Data Type`. + +### Set of Values + +A `Set of Values` is a mapping from a `indices` to a finite set of `Values`. + +An `index` is an interval of non-negative integers starting with zero. + +#### Value + +A `Value` may have a `data type` and/or a `unit`. + +A `data type` is an `Entity`. Note: this is not necessarily a `Data Type`. + +A `unit` is an arbitrary string. + +### Data Type + +A `Data Type` is an `Entity` with role `DataType`. + +#### Reference Data Type + +A `Reference Data Type` is a `Data Type`. It may have a `scope`. + +A `scope` contains an `Entity`. + +#### Collection Data Type + +A `Collection Data Type` is a `Data Type`. It may have an ordered set of `elements`. + +### Record Type + +A `Record Type` is an `Entity` with role `RecordType`. + +### Record + +A `Record` is an `Entity` with role `Record`. + +### Relation + +A `Relation` is an `Entity` with role `Relation`. + +### Property + +A `Property` is an `Entity` with role `Property`. It is also refered to as `Abstract Property`. + +### File + +A `File` is an `Entity` with role `File`. + +A `File` may have + +* a `path` +* a `size` +* a `checksum` + +A `path` is an arbitrary string. + +A `size` is a non-negative integer. + +A `checksum` is an ordered pair (`method`,`result`). + +A `method` is an arbitrary string. + +A `result` is an arbitrary string. + +### QueryTemplate + +A `QueryTemplate` is an `Entity` with role `QueryTemplate`. + +### Domain + +A `Domain` is an `Entity` with role `Domain`. + +### Unit + +A `Unit` is an `Entity` with role `Unit`. + +### Rule + +A `Rule` is an `Entity` with role `Rule`. + +### Remote + +A `Remote` is an `Entity` with role `Remote`. + +### Set of Parents + +A `Set of Parents` is a set of `Parents`. + +#### Parent + +A `Parent` may contain another `Entity`. + +A `Parent` may have an `affiliation`. + +An `affiliation` may contain of the following strings: + +* `subtyping` +* `instantiation` +* `membership` +* `parthood` +* `realization` + +### Set of Properties + +A `Set of Properties` is a tripple (`index`, set of `Implemented Properties`, `Phrases`). + +An `index` is a bijective mapping from an interval of non-negative integer numbers starting with zero to the set of `Implemented Properties`. + +#### Implemented Property + +An `Implemented Property` contains another `Entity`. + +An `Implemented Property` may have an `importance`. + +An `Implemented Property` may have a `maximum cardinality`. + +An `Implemented Property` may have a `minimum cardinality`. + +An `Implemented Property` may have an `import`. + +An `importance` is an arbitrary string. It may contain of the following strings: + +* `obligatory` +* `recommended` +* `suggested` +* `fix` + +A `maximum cardinality` is a non-negative integer. + +A `minimum cardinality` is a non-negative integer. + +An `import` is an arbitrary string. It may contain of the following strings: + +* `fix` +* `none` + +#### Phrases + +`Phrases` are a mapping from the cartesian product of the `index` with itself to a `predicate`. + +A `predicate` is an arbitrary string. + + diff --git a/src/doc/specification/Specification-of-the-Message-API.md b/src/doc/specification/Specification-of-the-Message-API.md new file mode 100644 index 0000000000000000000000000000000000000000..fc08b343f67ca8c052395cb0979b02ab455e98fa --- /dev/null +++ b/src/doc/specification/Specification-of-the-Message-API.md @@ -0,0 +1,167 @@ +# Specification of the Message API +## Introduction + +API Version 0.1.0 + +A Message is a way of communication between the server and a client. The main purpose is to inform the clients about errors which occured during transactions, issue warnings when entities have a certain state or just explicitly confirm that a transaction was successful. Messages represents information that is not persistent or just the reproducible outcome of a transaction. Messages are not stored aside from logging. + +## Message Classes And Their Properties + +### Message (generic super class) + +A `Message` must be either a `Server Message` or a `Client Message`. + +A `Message` must have a `description`. A `description` is a string and a human-readable explanation of the meaning and/or purpose of the message. The description must not have leading or trailing whitespaces. The description should be kept in English. For the time being there is no mechanism to indicate that the description is written in other languages. This could be changed in later versions of this API. + +### Server Message + +A `Server Message` is a Message issued by the server. It must not be issued by clients. + +A `Server Message` may be either a `Standard Server Message` or a `Non-Standard Server Message` + +#### Standard Server Message + +A `Standard Server Message` is one of a set of predefined messages with a certain meaning. The set of these `Standard Server Messages` is maintained and documented in the Java code of the server. There should be a server resource for these definitions in order to have a always up-to-date documentation of the messages on every server. + +A `Standard Server Message` must have an `id`. An `id` is a non-empty string that uniquely identifies a standard server message. An id should consist only of ASCII compliant upper-case Latin alphabetic letters from `A` to `Z` and the underscore character `_`. +An `id` of a `Standard Server Message` must not start with the string `NSSM_`. + +A `Standard Server Message` must have a `type`. A `type` is one these strings: `Info`, `Warning`, `Error`, or `Success`. + +##### Error Message + +A `Server Message` with type `Error` is also called `Error Message` and sometimes just `Error`. An `Error Message` indicates that a request has *failed*. It informs about the reasons for that failure or the nature of the problems which occurred. The description of each error message should explain the error and indicate if and how the client can remedy the problems with her request. + +##### Warning Message + +A `Server Message` with type `Error` is also called `Warning Message` and sometime just `Warning`. A `Warning Message` indicates that certain *irregularities* occurred during the processing of the request or that the client requested something that is *not recommended but not strictly forbidden*. + +##### Info Message + +A `Server Message` with type `Info` is also called `Info Message` and sometimes just `Info`. An `Info Message` is a means to inform the client about *arbitrary events* which occurred during the processing of the request and which are *not* to be considered *erroneous* or *non-recommended*. These info messages are primarily intended to make the processing of the request more understandable for the client. Info messages are not meant to be used for debugging. + +##### Success Message + +A `Server Message` with type `Success` is also called a `Success Message`. A `Success Message` indicates the successful *state change* due to portions of a request or the whole request. A success message must not be issued if the request fails. + +#### Non-Standard Server Message + +A `Non-Standard Server Message` may be issued by any non-standard server plugin or extension. It is a placeholder for extensions to the Message API. + +A `Non-Standard Server Message` may have an `id`. An `id` is a non-empty string. It should consist only of ASCII compliant upper-case Latin alphabetic letters from `A` to `Z` and the underscore character `_`. However, the id should not be equal to any id from the set of predefined standard server messages. Furthermore, the id of a non-standard server message should start with the string `NSSM_`. + +A `Non-Standard Server Message` may have a `type`. A `type` is a non-empty string. It should consist only of ASCII compliant upper-case or lower-case Latin alphabetic letters from `a` to `z`, from `A` to `Z`, and the underscore character `_`. If the type is equal to one of the above-mentioned types, it must have the same meaning and the same effects on the request as the respective type from above. Especially, a message with type `Error` must not be issued unless the request actually fails. Likewise a `Success` must not be issued unless the request actually caused a *state change* of the server. + +### Client Message + +A `Client Message` may have an `ignore` flag. The `ignore` flag can have one of these values: `no`, `yes`, `warn`, `silent` + +A `Client Message` is a message issued by a client. It should not be issued by the server. A `Client Message` may be completely ignored by clients. A client message must not be ignored by the server. A `Client Message` which cannot be understood by the server must result in an error, unless the `ignore` flag states otherwise. + +#### Ignore Flag + +If the `ignore` flag is set to `no` the server must not ignore the client message. If the server cannot understand the client message an error must be issued. This will cause the transaction to fail. + +If the `ignore` flag is set to `yes` the server must ignore the client message. + +If the `ignore` flag is set to `warn` the server should not ignore the message. If the server cannot understand the client message, a warning must be issued. The transaction will not fail due to this warning. + +### Message Parameters + +A `Message` may have zero or more parameters. A `Message Parameter` is a a triple of a `key`, a `value`. It is intended to facilitate the processing and representation of messages by clients and the server. For example, consider an `Error Message` which states that a certain server state cannot be reached and the reason be that there is an entity with certain features. Then it is useful to refer to the entity via the +parameters. A client can now resolve the entity and just show it or generate a URI for this entity. + +A `key` is a non-empty string which should consist only of ASCII compliant lower-case Latin alphabetic letters from `a` to `z` and the minus character `-`. A `key` must be unique among the keys of the message parameters. + +A `value` is a possibly empty, arbitrary string which must not have leading or trailing white spaces. + +A `Message Parameter` may have a `type`. The `type` of a `Message Parameter` is also called a `Message Parameter Type`. A `Message Parameter Type` is a non-empty string which should consist only of ASCII compliant lower-case Latin alphabetic letters from `a` to `z` and the minus character `-`. A message parameter type may be one these string: `entity-id`, `entity-name`, `entity-cuid`, `property-index`, `parent-id`, `parent-name`. + +A `Message Parameter` with a type which begins with `entity-` is also called an `Entity Message Parameter`. The value of an `Entity Message Parameter` must refer to an entity—via its id, name, or cuid, respectively. + +A `Message Parameter` with a type which begins with `property-` is also called a `Property Message Parameter`. The value of such a parameter must refer to an entity's property. In the case of the `property-index` type the value refers to a property via a zero-based index (among the list of properties of that entity). The list of properties in question must belong to the `Message Bearer` which must in turn be an `Entity`. + +A `Message Parameter` with a type which begins with `parent-` is also called a `Parent Message Parameter`. The value of such a parameter must refer to an entity's parent via its id or name, respectively. + +### Message Bearer + +A `Message` must have a single `Message Bearer`, or, equivalently, a `Message` `belongs to` a single `Message Bearer`. The message is usually considered to carry information about the message bearer if not stated otherwise. The message's subject should be the message bearer itself, so to speak. Although, possibly indicated by a `Message Parameter` the message may be additionally or solely concerned with other things than the message bearer. Please note: The message bearer may also indicate the context of evaluation of the message parameters, e.g. when the type of the message parameter is `property-index`. + +A `Message Bearer` may be an `Entity`, a `Property`, a `Container`, a `Request`, a `Response`, or a `Transaction`. + +## Representation and Serialization + +Messages can be serialized, deserialized by the means of XML. + +### XML Representation + +A `Message` is serialized into a single XML Element Node (hereafter the *root element* with zero or more Child Nodes. + +##### Root Element Tag + +The root element's tag of a `Server Message` must be equal to its `type` if and only if the type is equal to one of the allowed types of a `Standard Server Message` (even if it is a Non-Standard Server Message). Otherwise the root tag is just 'ServerMessage'. + +```xml +<Error/><!--an Error Message--> +<Warning/><!--a Warning Message--> +<Info/><!--an Info Message--> +<Success/><!--a Success Message--> +<ServerMessage/> <!--a Non-Standard Server Message with a non-standard type--> +``` + +The root element's tag of a `Client Message` must be 'ClientMessage'. E.g. +```xml +<ClientMessage/><!--a Client Message--> +``` + +##### Root Element Attributes + +The root element must have the attributes nodes `id`, and/or `ignore` if and only if the messages have corresponding properties. The root element must have a 'type' attribute only if the message has a type property and if the type is not equal to the root element's tag. The values of the attributes must equal the corresponding properties. E.g. + +```xml +<Error id="ENTITY_DOES_NOT_EXIST" type="Error"/><!--this and the next element are equivalent--> +<Error id="ENTITY_DOES_NOT_EXIST"/> +<ServerMessage type="CustomType"/><!--has no id--> +<ServerMessage id="NSSM_MY_ID"/><!--has no type--> +``` + +or + +```xml +<ClientMessage id="CM_MY_ID" ignore="warn"/> +``` + +All other Attributes should be ignored. + +##### Description Element + +The root element must have exactly one Child Element Node with tag 'Description' if and only if the message has a `description` property. The string value of the message's description must be the first Child Text Node of the 'Description' Element. E.g. + +```xml +<ServerMessage> + <Description>This is a description.</Description> +</ServerMessage> +``` + +Please note: Any leading or trailing whitespaces of the Text Node must be stripped during the deserialization. + +All other Attributes and Child Nodes should be ignored. + +##### Parameters Element + +The root element must have exactly one Child Element Node with tag 'Parameters' if the message has at least one `parameter`. The 'Parameters' Element in turn must have a single Child Element Node for each parameter which are called `Parameter Elements`. + +A `Parameter Element` must have a tag equal to the `key` of the parameter. +It must have a `type` attribute equal to the `type` property of the parameter if and only if the parameter has a type. And it must have a first Child Text Node which is equal to the parameter's `value`. E.g. + +```xml +<ClientMessage> + <Parameters> + <param-one type="entity-name">Experiment</param-one><!--One parameter with key="param-one", value="Experiment", and type="entity-name"--> + </Parameters> +</ClientMessage> +``` + +Please note: Any leading or trailing whitespaces of the Text Node must be stripped during the deserialization. + +All other Attributes and Child Nodes below the 'Parameters' Element should be ignored. diff --git a/src/doc/specification/index.rst b/src/doc/specification/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..e9683072f555725ee8b74a0ae468c6a407e244b2 --- /dev/null +++ b/src/doc/specification/index.rst @@ -0,0 +1,22 @@ + +Specification +============= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + AbstractProperty + C-Client + Fileserver + Record + Server-side-scripting + Specification of the Entity API <Specification-of-the-Entity-API> + Authentication + Datatype + Paging + RecordType + Server side scripting <Server-side-scripting-v0.1> + Specification of the Message API <Specification-of-the-Message-API> + diff --git a/src/main/java/org/caosdb/server/CaosDBServer.java b/src/main/java/org/caosdb/server/CaosDBServer.java index cfe9ff141d3f1ef06e665e36936446c8234e27c5..e0290050257ea2885f4191e3327f753e574b3fb4 100644 --- a/src/main/java/org/caosdb/server/CaosDBServer.java +++ b/src/main/java/org/caosdb/server/CaosDBServer.java @@ -142,7 +142,7 @@ public class CaosDBServer extends Application { public static void main(final String[] args) throws SecurityException, FileNotFoundException, IOException { try { - init(args); + parseArguments(args); initScheduler(); initServerProperties(); initTimeZone(); @@ -157,7 +157,20 @@ public class CaosDBServer extends Application { } } - private static void init(final String[] args) { + /** + * Parse the command line arguments. + * + * <ul> + * <li>"nobackend": flag to run caosdb without any backend (for testing purposes) + * <li>"insecure": flag to start only a http server (no https server) + * </ul> + * + * <p>Both flags are only available in the debug mode which is controlled by the `caosdb.debug` + * JVM Property. + * + * @param args + */ + private static void parseArguments(final String[] args) { for (final String s : args) { if (s.equals("nobackend")) { START_BACKEND = false; diff --git a/src/main/java/org/caosdb/server/database/BackendTransaction.java b/src/main/java/org/caosdb/server/database/BackendTransaction.java index 5bf978343d72272309d4f4ab0f039770419c87b8..61aebf42a2dfd65fb9ed1c84459064e3aa274c06 100644 --- a/src/main/java/org/caosdb/server/database/BackendTransaction.java +++ b/src/main/java/org/caosdb/server/database/BackendTransaction.java @@ -129,6 +129,8 @@ import org.caosdb.server.database.backend.interfaces.UpdateUserImpl; import org.caosdb.server.database.backend.interfaces.UpdateUserRolesImpl; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.TransactionBenchmark; +import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.RetrieveEntity; import org.caosdb.server.utils.UndoHandler; import org.caosdb.server.utils.Undoable; @@ -295,4 +297,9 @@ public abstract class BackendTransaction implements Undoable { this.benchmark.addMeasurement(o, time); } } + + /** Return the type of transaction of an entity, e.g. "Retrieve" for a {@link RetrieveEntity}. */ + public String getTransactionType(EntityInterface e) { + return e.getClass().getSimpleName().replace("Entity", ""); + } } diff --git a/src/main/java/org/caosdb/server/database/DatabaseAccessManager.java b/src/main/java/org/caosdb/server/database/DatabaseAccessManager.java new file mode 100644 index 0000000000000000000000000000000000000000..196a9fb2b4418d81ced4b174482f8f6a5dce8ce8 --- /dev/null +++ b/src/main/java/org/caosdb/server/database/DatabaseAccessManager.java @@ -0,0 +1,331 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.database; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.ReentrantLock; +import org.caosdb.server.database.access.Access; +import org.caosdb.server.database.access.AccessControlAccess; +import org.caosdb.server.database.access.InfoAccess; +import org.caosdb.server.database.access.InitAccess; +import org.caosdb.server.database.access.TransactionAccess; +import org.caosdb.server.transaction.AccessControlTransaction; +import org.caosdb.server.transaction.TransactionInterface; +import org.caosdb.server.transaction.WriteTransactionInterface; +import org.caosdb.server.utils.Info; +import org.caosdb.server.utils.Initialization; +import org.caosdb.server.utils.Releasable; + +/** + * Acquire and release read access. {@link DatabaseAccessManager} uses this class for managing + * access to the back-end during processing updates, inserts, deletions and retrievals. Read access + * will be granted immediately for every thread that requests it, unless a thread requests that read + * access be blocked (usually by requesting write access). If this happens, all threads that already + * have read access will proceed and release their read access as usual but no NEW read permits will + * be granted. + * + * <p>This is a blockable {@link Semaphore}. Any number of threads may acquire a permit unless it is + * blocked. The blocking thread (requesting a {@link WriteAccessLock}) waits until all threads have + * released their permits. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +class ReadAccessSemaphore extends Semaphore implements Releasable { + + private static final long serialVersionUID = 4384921156838881337L; + private int acquired = 0; // how many threads have read access + Semaphore writersBlock = + new Semaphore(1, true); // This semaphore is blocked as long as there are any + // unreleased read permits. + + public ReadAccessSemaphore() { + // this is a fair semaphore with no initial permit. + super(1, true); + } + + /** + * Acquire a read access permit if and only if it has not been blocked via block(). If read access + * is currently blocked, the thread waits until the unblock() method is invoked by any thread. + */ + @Override + public void acquire() throws InterruptedException { + super.acquire(); // Protect the next few lines + if (this.acquired == 0) { + this.writersBlock.acquire(); + } + this.acquired++; + super.release(); + } + + /** + * Release a read access permit. + * + * <p>If this is the last remaining acquired permit, also release the general writersBlock. + */ + @Override + public void release() { + this.acquired--; + if (this.acquired <= 0) { // Last permit: release + this.acquired = 0; + if (this.writersBlock.availablePermits() <= 0) { + this.writersBlock.release(); + } + } + } + + /** + * Acquire the permit to block further read access, if no thread currently has acquired read + * access and no thread currently has a block. + * + * <p>Consequences of calling this method are: + * + * <ul> + * <li>Further read access permits are blocked for any thread. + * <li>Every thread that has a read access already can proceed. The current thread waits until + * all threads have released their read access. + * </ul> + * + * <p>If another thread has invoked this method before, the current thread waits until the + * unblock() method is called. + * + * @throws InterruptedException + */ + public void block() throws InterruptedException { + super.reducePermits(1); + this.writersBlock.acquire(); + } + + /** + * Unblock read access. + * + * <p>This method releases the writersBlock introduced by calling the block() method. + */ + public void unblock() { + if (this.writersBlock.availablePermits() <= 0) { + this.writersBlock.release(); + } + super.release(); + } + + public int waitingAquireAccess() { + return getQueueLength(); + } +} + +/** + * Acquire and release write access. DatabaseAccessManager uses this class for managing access to + * entities while processing updates, inserts, deletions and retrievals. Write access will be + * granted to one and only one thread if no other thread already holds read or write access permits. + * The write access seat has to be reserved before acquiring the lock. + * + * <p>The flow is as follows: + * + * <p> + * + * <pre> + * No new read + * access possible, + * Read access Read access wait for running read + * possible possible threads to fininish + * + * +------+ +----------+ +----------+ + * +--> | no | ----> | reserved | ----> | acquired | --+ + * | | Lock | +----------+ +----------+ | + * | +------+ \ / | + * | (1 seat together) | + * +-----------------------------------------------------+ + * release() + * </pre> + * + * @author Timm Fitschen + */ +class WriteAccessLock extends ReentrantLock implements Releasable { + + private static final long serialVersionUID = 833147084787201103L; + private ReadAccessSemaphore readSem = null; + private Thread reservedBy = null; + + public WriteAccessLock(final ReadAccessSemaphore readSem) { + super(); + this.readSem = readSem; + } + + /** + * Reserve the seat for the next write access. While a write access is reserved but not yet + * acquired, all read access may still be granted. When a write access has been already granted or + * another reservation is active, the thread waits until the write access has been released. + * + * @throws InterruptedException + */ + public void reserve() throws InterruptedException { + super.lock(); + this.reservedBy = Thread.currentThread(); + } + + /** + * Lock the write access seat. This method returns once all current read permits have been + * released. + */ + @Override + public void lockInterruptibly() throws InterruptedException { + if (!super.isHeldByCurrentThread()) { + super.lock(); + } + this.readSem.block(); // Wait until all current read permits have been released. + } + + @Override + public void unlock() { + if (super.isHeldByCurrentThread()) { + this.readSem.unblock(); + this.reservedBy = null; + super.unlock(); + } + } + + @Override + public void release() { + unlock(); + } + + public Thread whoHasReservedAccess() { + return this.reservedBy; + } +} + +/** + * Manages the read and write access to the database. + * + * @author tf + */ +public class DatabaseAccessManager { + + private DatabaseAccessManager() {} + + private static final DatabaseAccessManager instance = new DatabaseAccessManager(); + private final ReadAccessSemaphore readAccess = new ReadAccessSemaphore(); + private final WriteAccessLock writeAccess = new WriteAccessLock(this.readAccess); + + public static DatabaseAccessManager getInstance() { + return instance; + } + + /** + * Return the thread which has successfully reserved write access. This is the thread which will + * be the next to actually acquire write access. + * + * @return + */ + public static Thread whoHasReservedWriteAccess() { + return instance.writeAccess.whoHasReservedAccess(); + } + + /** + * Acquire read access. This method returns the Access object as soon as there are no active write + * permits. + * + * <p>The returned Access object can be used to read in the data base back-end. + * + * <p>Read access can be acquired parallel to other threads having read access or while another + * thread has <em>reserved</em> write access. As soon as any thread has requested to + * <em>acquire</em> write access, all other threads have to wait. + * + * @param t the {@link TransactionInterface} which requests the read access + * @return {@link Access} object which holds and abstract away all connection details. + * @throws InterruptedException + */ + public Access acquireReadAccess(final TransactionInterface t) throws InterruptedException { + this.readAccess.acquire(); + return new TransactionAccess(t, this.readAccess); + } + + /** + * Reserve write access. This method returns the Access object as soon as there is no other + * reserved or acquired write access. + * + * <p>The returned Access object can be used to read in the data base back-end. + * + * <p>The reservation has no effect on granting of read access permits, but only one thread may at + * any time reserve or acquire write access. + * + * @param wt - the {@link WriteTransactionInterface} which request the reservation of the write + * access. + * @return {@link Access} object which holds and abstract away all connection details. + * @throws InterruptedException + */ + public Access reserveWriteAccess(final WriteTransactionInterface wt) throws InterruptedException { + this.writeAccess.reserve(); + return new TransactionAccess(wt, this.writeAccess); + } + + /** + * Acquire write access. This method returns the Access object as soon as all already acquired + * read access permits have been released. When the write access is acquired, no other access can + * be acquired (read or write). + * + * <p>The returned Access object can be used to read and write in the data base back-end. + * + * @param wt - the {@link WriteTransactionInterface} which request the acquisition of the write + * access. + * @return {@link Access} object which holds and abstract away all connection details. + * @throws InterruptedException + */ + public Access acquireWriteAccess(final WriteTransactionInterface wt) throws InterruptedException { + this.writeAccess.lockInterruptibly(); + return wt.getAccess(); + } + + /** + * Special access to be used by {@link Info}. + * + * @param i + * @return + */ + public static Access getInfoAccess(final Info i) { + return new InfoAccess(i); + } + + /** + * Special access to be used during the {@link Initialization} of the caosdb server. + * + * @param initialization + * @return + */ + public static Access getInitAccess(final Initialization initialization) { + return new InitAccess(initialization); + } + + /** + * Special access to be used for {@link AccessControlTransaction}s, mainly authentication and + * authorization. + * + * @param t + * @return + */ + public static Access getAccountAccess(final AccessControlTransaction t) { + return new AccessControlAccess(t); + } +} diff --git a/src/main/java/org/caosdb/server/database/DatabaseMonitor.java b/src/main/java/org/caosdb/server/database/DatabaseMonitor.java deleted file mode 100644 index ed98d1d6d88e3e797ce022c0880640d9a9128a0d..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/database/DatabaseMonitor.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.database; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.concurrent.Semaphore; -import java.util.concurrent.locks.ReentrantLock; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.access.AccessControlAccess; -import org.caosdb.server.database.access.InfoAccess; -import org.caosdb.server.database.access.InitAccess; -import org.caosdb.server.database.access.TransactionAccess; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.transaction.AccessControlTransaction; -import org.caosdb.server.transaction.TransactionInterface; -import org.caosdb.server.transaction.WriteTransaction; -import org.caosdb.server.utils.Info; -import org.caosdb.server.utils.Initialization; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; -import org.caosdb.server.utils.Releasable; - -/** - * Acquire and release weak access. DatabaseMonitor uses this class for managing access to entities - * during processing updates, inserts, deletions and retrievals. Weak access will be granted - * immediately for every thread that requests it, unless one or more threads requested for blocking - * the weak access (usually due to requesting for strong access). If this happens, all threads that - * already have weak access will proceed and release their weak access as usual but no NEW permits - * will be granted. - * - * @author Timm Fitschen - */ -class WeakAccessSemaphore extends Semaphore implements Observable, Releasable { - - private static final long serialVersionUID = -262226321839837533L; - private int acquired = 0; // how many thread have weak access - Semaphore block = new Semaphore(1, true); - - public WeakAccessSemaphore() { - // this is a fair semaphore with no initial permit. - super(1, true); - } - - /** - * Acquires a weak access permit if and only if it has not been blocked via block(). If the - * WeakAccess is currently blocked, the thread wait until the unblock() method is invoked by any - * thread. - */ - @Override - public void acquire() throws InterruptedException { - super.acquire(); - if (this.acquired == 0) { - this.block.acquire(); - } - this.acquired++; - notifyObservers(null); - super.release(); - } - - /** Releases a weak access permit. */ - @Override - public void release() { - this.acquired--; - notifyObservers(null); - if (this.acquired <= 0) { - this.acquired = 0; - if (this.block.availablePermits() <= 0) { - this.block.release(); - } - } - } - - /** - * Acquires the permit of a block of WeakAccess if no thread currently has a WeakAccess and no - * thread currently has a block. I.e. it blocks the further permission of weak access for any - * thread. Every thread that has a weak access yet can proceed. The current thread waits until any - * thread has released its weak access. If another thread has invoked this method yet the current - * thread waits until the unblock() method is called. - * - * @throws InterruptedException - */ - public void block() throws InterruptedException { - super.reducePermits(1); - this.block.acquire(); - } - - /** - * Unblock WeakAccess. - * - * @throws InterruptedException - */ - public void unblock() { - if (this.block.availablePermits() <= 0) { - this.block.release(); - } - super.release(); - } - - public int waitingAquireAccess() { - return getQueueLength(); - } - - public int acquiredAccess() { - return this.acquired; - } - - LinkedList<Observer> observers = new LinkedList<Observer>(); - - @Override - public boolean acceptObserver(final Observer o) { - return this.observers.add(o); - } - - @Override - public void notifyObservers(final String e) { - final Iterator<Observer> it = this.observers.iterator(); - while (it.hasNext()) { - final Observer o = it.next(); - if (!o.notifyObserver(e, this)) { - it.remove(); - } - } - } -} - -/** - * Acquire and release strong access. DatabaseMonitor uses this class for managing access to - * entities during processing updates, inserts, deletions and retrievals. Strong access will be - * granted to one and only one thread if no other thread yet holds weak or strong access permits. - * The strong access has to be allocated before requesting it to be permitted. See below. - * - * @author Timm Fitschen - */ -class StrongAccessLock extends ReentrantLock implements Observable, Releasable { - - private static final long serialVersionUID = -262226321839837533L; - private WeakAccessSemaphore wa = null; - private Thread allocator = null; - private Thread acquirer = null; - - public StrongAccessLock(final WeakAccessSemaphore wa) { - super(); - this.wa = wa; - } - - /** - * Allocates the strong access permits. While a strong access is allocated but not yet acquired - * any weak access may still be granted. When a strong access is yet granted or another allocation - * is still active, the thread waits until the strong access has been released. - * - * @throws InterruptedException - */ - public void allocate() throws InterruptedException { - super.lock(); - this.allocator = Thread.currentThread(); - notifyObservers(null); - } - - @Override - public void lockInterruptibly() throws InterruptedException { - if (!super.isHeldByCurrentThread()) { - super.lock(); - } - this.acquirer = Thread.currentThread(); - notifyObservers(null); - this.wa.block(); - } - - @Override - public void unlock() { - if (super.isHeldByCurrentThread()) { - this.wa.unblock(); - this.allocator = null; - this.acquirer = null; - notifyObservers(null); - super.unlock(); - } - } - - @Override - public void release() { - unlock(); - } - - LinkedList<Observer> observers = new LinkedList<Observer>(); - - @Override - public boolean acceptObserver(final Observer o) { - return this.observers.add(o); - } - - @Override - public void notifyObservers(final String e) { - final Iterator<Observer> it = this.observers.iterator(); - while (it.hasNext()) { - final Observer o = it.next(); - if (!o.notifyObserver(e, this)) { - it.remove(); - } - } - } - - public Thread whoHasAllocatedAccess() { - return this.allocator; - } - - public Thread whoHasAcquiredAccess() { - return this.acquirer; - } - - public int waitingAllocateAccess() { - return getQueueLength(); - } -} - -/** - * Manages the read and write access to the database. - * - * @author tf - */ -public class DatabaseMonitor { - - private DatabaseMonitor() {} - - private static final DatabaseMonitor instance = new DatabaseMonitor(); - private final WeakAccessSemaphore wa = new WeakAccessSemaphore(); - private final StrongAccessLock sa = new StrongAccessLock(this.wa); - - public static DatabaseMonitor getInstance() { - return instance; - } - - public void acquireWeakAccess() throws InterruptedException { - this.wa.acquire(); - } - - public void releaseWeakAccess() { - this.wa.release(); - } - - public void allocateStrongAccess() throws InterruptedException { - this.sa.allocate(); - } - - public void acquireStrongAccess() throws InterruptedException { - this.sa.lockInterruptibly(); - } - - public void releaseStrongAccess() { - this.sa.unlock(); - } - - public static final void acceptWeakAccessObserver(final Observer o) { - instance.wa.acceptObserver(o); - } - - public static final void acceptStrongAccessObserver(final Observer o) { - instance.sa.acceptObserver(o); - } - - public static int waitingAllocateStrongAccess() { - return instance.sa.waitingAllocateAccess(); - } - - public static int waitingAcquireWeakAccess() { - return instance.wa.waitingAquireAccess(); - } - - public static int acquiredWeakAccess() { - return instance.wa.acquiredAccess(); - } - - public static Thread whoHasAllocatedStrongAccess() { - return instance.sa.whoHasAllocatedAccess(); - } - - public static Thread whoHasAcquiredStrongAccess() { - return instance.sa.whoHasAcquiredAccess(); - } - - public Access acquiredWeakAccess(final TransactionInterface t) { - acquiredWeakAccess(); - return new TransactionAccess(t, this.wa); - } - - public Access allocateStrongAccess(final WriteTransaction<? extends TransactionContainer> wt) - throws InterruptedException { - allocateStrongAccess(); - return new TransactionAccess(wt, this.sa); - } - - public Access acquireStrongAccess(final WriteTransaction<? extends TransactionContainer> wt) - throws InterruptedException { - acquireStrongAccess(); - return wt.getAccess(); - } - - public static Access getInfoAccess(final Info i) { - return new InfoAccess(i); - } - - public static Access getInitAccess(final Initialization initialization) { - return new InitAccess(initialization); - } - - public static Access getAccountAccess(final AccessControlTransaction t) { - return new AccessControlAccess(t); - } -} diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java index 2ad36235bd16e1cecde9908d334f643ebc127e54..b1f445e2b8ad2d79b756e1dd662296d8e6c9a49c 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java @@ -55,7 +55,7 @@ public class MySQLHelper implements DBHelper { * * <p>In the database, this adds a row to the transaction table with SRID, user and timestamp. */ - public void initTransaction(Connection connection, WriteTransaction<?> transaction) + public void initTransaction(Connection connection, WriteTransaction transaction) throws SQLException { try (CallableStatement call = connection.prepareCall("CALL set_transaction(?,?,?,?,?)")) { @@ -89,7 +89,7 @@ public class MySQLHelper implements DBHelper { } else if (transaction instanceof WriteTransaction) { connection.setReadOnly(false); connection.setAutoCommit(false); - initTransaction(connection, (WriteTransaction<?>) transaction); + initTransaction(connection, (WriteTransaction) transaction); } else { connection.setReadOnly(false); connection.setAutoCommit(true); diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityTransaction.java similarity index 92% rename from src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityTransaction.java index 7f3d3435e1d4913d99ea0c9b7b064da85e12ac02..f8ebcd2af6ff73dd11f1b845d7e5aef58ff925f3 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityTransaction.java @@ -27,11 +27,11 @@ import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; -public class DeleteEntity extends BackendTransaction { +public class DeleteEntityTransaction extends BackendTransaction { private final TransactionContainer container; - public DeleteEntity(final TransactionContainer container) { + public DeleteEntityTransaction(final TransactionContainer container) { this.container = container; } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityTransaction.java similarity index 94% rename from src/main/java/org/caosdb/server/database/backend/transaction/InsertEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityTransaction.java index 42439aa54e06e2556cb7b814f1c9056afb35f9d7..9c86ba33bf47b8051bc365bb32a6f36d0342ff92 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityTransaction.java @@ -27,11 +27,11 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.utils.EntityStatus; -public class InsertEntity extends BackendTransaction { +public class InsertEntityTransaction extends BackendTransaction { private final TransactionContainer container; - public InsertEntity(final TransactionContainer container) { + public InsertEntityTransaction(final TransactionContainer container) { this.container = container; } diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java index db7393b04f0afe3c15622dfb750cf31d42050521..9b798de4279de8034168c4ecfbc1738c70370a65 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertTransactionHistory.java @@ -30,22 +30,24 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.utils.EntityStatus; +/** + * Record the current transaction in the entities transaction history. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class InsertTransactionHistory extends BackendTransaction { private final TransactionContainer container; - private final String transaction; private final UTCDateTime datetime; private final String user; private final String realm; public InsertTransactionHistory( final TransactionContainer container, - final String transaction, final String realm, final String user, final UTCDateTime timestamp) { this.container = container; - this.transaction = transaction; this.user = user; this.datetime = timestamp; this.realm = realm; @@ -60,7 +62,7 @@ public class InsertTransactionHistory extends BackendTransaction { || e.getEntityStatus() == EntityStatus.VALID) { t.execute( - this.transaction, + getTransactionType(e), this.realm, this.user, this.datetime.getUTCSeconds(), diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTransaction.java similarity index 51% rename from src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTransaction.java index f9b6356adfcbe42760f272c4540c25489d060446..6bac0ff187fe2a08d2d0c1c7450ac954bc023d5e 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTransaction.java @@ -1,20 +1,24 @@ /* - * ** header v3.0 This file is a part of the CaosDB Project. + * ** header v3.0 + * This file is a part of the CaosDB Project. * - * Copyright (C) 2018 Research Group Biomedical Physics, Max-Planck-Institute for Dynamics and - * Self-Organization Göttingen Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> Copyright - * (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020-2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com> * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero General Public License as published by the Free Software Foundation, either version 3 - * of the License, or (at your option) any later version. + * 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. + * 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/>. + * 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 */ @@ -24,7 +28,8 @@ import java.util.LinkedList; import java.util.List; import org.caosdb.server.database.BackendTransaction; import org.caosdb.server.database.exceptions.EntityDoesNotExistException; -import org.caosdb.server.datatype.ReferenceDatatype; +import org.caosdb.server.datatype.CollectionValue; +import org.caosdb.server.datatype.IndexedSingleValue; import org.caosdb.server.datatype.ReferenceValue; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; @@ -48,21 +53,21 @@ import org.caosdb.server.utils.EntityStatus; * * @author Timm Fitschen <t.fitschen@indiscale.com> */ -public class RetrieveFullEntity extends BackendTransaction { +public class RetrieveFullEntityTransaction extends BackendTransaction { private final Container<? extends EntityInterface> container; - public RetrieveFullEntity(final EntityInterface entity) { + public RetrieveFullEntityTransaction(final EntityInterface entity) { final Container<EntityInterface> c = new Container<>(); c.add(entity); this.container = c; } - public RetrieveFullEntity(final Container<? extends EntityInterface> container) { + public RetrieveFullEntityTransaction(final Container<? extends EntityInterface> container) { this.container = container; } - public RetrieveFullEntity(Integer id) { + public RetrieveFullEntityTransaction(Integer id) { this(new RetrieveEntity(id)); } @@ -113,6 +118,53 @@ public class RetrieveFullEntity extends BackendTransaction { } } + /** + * Recursively resolve the reference values of the list of reference property `p` (but only the + * selected sub-properties). + */ + private void resolveReferenceListProperty( + Property p, List<Selection> selections, String propertyName) { + try { + p.parseValue(); + } catch (Message m) { + p.addError(m); + } + + CollectionValue values = (CollectionValue) p.getValue(); + for (IndexedSingleValue sv : values) { + resolveReferenceValue((ReferenceValue) sv.getWrapped(), selections, propertyName); + } + } + + /** + * Recursively resolve the reference values of the reference property `p` (but only the selected + * sub-properties). + */ + private void resolveReferenceProperty( + Property p, List<Selection> selections, String propertyName) { + try { + p.parseValue(); + } catch (Message m) { + p.addError(m); + } + + resolveReferenceValue((ReferenceValue) p.getValue(), selections, propertyName); + } + + /** + * Recursively resolve the reference value. + * + * <p>To be called by {@link #resolveReferenceListProperty(Property, List, String)} and {@link + * #resolveReferenceProperty(Property, List, String)}. + */ + private void resolveReferenceValue( + ReferenceValue value, List<Selection> selections, String propertyName) { + RetrieveEntity ref = new RetrieveEntity(value.getId()); + // recursion! (Only for the matching selections) + retrieveFullEntity(ref, getSubSelects(selections, propertyName)); + value.setEntity(ref, true); + } + /** * Retrieve the Entities which match the selections and are referenced by the Entity 'e'. * @@ -122,55 +174,47 @@ public class RetrieveFullEntity extends BackendTransaction { public void retrieveSubEntities(EntityInterface e, List<Selection> selections) { for (final Selection s : selections) { String propertyName = s.getSelector(); - if (s.getSubselection() != null) { - - // Find matching (i.e. referencing) Properties - for (Property p : e.getProperties()) { - // get reference properties by name. - if (propertyName.equalsIgnoreCase(p.getName()) - && p.getDatatype() instanceof ReferenceDatatype) { - if (p.getValue() != null) { - try { - p.parseValue(); - } catch (Message m) { - p.addError(m); + for (Property p : e.getProperties()) { + if (s.getSubselection() != null) { + // The presence of sub-selections means that the properties are + // expected to be references (list or plain). + if (p.getValue() != null) { + if (propertyName.equalsIgnoreCase(p.getName())) { + if (p.isReference()) { + // handle (single) reference properties with matching name... + resolveReferenceProperty(p, selections, propertyName); + continue; + } else if (p.isReferenceList()) { + // handle (list) reference properties with matching name... + resolveReferenceListProperty(p, selections, propertyName); + continue; } - - ReferenceValue value = (ReferenceValue) p.getValue(); - RetrieveEntity ref = new RetrieveEntity(value.getId()); - // recursion! (Only for the matching selections) - retrieveFullEntity(ref, getSubSelects(selections, propertyName)); - value.setEntity(ref, true); - } - continue; - } - try { - boolean isSubtype = execute(new IsSubType(p.getId(), propertyName)).isSubType(); - if (isSubtype) { - if (p.getValue() != null) { - if (p.getDatatype() instanceof ReferenceDatatype) { - try { - p.parseValue(); - } catch (Message m) { - p.addError(m); + } else { + try { + boolean isSubtype = execute(new IsSubType(p.getId(), propertyName)).isSubType(); + if (isSubtype) { + // ... handle reference properties that are a subtype of `propertyName`. + if (p.getValue() != null) { + if (p.isReference()) { + resolveReferenceProperty(p, selections, propertyName); + } else if (p.isReferenceList()) { + resolveReferenceListProperty(p, selections, propertyName); + } } - ReferenceValue value = (ReferenceValue) p.getValue(); - RetrieveEntity ref = new RetrieveEntity(value.getId()); - // recursion! (Only for the matching selections) - retrieveFullEntity(ref, getSubSelects(selections, propertyName)); - value.setEntity(ref, true); + // the name is set the the super-types name! Otherwise, clients + // wouldn't know what this property is supposed to be. p.setName(propertyName); } + } catch (EntityDoesNotExistException exc) { + // unknown parent name. } } - } catch (EntityDoesNotExistException exc) { - // unknown parent name. } - } - } else { - for (Property p : e.getProperties()) { + } else { + // no subselections - no need to resolve any references... if (!propertyName.equalsIgnoreCase(p.getName())) { + // ... we only need to cover property sub-typing try { boolean isSubtype = execute(new IsSubType(p.getId(), propertyName)).isSubType(); if (isSubtype) { diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java b/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java index 2bc6a92a2449a8ba2d42119c0ba743129bcee2ac..c73023bd4429db19ca0a968aa99b657f0d0cbea4 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/RuleLoader.java @@ -33,9 +33,16 @@ import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.proto.Rule; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.Job; import org.caosdb.server.transaction.Transaction; +/** + * Load transaction rules (e.g. "Referenced entities must always exist") and their configuration + * from the back-end. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Rule>> { private static final ICacheAccess<String, ArrayList<Rule>> cache = @@ -45,6 +52,7 @@ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Ru private final Integer entity; private final Integer domain; private ArrayList<Job> jobs; + private String transactionType; public RuleLoader( final Integer domain, @@ -55,18 +63,17 @@ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Ru this.domain = domain; this.entity = entity; this.e = e; + if (e instanceof Property) { + this.transactionType = getTransactionType(((Property) e).getDomainEntity()); + } else { + this.transactionType = getTransactionType(e); + } this.transaction = transaction; } @Override protected String getKey() { - return "<" - + this.domain - + "," - + this.entity - + "," - + this.transaction.getClass().getSimpleName() - + ">"; + return "<" + this.domain + "," + this.entity + "," + this.transactionType + ">"; } public ArrayList<Job> getJobs() { @@ -76,7 +83,7 @@ public class RuleLoader extends CacheableBackendTransaction<String, ArrayList<Ru @Override public ArrayList<Rule> executeNoCache() throws TransactionException { final RuleLoaderImpl t = getImplementation(RuleLoaderImpl.class); - return t.executeNoCache(this.domain, this.entity, this.transaction.getClass().getSimpleName()); + return t.executeNoCache(this.domain, this.entity, this.transactionType); } @Override diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntityTransaction.java similarity index 93% rename from src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntity.java rename to src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntityTransaction.java index 2bf965575884f29640a80ec3d3ab284b7779c8f2..275947abd9b42a2424141bc28111889859fb1475 100644 --- a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntity.java +++ b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateEntityTransaction.java @@ -28,11 +28,11 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.utils.EntityStatus; -public class UpdateEntity extends BackendTransaction { +public class UpdateEntityTransaction extends BackendTransaction { private final TransactionContainer container; - public UpdateEntity(final TransactionContainer container) { + public UpdateEntityTransaction(final TransactionContainer container) { this.container = container; } diff --git a/src/main/java/org/caosdb/server/datatype/SQLiteDatatype.java b/src/main/java/org/caosdb/server/datatype/SQLiteDatatype.java deleted file mode 100644 index ffd4341013176992e802e8e0c8c4bc2d58b78ad3..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/datatype/SQLiteDatatype.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.datatype; - -@DatatypeDefinition(name = "SQLite") -public class SQLiteDatatype extends ReferenceDatatype {} diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java index 845bb25a3cee665fd8f0985c6969f53dfc868025..aa4c96127cfc97b03f308a50cb587dc12949c105 100644 --- a/src/main/java/org/caosdb/server/entity/Entity.java +++ b/src/main/java/org/caosdb/server/entity/Entity.java @@ -41,6 +41,7 @@ import org.caosdb.server.datatype.AbstractCollectionDatatype; import org.caosdb.server.datatype.AbstractDatatype; import org.caosdb.server.datatype.CollectionValue; import org.caosdb.server.datatype.GenericValue; +import org.caosdb.server.datatype.ReferenceDatatype; import org.caosdb.server.datatype.Value; import org.caosdb.server.entity.Message.MessageType; import org.caosdb.server.entity.container.ParentContainer; @@ -1159,4 +1160,16 @@ public class Entity extends AbstractObservable implements EntityInterface { } return getId().toString(); } + + @Override + public boolean isReference() { + return this.hasDatatype() && this.getDatatype() instanceof ReferenceDatatype; + } + + @Override + public boolean isReferenceList() { + return this.hasDatatype() + && this.getDatatype() instanceof AbstractCollectionDatatype + && ((AbstractCollectionDatatype) getDatatype()).getDatatype() instanceof ReferenceDatatype; + } } diff --git a/src/main/java/org/caosdb/server/entity/EntityInterface.java b/src/main/java/org/caosdb/server/entity/EntityInterface.java index ef97630808536f367869851c5be06d05e6247ac2..954bcc47ca5b6117d3164ba3c9c585dbd373d4f8 100644 --- a/src/main/java/org/caosdb/server/entity/EntityInterface.java +++ b/src/main/java/org/caosdb/server/entity/EntityInterface.java @@ -194,4 +194,13 @@ public interface EntityInterface public abstract void setVersion(Version version); public abstract void addToElement(Element element, SetFieldStrategy strategy); + + /** Return true iff the data type is present and is an instance of ReferenceDatatype. */ + public abstract boolean isReference(); + + /** + * Return true iff the data type is present, an instance of AbstractCollectionDatatype and the + * AbstractCollectionDatatype's elements' data type is an instance of ReferenceDatatype. + */ + public abstract boolean isReferenceList(); } diff --git a/src/main/java/org/caosdb/server/entity/FileProperties.java b/src/main/java/org/caosdb/server/entity/FileProperties.java index 0c10288e0cbaa6a4f55a2959f72ac09db9d6f394..c2c2c10fae44d4d84af56a33a052f5ed589debed 100644 --- a/src/main/java/org/caosdb/server/entity/FileProperties.java +++ b/src/main/java/org/caosdb/server/entity/FileProperties.java @@ -289,4 +289,8 @@ public class FileProperties { public void removeOnCleanUp(final String tempPath) { this.tempPath = tempPath; } + + public boolean hasTmpIdentifier() { + return this.tmpIdentifyer != null; + } } diff --git a/src/main/java/org/caosdb/server/entity/InsertEntity.java b/src/main/java/org/caosdb/server/entity/InsertEntity.java index 56eeb12b1c2e7b9d5259a4d418e87c79e8068f51..4f7c33015e6ba70fb3d062ff4b32c27ebd80b8a8 100644 --- a/src/main/java/org/caosdb/server/entity/InsertEntity.java +++ b/src/main/java/org/caosdb/server/entity/InsertEntity.java @@ -29,4 +29,8 @@ public class InsertEntity extends WritableEntity { public InsertEntity(final Element element) { super(element); } + + public InsertEntity(String name, Role role) { + super(name, role); + } } diff --git a/src/main/java/org/caosdb/server/entity/ValidEntity.java b/src/main/java/org/caosdb/server/entity/ValidEntity.java deleted file mode 100644 index 6378e044e65afe4b7340cefd04e0b09153e5ca94..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/ValidEntity.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity; - -public class ValidEntity extends Entity { - - public ValidEntity(final int id) { - super(id); - } -} diff --git a/src/main/java/org/caosdb/server/entity/WritableEntity.java b/src/main/java/org/caosdb/server/entity/WritableEntity.java index d9dc3edc4fc1fa886370bafb5c40ce0e732b3e4b..ab8ccbb995430e267e84d1f2a235c0d928e6762d 100644 --- a/src/main/java/org/caosdb/server/entity/WritableEntity.java +++ b/src/main/java/org/caosdb/server/entity/WritableEntity.java @@ -29,4 +29,8 @@ public class WritableEntity extends Entity { public WritableEntity(final Element element) { super(element); } + + public WritableEntity(String name, Role role) { + super(name, role); + } } diff --git a/src/main/java/org/caosdb/server/entity/container/DeleteContainer.java b/src/main/java/org/caosdb/server/entity/container/DeleteContainer.java deleted file mode 100644 index b0c2f21ff3b0e0dd96e24f0ef042a29e843c54f0..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/container/DeleteContainer.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity.container; - -import java.util.HashMap; -import org.apache.shiro.subject.Subject; -import org.caosdb.server.entity.DeleteEntity; - -public class DeleteContainer extends EntityByIdContainer { - - private static final long serialVersionUID = -1279198934417710461L; - - public DeleteContainer( - final Subject user, - final Long timestamp, - final String srid, - final HashMap<String, String> flags) { - super(user, timestamp, srid, flags); - } - - @Override - public void add(final int id) { - add(new DeleteEntity(id)); - } - - @Override - public void add(int id, String version) { - add(new DeleteEntity(id, version)); - } -} diff --git a/src/main/java/org/caosdb/server/entity/container/InsertContainer.java b/src/main/java/org/caosdb/server/entity/container/InsertContainer.java deleted file mode 100644 index 99128a8a56758e67ce1529785de0151b1fe624d4..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/container/InsertContainer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity.container; - -import java.util.HashMap; -import org.apache.shiro.subject.Subject; -import org.caosdb.server.entity.InsertEntity; -import org.jdom2.Element; - -public class InsertContainer extends WritableContainer { - - public InsertContainer( - final Subject user, - final Long timestamp, - final String srid, - final HashMap<String, String> flags) { - super(user, timestamp, srid, flags); - } - - private static final long serialVersionUID = 8896630069350477274L; - - @Override - public void add(final Element entity) { - add(new InsertEntity(entity)); - } -} diff --git a/src/main/java/org/caosdb/server/entity/container/UpdateContainer.java b/src/main/java/org/caosdb/server/entity/container/UpdateContainer.java deleted file mode 100644 index 1c2a807b7759ff680d30ed0f1f9f14d5952e98f8..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/entity/container/UpdateContainer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.entity.container; - -import java.util.HashMap; -import org.apache.shiro.subject.Subject; -import org.caosdb.server.entity.UpdateEntity; -import org.jdom2.Element; - -public class UpdateContainer extends WritableContainer { - - private static final long serialVersionUID = 8489287672264669883L; - - public UpdateContainer( - final Subject user, - final Long timestamp, - final String srid, - final HashMap<String, String> flags) { - super(user, timestamp, srid, flags); - } - - @Override - public void add(final Element entity) { - add(new UpdateEntity(entity)); - } -} diff --git a/src/main/java/org/caosdb/server/entity/container/WritableContainer.java b/src/main/java/org/caosdb/server/entity/container/WritableContainer.java index 2f9148651c890a0e7188acb035bf8fc55609834b..5fdb8c39ec7f01280278bec0a1f2928bfed7f5c2 100644 --- a/src/main/java/org/caosdb/server/entity/container/WritableContainer.java +++ b/src/main/java/org/caosdb/server/entity/container/WritableContainer.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,12 +25,12 @@ package org.caosdb.server.entity.container; import java.util.HashMap; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.caosdb.server.entity.FileProperties; -import org.jdom2.Element; -public abstract class WritableContainer extends TransactionContainer { - private static final long serialVersionUID = 3011242254959617091L; +public class WritableContainer extends TransactionContainer { + private static final long serialVersionUID = -4097777313518959519L; public WritableContainer( final Subject user, @@ -38,7 +40,9 @@ public abstract class WritableContainer extends TransactionContainer { super(user, timestamp, srid, flags); } - public abstract void add(final Element entity); + public WritableContainer() { + this(SecurityUtils.getSubject(), System.currentTimeMillis(), null, null); + } @Override public void addFile(final String uploadId, final FileProperties fileProperties) { diff --git a/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java b/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java index e042c2326cc43e5d98cc3a27b61c21301bf9e90d..c83cb67883f5a616109268fb6d62bae40cd73e6b 100644 --- a/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java +++ b/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java @@ -580,4 +580,14 @@ public class EntityWrapper implements EntityInterface { public void addToElement(Element element, SetFieldStrategy strategy) { this.entity.addToElement(element, strategy); } + + @Override + public boolean isReference() { + return this.entity.isReference(); + } + + @Override + public boolean isReferenceList() { + return this.entity.isReferenceList(); + } } diff --git a/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java index a829d711aa6345a16c9c368db27cb2f146f044b8..89420c596894da9be35e33a507821c96d10f5b7d 100644 --- a/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java +++ b/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java @@ -4,8 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020-2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,6 +25,8 @@ package org.caosdb.server.entity.xml; import org.apache.shiro.SecurityUtils; +import org.caosdb.server.datatype.CollectionValue; +import org.caosdb.server.datatype.IndexedSingleValue; import org.caosdb.server.datatype.ReferenceValue; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; @@ -137,8 +139,7 @@ public class EntityToElementStrategy implements ToElementStrategy { // CheckValueParsable job. } - if (entity.getValue() instanceof ReferenceValue - && setFieldStrategy.isToBeSet("_referenced")) { + if (entity.isReference() && setFieldStrategy.isToBeSet("_referenced")) { // Append the complete entity. This needs to be done when we are // processing SELECT Queries. EntityInterface ref = ((ReferenceValue) entity.getValue()).getEntity(); @@ -151,6 +152,26 @@ public class EntityToElementStrategy implements ToElementStrategy { // adding the reference id as well. return; } + } else if (entity.isReferenceList() && setFieldStrategy.isToBeSet("_referenced")) { + // Append the all referenced entities. This needs to be done when we are + // processing SELECT Queries. + boolean skipValue = false; + for (IndexedSingleValue sv : ((CollectionValue) entity.getValue())) { + EntityInterface ref = ((ReferenceValue) sv.getWrapped()).getEntity(); + if (ref != null) { + if (entity.hasDatatype()) { + setDatatype(entity, element); + } + Element valueElem = new Element("Value"); + ref.addToElement(valueElem, setFieldStrategy); + element.addContent(valueElem); + skipValue = true; + } + } + if (skipValue) + // the referenced entity has been appended. Return here to suppress + // adding the reference id as well. + return; } if (setFieldStrategy.isToBeSet("value")) { diff --git a/src/main/java/org/caosdb/server/jobs/Job.java b/src/main/java/org/caosdb/server/jobs/Job.java index 5d8dd5d98a00960561c6b97f8e1f4fa118eafd0b..28dc512b669eb6f1df0cfd24e32e2051f734b697 100644 --- a/src/main/java/org/caosdb/server/jobs/Job.java +++ b/src/main/java/org/caosdb/server/jobs/Job.java @@ -37,7 +37,7 @@ import org.caosdb.server.CaosDBException; import org.caosdb.server.database.BackendTransaction; import org.caosdb.server.database.backend.transaction.GetIDByName; import org.caosdb.server.database.backend.transaction.IsSubType; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.database.backend.transaction.RetrieveParents; import org.caosdb.server.database.backend.transaction.RetrieveSparseEntity; import org.caosdb.server.database.backend.transaction.RuleLoader; @@ -218,7 +218,7 @@ public abstract class Job extends AbstractObservable implements Observer { } protected final EntityInterface retrieveValidEntity(Integer id) { - return execute(new RetrieveFullEntity(id)).getContainer().get(0); + return execute(new RetrieveFullEntityTransaction(id)).getContainer().get(0); } protected final Integer retrieveValidIDByName(final String name) { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java b/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java index e2dd63b1d733d737e09a6b4e86f3fe4ae4bd0e30..037b38b90c94827265d47876e722686d5d030cc0 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckEntityACLRoles.java @@ -26,10 +26,10 @@ import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.caosdb.server.accessControl.AuthenticationUtils; import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.jobs.ContainerJob; import org.caosdb.server.permissions.EntityACI; -import org.caosdb.server.transaction.Insert; -import org.caosdb.server.transaction.Update; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -37,12 +37,9 @@ public class CheckEntityACLRoles extends ContainerJob { @Override protected void run() { - if (!(getTransaction() instanceof Update || getTransaction() instanceof Insert)) { - return; - } - for (final EntityInterface entity : getContainer()) { - if (entity.getEntityACL() != null) { + if ((entity instanceof UpdateEntity || entity instanceof InsertEntity) + && entity.getEntityACL() != null) { for (final EntityACI aci : entity.getEntityACL().getRules()) { if (!AuthenticationUtils.isResponsibleAgentExistent(aci.getResponsibleAgent())) { if (CaosDBServer.getServerProperty(ServerProperties.KEY_CHECK_ENTITY_ACL_ROLES_MODE) diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java index c592eca81394e4a92582fd3d2286b5ac0507dcce..7aca2bac3dd3709006497f9ce8f556f267c4ca81 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java @@ -22,7 +22,7 @@ */ package org.caosdb.server.jobs.core; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.StatementStatus; import org.caosdb.server.jobs.EntityJob; @@ -48,7 +48,7 @@ public class CheckParOblPropPresent extends EntityJob { runJobFromSchedule(getEntity(), CheckParValid.class); if (getEntity().hasParents()) { - execute(new RetrieveFullEntity(getEntity().getParents())); + execute(new RetrieveFullEntityTransaction(getEntity().getParents())); } // inherit properties diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java index 42476f87b43496e162b6eb036480b6b745ee91f2..c9d06cb5487ff62a276ff1028c22864afa850562 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java @@ -4,8 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020-2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -26,10 +26,8 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.database.exceptions.EntityDoesNotExistException; import org.caosdb.server.database.exceptions.EntityWasNotUniqueException; -import org.caosdb.server.datatype.AbstractCollectionDatatype; import org.caosdb.server.datatype.CollectionValue; import org.caosdb.server.datatype.IndexedSingleValue; -import org.caosdb.server.datatype.ReferenceDatatype; import org.caosdb.server.datatype.ReferenceValue; import org.caosdb.server.entity.Entity; import org.caosdb.server.entity.EntityInterface; @@ -49,7 +47,7 @@ public class CheckRefidValid extends EntityJob { @Override public final void run() { try { - if (isReference(getEntity())) { + if (assureReference(getEntity())) { if (getEntity().hasValue()) { // parse referenced id @@ -58,11 +56,9 @@ public class CheckRefidValid extends EntityJob { return; } - if (getEntity().getDatatype() instanceof ReferenceDatatype) { + if (getEntity().isReference()) { checkRefValue((ReferenceValue) getEntity().getValue()); - } else if (getEntity().getDatatype() instanceof AbstractCollectionDatatype - && ((AbstractCollectionDatatype) getEntity().getDatatype()).getDatatype() - instanceof ReferenceDatatype) { + } else if (getEntity().isReferenceList()) { final CollectionValue vals = (CollectionValue) getEntity().getValue(); for (final IndexedSingleValue v : vals) { if (v != null && v.getWrapped() != null) { @@ -175,14 +171,15 @@ public class CheckRefidValid extends EntityJob { return true; } - private final boolean isReference(final EntityInterface entity) { + /** + * Return true if this is a reference or a list of reference property. + * + * <p>If the data type is not present (yet), append a data type listener which calls this job + * again when the data type is present. + */ + private final boolean assureReference(final EntityInterface entity) { if (entity.hasDatatype()) { - if (entity.getDatatype() instanceof ReferenceDatatype) { - return true; - } else if (entity.getDatatype() instanceof AbstractCollectionDatatype) { - return ((AbstractCollectionDatatype) entity.getDatatype()).getDatatype() - instanceof ReferenceDatatype; - } + return entity.isReference() || entity.isReferenceList(); } else { entity.acceptObserver(this); } diff --git a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java index 086787e8bd4752ccf0a25f8684afa5a5c4a33c82..d49bec09c909c5e884eae80619a5911a50b1f333 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java +++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java @@ -24,16 +24,16 @@ package org.caosdb.server.jobs.core; import java.util.ArrayList; import java.util.List; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.entity.Entity; import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.InsertEntity; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Message.MessageType; import org.caosdb.server.entity.StatementStatus; +import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; -import org.caosdb.server.transaction.Insert; -import org.caosdb.server.transaction.Update; import org.caosdb.server.utils.EntityStatus; /** @@ -63,7 +63,7 @@ public class Inheritance extends EntityJob { @Override protected void run() { - if (getTransaction() instanceof Insert || getTransaction() instanceof Update) { + if (getEntity() instanceof InsertEntity || getEntity() instanceof UpdateEntity) { if (getEntity().hasParents()) { final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>(); parentLoop: @@ -87,7 +87,7 @@ public class Inheritance extends EntityJob { EntityInterface foreign = getEntityByName(parent.getName()); if (foreign == null) { // was not in container -> retrieve from database. - execute(new RetrieveFullEntity(parent)); + execute(new RetrieveFullEntityTransaction(parent)); foreign = parent; } @@ -144,7 +144,7 @@ public class Inheritance extends EntityJob { } } } else { - execute(new RetrieveFullEntity(validProperty)); + execute(new RetrieveFullEntityTransaction(validProperty)); } if (validProperty.getEntityStatus() == EntityStatus.VALID) { collectInheritedProperties(transfer, validProperty, inheritance); diff --git a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java index 73d7ee3f5be0b8847ce322f6774719b7f5a768c2..35ce49297291accd41a7c293fc909cc0a27f9317 100644 --- a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java +++ b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -34,16 +36,19 @@ import org.caosdb.server.FileSystem; import org.caosdb.server.ServerProperties; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.RollBackHandler; -import org.caosdb.server.entity.Entity; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.FileProperties; +import org.caosdb.server.entity.InsertEntity; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.RetrieveEntity; import org.caosdb.server.entity.Role; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.Job; import org.caosdb.server.jobs.JobAnnotation; import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.transaction.Retrieve; +import org.caosdb.server.transaction.WriteTransactionInterface; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.FileUtils; import org.caosdb.server.utils.Undoable; @@ -208,7 +213,7 @@ public class InsertFilesInDir extends FlagJob { } else { i++; final String targetPath = root + sub.getName(); - final Entity newFileEntity = new Entity(sub.getName(), Role.File); + final EntityInterface newFileEntity = createInsertFileEntity(sub.getName()); final long size = sub.length(); final FileProperties fp = new FileProperties(null, targetPath, size); newFileEntity.setFileProperties(fp); @@ -227,7 +232,7 @@ public class InsertFilesInDir extends FlagJob { continue; } - // add create symlink and file record to this + // add create symlink and add file record to this // container if the target // path is allowed if (FileSystem.checkTarget( @@ -253,6 +258,22 @@ public class InsertFilesInDir extends FlagJob { return i; } + /** + * Create a new InsertEntity (if this is an actual run) or a new RetrieveEntity (in dry-run mode) + * with {@link Role.File}. + * + * @param name the file name + * @return new File entity + */ + private EntityInterface createInsertFileEntity(String name) { + if (getTransaction() instanceof WriteTransactionInterface) { + return new InsertEntity(name, Role.File); + } + EntityInterface result = new RetrieveEntity(name); + result.setRole(Role.File); + return result; + } + boolean isExcluded(File f) throws IOException { return this.exclude != null && this.exclude.matcher(f.getCanonicalPath()).find(); } @@ -317,7 +338,7 @@ public class InsertFilesInDir extends FlagJob { return true; } - private void loadJobs(final Entity e) { + private void loadJobs(final EntityInterface e) { final List<Job> loadJobs = loadJobs(e, getTransaction()); getTransaction().getSchedule().addAll(loadJobs); } diff --git a/src/main/java/org/caosdb/server/query/Backreference.java b/src/main/java/org/caosdb/server/query/Backreference.java index b9114ad55147ca92163a55cf374d3b13093ead98..fec64b19b761418a4da148ca4adff9d9145960af 100644 --- a/src/main/java/org/caosdb/server/query/Backreference.java +++ b/src/main/java/org/caosdb/server/query/Backreference.java @@ -130,6 +130,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(2, VARCHAR); } if (this.propertiesTable != null) { // propertiesTable + getQuery().filterEntitiesWithoutRetrievePermission(this.propertiesTable); callApplyBackRef.setString(3, this.propertiesTable); this.statistics.put("propertiesTable", this.propertiesTable); this.statistics.put( @@ -139,6 +140,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(3, VARCHAR); } if (this.entitiesTable != null) { // entitiesTable + getQuery().filterEntitiesWithoutRetrievePermission(this.entitiesTable); callApplyBackRef.setString(4, this.entitiesTable); this.statistics.put("entitiesTable", this.entitiesTable); this.statistics.put( @@ -330,4 +332,12 @@ public class Backreference implements EntityFilterInterface, QueryInterface { public boolean isVersioned() { return this.query.isVersioned(); } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append(toString()); + if (this.hasSubProperty()) sb.append(getSubProperty().getCacheKey()); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Conjunction.java b/src/main/java/org/caosdb/server/query/Conjunction.java index 2e44aea4f64a43ebb2a1c1edb153c6f68c172f8e..03b331242e239a26f108973ca7a37624ab80ea64 100644 --- a/src/main/java/org/caosdb/server/query/Conjunction.java +++ b/src/main/java/org/caosdb/server/query/Conjunction.java @@ -34,7 +34,8 @@ import org.caosdb.server.database.access.Access; import org.caosdb.server.query.Query.QueryException; import org.jdom2.Element; -public class Conjunction extends EntityFilterContainer implements QueryInterface { +public class Conjunction extends EntityFilterContainer + implements QueryInterface, EntityFilterInterface { private String sourceSet = null; private String targetSet = null; @@ -155,4 +156,15 @@ public class Conjunction extends EntityFilterContainer implements QueryInterface public boolean isVersioned() { return this.query.isVersioned(); } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("Conj("); + for (EntityFilterInterface filter : getFilters()) { + sb.append(filter.getCacheKey()); + } + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Disjunction.java b/src/main/java/org/caosdb/server/query/Disjunction.java index 31288f2f169d4ebce1cf3d4da5b2ab369fcd5e1f..edd2e6daf243cd25ea112fdbfb946f2b80ee80b1 100644 --- a/src/main/java/org/caosdb/server/query/Disjunction.java +++ b/src/main/java/org/caosdb/server/query/Disjunction.java @@ -34,7 +34,8 @@ import org.caosdb.server.database.access.Access; import org.caosdb.server.query.Query.QueryException; import org.jdom2.Element; -public class Disjunction extends EntityFilterContainer implements QueryInterface { +public class Disjunction extends EntityFilterContainer + implements QueryInterface, EntityFilterInterface { private String targetSet = null; private int targetSetCount = -1; @@ -143,4 +144,15 @@ public class Disjunction extends EntityFilterContainer implements QueryInterface public boolean isVersioned() { return this.query.isVersioned(); } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("Disj("); + for (EntityFilterInterface filter : getFilters()) { + sb.append(filter.getCacheKey()); + } + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/EntityFilterContainer.java b/src/main/java/org/caosdb/server/query/EntityFilterContainer.java index c2409f90ecc43cfb318a17cc8a9f3ba6df60beb0..1b270e9b120f2c336381db16c88769862b68a725 100644 --- a/src/main/java/org/caosdb/server/query/EntityFilterContainer.java +++ b/src/main/java/org/caosdb/server/query/EntityFilterContainer.java @@ -24,7 +24,7 @@ package org.caosdb.server.query; import java.util.LinkedList; -public abstract class EntityFilterContainer implements EntityFilterInterface { +public abstract class EntityFilterContainer { private final LinkedList<EntityFilterInterface> filters = new LinkedList<EntityFilterInterface>(); public void add(final EntityFilterInterface filter) { diff --git a/src/main/java/org/caosdb/server/query/EntityFilterInterface.java b/src/main/java/org/caosdb/server/query/EntityFilterInterface.java index 3bf113edbfa6aeb3bbe1c4ead7c2bbca0aca0d03..be8484c2f122ae301ea78a014c771e116b0fc012 100644 --- a/src/main/java/org/caosdb/server/query/EntityFilterInterface.java +++ b/src/main/java/org/caosdb/server/query/EntityFilterInterface.java @@ -30,4 +30,12 @@ public interface EntityFilterInterface { void apply(QueryInterface query) throws QueryException; public Element toElement(); + + /** + * Return a string which can serve as a cache key. The string must describe all relevant + * parameters of the filter. + * + * @return a cache key. + */ + public String getCacheKey(); } diff --git a/src/main/java/org/caosdb/server/query/IDFilter.java b/src/main/java/org/caosdb/server/query/IDFilter.java index 12f53b44472d5b897c3d8b2a065388969c1edcbc..ce01f5f242f914c6f6f812f0875332f7ca004c07 100644 --- a/src/main/java/org/caosdb/server/query/IDFilter.java +++ b/src/main/java/org/caosdb/server/query/IDFilter.java @@ -125,4 +125,17 @@ public class IDFilter implements EntityFilterInterface { public String getAggregate() { return this.aggregate; } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("ID("); + if (this.aggregate != null) sb.append(this.aggregate); + sb.append(","); + if (this.operator != null) sb.append(this.operator); + sb.append(","); + if (this.value != null) sb.append(this.value); + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Negation.java b/src/main/java/org/caosdb/server/query/Negation.java index 981b4c8f2ca19a1a6ec47df9e85f108ab7bb01ac..cfdfd05e286030f525b8d4f3c291e2bdfb27b100 100644 --- a/src/main/java/org/caosdb/server/query/Negation.java +++ b/src/main/java/org/caosdb/server/query/Negation.java @@ -55,14 +55,6 @@ public class Negation implements EntityFilterInterface, QueryInterface { } return this.neg; } - - @Override - public void apply(final QueryInterface query) {} - - @Override - public Element toElement() { - return null; - } } private String targetSet = null; @@ -175,4 +167,13 @@ public class Negation implements EntityFilterInterface, QueryInterface { public boolean isVersioned() { return this.query.isVersioned(); } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("NEG("); + if (this.filter != null) sb.append(this.filter.getCacheKey()); + sb.append(")"); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/POV.java b/src/main/java/org/caosdb/server/query/POV.java index fcc719e6ff24299d0b5a62a241442e08fa33962f..a1008bd6a67a8712baf2c9b52ab164f619236263 100644 --- a/src/main/java/org/caosdb/server/query/POV.java +++ b/src/main/java/org/caosdb/server/query/POV.java @@ -512,4 +512,15 @@ public class POV implements EntityFilterInterface { private String measurement(String m) { return String.join("", prefix) + m; } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + if (this.getAggregate() != null) sb.append(this.aggregate); + sb.append(toString()); + if (this.hasSubProperty()) { + sb.append(getSubProperty().getCacheKey()); + } + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java index 695a295da5f64ec788c4dfbb3332734ead44b289..702c6fa22a0528ec0a99b1f463e18151501d0a36 100644 --- a/src/main/java/org/caosdb/server/query/Query.java +++ b/src/main/java/org/caosdb/server/query/Query.java @@ -24,6 +24,7 @@ package org.caosdb.server.query; import static org.caosdb.server.database.DatabaseUtils.bytes2UTF8; +import java.io.Serializable; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; @@ -40,9 +41,11 @@ import java.util.Map; import java.util.Map.Entry; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.commons.jcs.access.behavior.ICacheAccess; import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; +import org.caosdb.server.caching.Cache; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; import org.caosdb.server.database.backend.implementation.MySQL.MySQLHelper; @@ -63,6 +66,7 @@ import org.caosdb.server.query.CQLParser.CqContext; import org.caosdb.server.query.CQLParsingErrorListener.ParsingError; import org.caosdb.server.transaction.TransactionInterface; import org.jdom2.Element; +import org.slf4j.Logger; public class Query implements QueryInterface, ToElementable, TransactionInterface { @@ -184,6 +188,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac ServerProperties.KEY_QUERY_FILTER_ENTITIES_WITHOUT_RETRIEVE_PERMISSIONS) .equalsIgnoreCase("FALSE"); + private Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); List<IdVersionPair> resultSet = null; private final String query; private Pattern entity = null; @@ -200,6 +205,16 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac private final ArrayList<ToElementable> messages = new ArrayList<>(); private Access access; private boolean versioned = false; + private static ICacheAccess<String, Serializable> cache = + Cache.getCache("HIGH_LEVEL_QUERY_CACHE"); + /** Cached=true means that the results of this query have actually been pulled from the cache. */ + private boolean cached = false; + /** + * Cachable=false means that the results of this query cannot be used for the cache, because the + * query evaluation contains complex permission checking which could only be cached on a per-user + * basis (maybe in the future). + */ + private boolean cachable = true; public Type getType() { return this.type; @@ -500,31 +515,95 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } + /** + * Whether the transaction allows this query instance to use the query cache. This is controlled + * by the "cache" flag. + * + * @see {@link NoCache} + * @return true if caching is encouraged. + */ + private boolean useCache() { + return getAccess().useCache(); + } + + /** + * Execute the query. + * + * <p>First try the cache and only then use the back-end. + * + * @param access + * @return + * @throws ParsingException + */ public Query execute(final Access access) throws ParsingException { - setAccess(access); parse(); + setAccess(access); + if (useCache()) { + this.resultSet = getCached(getCacheKey()); + } - try { - - this.resultSet = getResultSet(executeStrategy(this.versioned), this.versioned); + if (this.resultSet == null) { + executeNoCache(access); + if (this.cachable) { + setCache(getCacheKey(), this.resultSet); + } + this.logger.debug("Uncached query {}", this.query); + } else { + this.logger.debug("Using cached result for {}", this.query); + this.cached = true; + } - filterEntitiesWithoutRetrievePermission(this.resultSet); + this.resultSet = filterEntitiesWithoutRetrievePermission(this.resultSet); - // Fill resulting entities into container - if (this.container != null && this.type == Type.FIND) { - for (final IdVersionPair p : this.resultSet) { + // Fill resulting entities into container + if (this.container != null && this.type == Type.FIND) { + for (final IdVersionPair p : this.resultSet) { - final Entity e = new RetrieveEntity(p.id, p.version); + final Entity e = new RetrieveEntity(p.id, p.version); - // if query has select-clause: - if (this.selections != null && !this.selections.isEmpty()) { - e.addSelections(this.selections); - } - this.container.add(e); + // if query has select-clause: + if (this.selections != null && !this.selections.isEmpty()) { + e.addSelections(this.selections); } + this.container.add(e); } - return this; + } + return this; + } + /** Remove all cached queries from the cache. */ + public static void clearCache() { + cache.clear(); + } + + /** + * Cache a query result. + * + * @param key + * @param resultSet + */ + private void setCache(String key, List<IdVersionPair> resultSet) { + if (resultSet instanceof Serializable) { + cache.put(key, (Serializable) resultSet); + } else { + cache.put(key, new ArrayList<>(resultSet)); + } + } + + /** + * Retrieve a result set of entity ids (and the version) from the cache. + * + * @param key + * @return + */ + @SuppressWarnings("unchecked") + private List<IdVersionPair> getCached(String key) { + return (List<IdVersionPair>) cache.get(key); + } + + protected void executeNoCache(Access access) { + try { + this.resultSet = getResultSet(executeStrategy(this.versioned), this.versioned); } finally { cleanUp(); } @@ -564,31 +643,31 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * Filter out all entities which may not be retrieved by this user due to a missing RETRIEVE * permission. This one is also designed for filtering of intermediate results. * - * @param query * @param resultSet * @throws SQLException * @throws TransactionException */ - public void filterEntitiesWithoutRetrievePermission( - final QueryInterface query, final String resultSet) + public void filterEntitiesWithoutRetrievePermission(final String resultSet) throws SQLException, TransactionException { if (!filterEntitiesWithoutRetrievePermisions) { return; } - try (final Statement stmt = query.getConnection().createStatement()) { + cachable = false; + try (final Statement stmt = this.getConnection().createStatement()) { final ResultSet rs = stmt.executeQuery("SELECT id from `" + resultSet + "`"); final List<Integer> toBeDeleted = new LinkedList<Integer>(); while (rs.next()) { final long t1 = System.currentTimeMillis(); final Integer id = rs.getInt("id"); - if (!execute(new RetrieveSparseEntity(id, null), query.getAccess()) - .getEntity() - .getEntityACL() - .isPermitted(query.getUser(), EntityPermission.RETRIEVE_ENTITY)) { + if (id > 99 + && !execute(new RetrieveSparseEntity(id, null), this.getAccess()) + .getEntity() + .getEntityACL() + .isPermitted(this.getUser(), EntityPermission.RETRIEVE_ENTITY)) { toBeDeleted.add(id); } final long t2 = System.currentTimeMillis(); - query.addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); + this.addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); } rs.close(); for (final Integer id : toBeDeleted) { @@ -604,25 +683,30 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * * @param entities * @throws TransactionException + * @return the filtered list. */ - private void filterEntitiesWithoutRetrievePermission(final List<IdVersionPair> entities) - throws TransactionException { + private List<IdVersionPair> filterEntitiesWithoutRetrievePermission( + final List<IdVersionPair> entities) throws TransactionException { if (!filterEntitiesWithoutRetrievePermisions) { - return; + return entities; } + + List<IdVersionPair> result = new ArrayList<>(); final Iterator<IdVersionPair> iterator = entities.iterator(); while (iterator.hasNext()) { final long t1 = System.currentTimeMillis(); final IdVersionPair next = iterator.next(); - if (!execute(new RetrieveSparseEntity(next.id, next.version), getAccess()) - .getEntity() - .getEntityACL() - .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) { - iterator.remove(); + if (next.id > 99 + && execute(new RetrieveSparseEntity(next.id, next.version), getAccess()) + .getEntity() + .getEntityACL() + .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) { + result.add(next); } final long t2 = System.currentTimeMillis(); addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); } + return result; } @Override @@ -679,6 +763,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } else { ret.setAttribute("results", "0"); } + ret.setAttribute("cached", Boolean.toString(this.cached)); final Element parseTreeElem = new Element("ParseTree"); if (this.el.hasErrors()) { @@ -769,4 +854,19 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac public boolean isVersioned() { return this.versioned; } + + /** + * Return a key for the query cache. The key should describe the query with all the filters but + * without the FIND, COUNT and SELECT ... FROM parts. + * + * @return A Cache key. + */ + String getCacheKey() { + StringBuilder sb = new StringBuilder(); + if (this.versioned) sb.append("versioned"); + if (this.role != null) sb.append(this.role.toString()); + if (this.entity != null) sb.append(this.entity.toString()); + if (this.filter != null) sb.append(this.filter.getCacheKey()); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/RoleFilter.java b/src/main/java/org/caosdb/server/query/RoleFilter.java index 48c8372014c6e702be8b88113102da030dbc6046..97d1b1eab3bcc9e21adab4fa5e2a5204411e58fb 100644 --- a/src/main/java/org/caosdb/server/query/RoleFilter.java +++ b/src/main/java/org/caosdb/server/query/RoleFilter.java @@ -148,4 +148,10 @@ public class RoleFilter implements EntityFilterInterface { private String getOperator() { return this.operator; } + + @Override + public String getCacheKey() { + // unused + return null; + } } diff --git a/src/main/java/org/caosdb/server/query/StoredAt.java b/src/main/java/org/caosdb/server/query/StoredAt.java index 84a12d2c6b5b7ca3c3b0644eef4ea72a200cc328..90adc188b61e375a30721b605cb91b93fe76a88b 100644 --- a/src/main/java/org/caosdb/server/query/StoredAt.java +++ b/src/main/java/org/caosdb/server/query/StoredAt.java @@ -201,4 +201,9 @@ public class StoredAt implements EntityFilterInterface { public String toString() { return "SAT(" + (this.pattern_matching ? this.likeLocation : this.location) + ")"; } + + @Override + public String getCacheKey() { + return toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/SubProperty.java b/src/main/java/org/caosdb/server/query/SubProperty.java index 4668935e56f0057d3d0fe927aa0a729669e7e0f6..f3be825ccf28b7df2ba646306a61f006664ae5a6 100644 --- a/src/main/java/org/caosdb/server/query/SubProperty.java +++ b/src/main/java/org/caosdb/server/query/SubProperty.java @@ -87,7 +87,7 @@ public class SubProperty implements QueryInterface, EntityFilterInterface { this.filter.apply(this); - getQuery().filterEntitiesWithoutRetrievePermission(this, this.sourceSet); + getQuery().filterEntitiesWithoutRetrievePermission(this.sourceSet); final CallableStatement callFinishSubProperty = getConnection().prepareCall("call finishSubProperty(?,?,?,?)"); @@ -167,4 +167,12 @@ public class SubProperty implements QueryInterface, EntityFilterInterface { public boolean isVersioned() { return this.query.isVersioned(); } + + @Override + public String getCacheKey() { + StringBuilder sb = new StringBuilder(); + sb.append("SUB("); + if (this.filter != null) sb.append(this.filter.getCacheKey()); + return sb.toString(); + } } diff --git a/src/main/java/org/caosdb/server/query/TransactionFilter.java b/src/main/java/org/caosdb/server/query/TransactionFilter.java index 55176594f5d17fdb726b82f37e5d3eb83944e611..fed4a7156048f1bd43f7e4000df80f29a38187a0 100644 --- a/src/main/java/org/caosdb/server/query/TransactionFilter.java +++ b/src/main/java/org/caosdb/server/query/TransactionFilter.java @@ -256,4 +256,9 @@ public class TransactionFilter implements EntityFilterInterface { + this.transactor + ")"; } + + @Override + public String getCacheKey() { + return toString(); + } } diff --git a/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java index c7ecd4deb76117474adc188c3f8a7d0f6064a3b8..876b43a160f673b96a7220a746c9a7dcd4951388 100644 --- a/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java +++ b/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java @@ -117,7 +117,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { getRequest().setEntity(r); } - this.utils = WebinterfaceUtils.getInstance(getHostRef()); + this.utils = WebinterfaceUtils.getInstance(getRequest()); this.timestamp = getRequest().getDate().getTime(); @@ -177,7 +177,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { retRoot.setAttribute("crid", this.getCRID()); } retRoot.setAttribute("timestamp", getTimestamp().toString()); - retRoot.setAttribute("baseuri", getRootRef().toString()); + retRoot.setAttribute("baseuri", getUtils().getServerRootURI()); return retRoot; } diff --git a/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java b/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java index f20a4ab042bd996ef7e7d004e9246935cf034b64..41f92a5631d9cabb04ee5498373848b516885424 100644 --- a/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java +++ b/src/main/java/org/caosdb/server/resource/EntityOwnerResource.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,20 +24,26 @@ */ package org.caosdb.server.resource; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -import org.caosdb.server.resource.transaction.handlers.RetriveOwnerRequestHandler; +import org.caosdb.server.permissions.EntityPermission; +import org.caosdb.server.resource.transaction.RetrieveEntityResource; -public class EntityOwnerResource extends EntityResource { - - public EntityOwnerResource() { - // only retrieval is allowed - super(true, false, false, false); - } +/** + * Resource which returns the entity with the owner information attached to it. + * + * <p>Note: 'Owners' are all roles with the {@link EntityPermission#EDIT_ACL} + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityOwnerResource extends RetrieveEntityResource { @Override - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new RetriveOwnerRequestHandler(); + protected void handleRetrieveContainer(RetrieveContainer container) { + super.handleRetrieveContainer(container); + for (final EntityInterface e : container) { + // with this flag, the owner info will be loaded as well. + e.setFlag("owner", null); + } } } diff --git a/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java b/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java index ffce6fe18378ec2586276cd69124cfaeaf8f29fc..1f5ee5f7d8a28a235de02bbc33f826da32af0615 100644 --- a/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java +++ b/src/main/java/org/caosdb/server/resource/EntityPermissionsResource.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,19 +25,20 @@ package org.caosdb.server.resource; import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -import org.caosdb.server.resource.transaction.handlers.RetrieveGlobalEntityPermissionsHandler; +import org.caosdb.server.permissions.EntityPermission; +import org.caosdb.server.resource.transaction.RetrieveEntityResource; -public class EntityPermissionsResource extends EntityResource { - - public EntityPermissionsResource() { - // only retrieval is allowed - super(true, false, false, false); - } +/** + * Resource which appends the entity permissions (esp. the global ones) to the normal entity + * response. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityPermissionsResource extends RetrieveEntityResource { @Override - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new RetrieveGlobalEntityPermissionsHandler(); + protected void handleRetrieveContainer(RetrieveContainer container) { + super.handleRetrieveContainer(container); + container.addMessage(EntityPermission.getAllEntityPermissions()); } } diff --git a/src/main/java/org/caosdb/server/resource/Webinterface.java b/src/main/java/org/caosdb/server/resource/Webinterface.java index bba1fb09c7f2c0a14b95b7314832b6ff174cef6b..adba714a96e5bbb4bc3ea13b8d72ec69a88cb33b 100644 --- a/src/main/java/org/caosdb/server/resource/Webinterface.java +++ b/src/main/java/org/caosdb/server/resource/Webinterface.java @@ -47,7 +47,7 @@ public class Webinterface extends ServerResource { @Override protected void doInit() throws ResourceException { - this.utils = WebinterfaceUtils.getInstance(getHostRef()); + this.utils = WebinterfaceUtils.getInstance(getRequest()); super.doInit(); } diff --git a/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java b/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java index 0fe186a0426002071e0b6c6a5ebe0b3f234e9b2a..22f1a46e4fd314b0dcaebde7abdcc1d41a9f50f0 100644 --- a/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java +++ b/src/main/java/org/caosdb/server/resource/WebinterfaceBuildNumber.java @@ -39,7 +39,7 @@ public class WebinterfaceBuildNumber extends ServerResource { @Override protected void doInit() throws ResourceException { super.doInit(); - this.utils = WebinterfaceUtils.getInstance(getHostRef()); + this.utils = WebinterfaceUtils.getInstance(getRequest()); } /** diff --git a/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java b/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java index 0b3f8b7aa4f4a7724b9b6646e7ecb72330293980..0c66485ea3d62286d633d9580c1362ba33435095 100644 --- a/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java +++ b/src/main/java/org/caosdb/server/resource/transaction/EntityNamesResource.java @@ -1,18 +1,41 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ package org.caosdb.server.resource.transaction; import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.handlers.GetNamesRequestHandler; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -public class EntityNamesResource extends EntityResource { - - public EntityNamesResource() { - // only get is allowed - super(true, false, false, false); - } +/** + * Resource which returns only the names of entities. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityNamesResource extends RetrieveEntityResource { @Override - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new GetNamesRequestHandler(); + protected void handleRetrieveContainer(RetrieveContainer container) { + super.handleRetrieveContainer(container); + container.getFlags().put("names", null); } } diff --git a/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java b/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java index 12659362e0a9a82e8cbb95e73e4d6ac6c2d27c6c..704fcfb9aa92e56ad963365d763f70fa7feb470e 100644 --- a/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java +++ b/src/main/java/org/caosdb/server/resource/transaction/EntityResource.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -24,182 +26,167 @@ package org.caosdb.server.resource.transaction; import java.io.IOException; import java.security.NoSuchAlgorithmException; -import java.sql.SQLException; +import java.util.List; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileItemStream; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.caosdb.server.CaosDBException; +import org.caosdb.server.FileSystem; import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; -import org.caosdb.server.entity.container.DeleteContainer; -import org.caosdb.server.entity.container.InsertContainer; -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.entity.container.UpdateContainer; -import org.caosdb.server.resource.AbstractCaosDBServerResource; -import org.caosdb.server.resource.transaction.handlers.FileUploadHandler; -import org.caosdb.server.resource.transaction.handlers.RequestHandler; -import org.caosdb.server.resource.transaction.handlers.SimpleDeleteRequestHandler; -import org.caosdb.server.resource.transaction.handlers.SimpleGetRequestHandler; -import org.caosdb.server.resource.transaction.handlers.SimpleWriteHandler; -import org.caosdb.server.transaction.Delete; -import org.caosdb.server.transaction.Insert; -import org.caosdb.server.transaction.Retrieve; -import org.caosdb.server.transaction.Update; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.FileProperties; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.UpdateEntity; +import org.caosdb.server.entity.container.WritableContainer; +import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.transaction.WriteTransactionInterface; +import org.caosdb.server.utils.ServerMessages; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.restlet.data.MediaType; -import org.restlet.data.Status; +import org.restlet.ext.fileupload.RestletFileUpload; import org.restlet.representation.Representation; -public class EntityResource extends AbstractCaosDBServerResource { - - private final boolean get; - private final boolean post; - private final boolean put; - private final boolean delete; - - public EntityResource( - final boolean get, final boolean post, final boolean put, final boolean delete) { - this.get = get; - this.post = post; - this.put = put; - this.delete = delete; - } - - public EntityResource() { - this(true, true, true, true); - } - - protected RequestHandler<RetrieveContainer> getGetRequestHandler() { - return new SimpleGetRequestHandler(); - } - - protected RequestHandler<DeleteContainer> getDeleteRequestHandler() { - return new SimpleDeleteRequestHandler(); - } - - protected RequestHandler<InsertContainer> getPostRequestHandler() { - if (getRequest().getEntity().getMediaType() != null - && getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { - return new FileUploadHandler<InsertContainer>(); - } else { - return new SimpleWriteHandler<InsertContainer>(); - } - } - - protected RequestHandler<UpdateContainer> getPutRequestHandler() { - if (getRequest().getEntity().getMediaType() != null - && getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { - return new FileUploadHandler<UpdateContainer>(); - } else { - return new SimpleWriteHandler<UpdateContainer>(); - } - } - - @Override - protected final Representation httpGetInChildClass() - throws ConnectionException, IOException, SQLException, CaosDBException, - NoSuchAlgorithmException, Exception { - - final long t1 = System.currentTimeMillis(); - if (!this.get) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - final RetrieveContainer entityContainer = - new RetrieveContainer(getUser(), getTimestamp(), getSRID(), getFlags()); - final Document doc = new Document(); - - getGetRequestHandler().handle(this, entityContainer); - - final long t2 = System.currentTimeMillis(); - entityContainer - .getTransactionBenchmark() - .addMeasurement(getClass().getSimpleName() + ".httpGetInChildClass#handle", t2 - t1); - - final Retrieve retrieve = new Retrieve(entityContainer); - retrieve.execute(); - - final long t3 = System.currentTimeMillis(); - entityContainer - .getTransactionBenchmark() - .addMeasurement( - getClass().getSimpleName() + ".httpGetInChildClass#retrieve.execute", t3 - t2); - - final Element rootElem = generateRootElement(); - entityContainer.addToElement(rootElem); - doc.setRootElement(rootElem); - - final long t4 = System.currentTimeMillis(); - entityContainer - .getTransactionBenchmark() - .addMeasurement( - getClass().getSimpleName() + ".httpGetInChildClass#element_handling", t4 - t3); - - return ok(doc); - } +/** + * Handles POST, PUT, and DELETE requests for Entities. + * + * <p>The GET requests (Retrieval) is handled in the superclass {@link RetrieveEntityResource}. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class EntityResource extends RetrieveEntityResource { + /** Handle entity deletions (DELETE requests). */ @Override protected final Representation httpDeleteInChildClass() throws Exception { - if (!this.delete) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - final DeleteContainer entityContainer = - new DeleteContainer(getUser(), getTimestamp(), getSRID(), getFlags()); + final WritableContainer container = + new WritableContainer(getUser(), getTimestamp(), getSRID(), getFlags()); final Document doc = new Document(); - getDeleteRequestHandler().handle(this, entityContainer); + for (final String item : getRequestedItems()) { + String[] elem = item.split("@", 1); + Integer id = null; + String version = null; + try { + id = Integer.parseInt(elem[0]); + } catch (NumberFormatException e) { + // pass + } + if (elem.length > 1) { + version = elem[1]; + } + + if (id != null) { + container.add(new DeleteEntity(id, version)); + } + } - final Delete delete = new Delete(entityContainer); + final WriteTransactionInterface delete = new WriteTransaction(container); delete.execute(); final Element rootElem = generateRootElement(); - entityContainer.addToElement(rootElem); + container.addToElement(rootElem); doc.setRootElement(rootElem); return ok(doc); } + /** Handle entity insertions (POST requests). */ @Override protected final Representation httpPostInChildClass(final Representation entity) throws xmlNotWellFormedException, Exception { - if (!this.post) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } + final WritableContainer entityContainer = + new WritableContainer(getUser(), getTimestamp(), getSRID(), getFlags()); - final InsertContainer entityContainer = - new InsertContainer(getUser(), getTimestamp(), getSRID(), getFlags()); - final Document doc = new Document(); + List<Element> insertEntities; + if (entity.getMediaType() != null + && entity.getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { + insertEntities = parseMultipartEntity(entityContainer).getChildren(); + } else { + insertEntities = parseEntity(entity).getRootElement().getChildren(); + } - getPostRequestHandler().handle(this, entityContainer); + for (final Element e : insertEntities) { + entityContainer.add(new InsertEntity(e)); + } - final Insert insert = new Insert(entityContainer); + final WriteTransactionInterface insert = new WriteTransaction(entityContainer); insert.execute(); final Element rootElem = generateRootElement(); entityContainer.addToElement(rootElem); + final Document doc = new Document(); doc.setRootElement(rootElem); return ok(doc); } + /** + * Parse the body of requests with content type "multipart/form-data". This is specific for + * requests which also contain file blobs. + */ + private Element parseMultipartEntity(WritableContainer container) + throws FileUploadException, IOException, Message, xmlNotWellFormedException, + NoSuchAlgorithmException, CaosDBException { + final DiskFileItemFactory factory = new DiskFileItemFactory(); + factory.setSizeThreshold(1000240); + final RestletFileUpload upload = new RestletFileUpload(factory); + final FileItemIterator iter = upload.getItemIterator(getRequest().getEntity()); + + FileItemStream item = iter.next(); + + final Element element; + // First part of the multi-part form data entity must be the xml + // representation of the files. + // Name of the form field: FileRepresentation + if (item.isFormField()) { + if (item.getFieldName().equals("FileRepresentation")) { + element = parseEntity(item.openStream()).getRootElement(); + } else { + throw ServerMessages.NO_FILE_REPRESENTATION_SUBMITTED; + } + } else { + throw ServerMessages.FORM_CONTAINS_UNSUPPORTED_CONTENT; + } + + // Get files, store to tmp dir. + while (iter.hasNext()) { + item = iter.next(); + final FileProperties file = FileSystem.upload(item, getSRID()); + container.addFile(item.getName(), file); + } + + // transform xml elements to entities and add them to the container. + return element; + } + + /** Handle entity updates (PUT requests). */ @Override protected final Representation httpPutInChildClass(final Representation entity) throws ConnectionException, JDOMException, Exception, xmlNotWellFormedException { - if (!this.put) { - getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); - return null; - } - - final UpdateContainer entityContainer = - new UpdateContainer(getUser(), getTimestamp(), getSRID(), getFlags()); + final WritableContainer entityContainer = + new WritableContainer(getUser(), getTimestamp(), getSRID(), getFlags()); final Document doc = new Document(); - getPutRequestHandler().handle(this, entityContainer); + List<Element> updateEntities; + if (getRequest().getEntity().getMediaType() != null + && getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { + updateEntities = parseMultipartEntity(entityContainer).getChildren(); + } else { + updateEntities = parseEntity(getRequest().getEntity()).getRootElement().getChildren(); + } + + for (final Element e : updateEntities) { + entityContainer.add(new UpdateEntity(e)); + } - final Update update = new Update(entityContainer); + final WriteTransactionInterface update = new WriteTransaction(entityContainer); update.execute(); final Element rootElem = generateRootElement(); diff --git a/src/main/java/org/caosdb/server/resource/transaction/RetrieveEntityResource.java b/src/main/java/org/caosdb/server/resource/transaction/RetrieveEntityResource.java new file mode 100644 index 0000000000000000000000000000000000000000..5eb9db72f69f8f054dbb6f02b970d09e09ff366c --- /dev/null +++ b/src/main/java/org/caosdb/server/resource/transaction/RetrieveEntityResource.java @@ -0,0 +1,110 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.resource.transaction; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.sql.SQLException; +import org.caosdb.server.CaosDBException; +import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; +import org.caosdb.server.entity.container.RetrieveContainer; +import org.caosdb.server.resource.AbstractCaosDBServerResource; +import org.caosdb.server.transaction.Retrieve; +import org.jdom2.Document; +import org.jdom2.Element; +import org.restlet.representation.Representation; + +/** + * Handles GET requests for different subclasses which all have in common that they retrieve + * Entities (plus other information in some cases). + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public abstract class RetrieveEntityResource extends AbstractCaosDBServerResource { + + /** + * Parse the segment which specifies the entities which are to be retrieved + * + * @param container + */ + protected void handleRetrieveContainer(RetrieveContainer container) { + + for (final String item : getRequestedItems()) { + String[] elem = item.split("@", 2); + Integer id = null; + String name = null; + String version = null; + try { + id = Integer.parseInt(elem[0]); + } catch (NumberFormatException e) { + name = elem[0]; + } + if (elem.length > 1) { + version = elem[1]; + } + + if (id != null) { + container.add(id, version); + } else { + container.add(name); + } + } + } + + /** Handle the GET request. */ + @Override + protected final Representation httpGetInChildClass() + throws ConnectionException, IOException, SQLException, CaosDBException, + NoSuchAlgorithmException, Exception { + + final RetrieveContainer entityContainer = + new RetrieveContainer(getUser(), getTimestamp(), getSRID(), getFlags()); + + handleRetrieveContainer(entityContainer); + + final long t2 = System.currentTimeMillis(); + final Retrieve retrieve = new Retrieve(entityContainer); + retrieve.execute(); + + final long t3 = System.currentTimeMillis(); + entityContainer + .getTransactionBenchmark() + .addMeasurement( + getClass().getSimpleName() + ".httpGetInChildClass#retrieve.execute", t3 - t2); + + final Element rootElem = generateRootElement(); + entityContainer.addToElement(rootElem); + final Document doc = new Document(); + doc.setRootElement(rootElem); + + final long t4 = System.currentTimeMillis(); + entityContainer + .getTransactionBenchmark() + .addMeasurement( + getClass().getSimpleName() + ".httpGetInChildClass#element_handling", t4 - t3); + + return ok(doc); + } +} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/FileUploadHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/FileUploadHandler.java deleted file mode 100644 index ec5c5bc4d144af9ce99f7b27915a52420c331bb5..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/FileUploadHandler.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import java.io.IOException; -import org.apache.commons.fileupload.FileItemIterator; -import org.apache.commons.fileupload.FileItemStream; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.caosdb.server.FileSystem; -import org.caosdb.server.entity.FileProperties; -import org.caosdb.server.entity.container.WritableContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.caosdb.server.utils.ServerMessages; -import org.jdom2.Element; -import org.restlet.data.MediaType; -import org.restlet.ext.fileupload.RestletFileUpload; - -public class FileUploadHandler<T extends WritableContainer> extends SimpleWriteHandler<T> { - - @Override - public void handle(final EntityResource t, final T container) throws Exception { - if (!t.getRequest().getEntity().getMediaType().equals(MediaType.MULTIPART_FORM_DATA, true)) { - throw new Exception("Wrong MEDIATYPE"); - } - final DiskFileItemFactory factory = new DiskFileItemFactory(); - factory.setSizeThreshold(1000240); - final RestletFileUpload upload = new RestletFileUpload(factory); - final FileItemIterator iter = upload.getItemIterator(t.getRequest().getEntity()); - - FileItemStream item = iter.next(); - - final Element element; - // First part of the multi-part form data entity must be the xml - // representation of the files. - // Name of the form field: FileRepresentation - if (item.isFormField()) { - if (item.getFieldName().equals("FileRepresentation")) { - element = t.parseEntity(item.openStream()).getRootElement(); - } else { - throw ServerMessages.NO_FILE_REPRESENTATION_SUBMITTED; - } - } else { - throw ServerMessages.FORM_CONTAINS_UNSUPPORTED_CONTENT; - } - - // Get files, store to tmp dir. - while (iter.hasNext()) { - item = iter.next(); - try { - final FileProperties file = FileSystem.upload(item, t.getSRID()); - container.addFile(item.getName(), file); - } catch (final IOException e) { - throw new FileUploadException(); - } - } - - // transform xml elements to entities and add them to the container. - processEntities(container, element); - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/GetNamesRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/GetNamesRequestHandler.java deleted file mode 100644 index c327c6833d281c07148c59be689ddfac25b33f4d..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/GetNamesRequestHandler.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class GetNamesRequestHandler extends RequestHandler<RetrieveContainer> { - - /** Adds the `names` flag to the RetrieveContainer which triggers the RetrieveAllNames job. */ - @Override - public void handle(EntityResource t, RetrieveContainer container) throws Exception { - container.getFlags().put("names", null); - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/RequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/RequestHandler.java deleted file mode 100644 index fc49cc518db12e1f750716372f94b808dcb3874b..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/RequestHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public abstract class RequestHandler<T extends TransactionContainer> { - - public abstract void handle(final EntityResource t, T container) throws Exception; -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetrieveGlobalEntityPermissionsHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/RetrieveGlobalEntityPermissionsHandler.java deleted file mode 100644 index 6801e804a8bcb721e2c7170ad857126579cbfb46..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetrieveGlobalEntityPermissionsHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.permissions.EntityPermission; -import org.caosdb.server.resource.transaction.EntityResource; - -public class RetrieveGlobalEntityPermissionsHandler extends SimpleGetRequestHandler { - - @Override - public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - super.handle(t, container); - container.addMessage(EntityPermission.getAllEntityPermissions()); - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetriveOwnerRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/RetriveOwnerRequestHandler.java deleted file mode 100644 index 7b2d698cb4673ba7e4ed30fc81fc249e89632734..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/RetriveOwnerRequestHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class RetriveOwnerRequestHandler extends SimpleGetRequestHandler { - - @Override - public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - super.handle(t, container); - for (final EntityInterface e : container) { - e.setFlag("owner", null); - } - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java deleted file mode 100644 index 34c1a3836290f4e90fde5a1ec10a2818b7b63fae..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> - * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.DeleteContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class SimpleDeleteRequestHandler extends RequestHandler<DeleteContainer> { - - @Override - public void handle(final EntityResource t, final DeleteContainer container) throws Exception { - // TODO a lot of code duplication, see SimpleGetRequestHandle#handle. - // However, this is about to be changed again when string ids are - // introduced, anyways. So we just leave it. - for (final String item : t.getRequestedItems()) { - String[] elem = item.split("@", 1); - Integer id = null; - String version = null; - try { - id = Integer.parseInt(elem[0]); - } catch (NumberFormatException e) { - // pass - } - if (elem.length > 1) { - version = elem[1]; - } - - if (id != null) { - container.add(id, version); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java deleted file mode 100644 index b80d9c219e2cbda1e012c9c73993b5ea427c6ccf..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import org.caosdb.server.entity.container.RetrieveContainer; -import org.caosdb.server.resource.transaction.EntityResource; - -public class SimpleGetRequestHandler extends RequestHandler<RetrieveContainer> { - - @Override - public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - for (final String item : t.getRequestedItems()) { - String[] elem = item.split("@", 2); - Integer id = null; - String name = null; - String version = null; - try { - id = Integer.parseInt(elem[0]); - } catch (NumberFormatException e) { - name = elem[0]; - } - if (elem.length > 1) { - version = elem[1]; - } - - if (id != null) { - container.add(id, version); - } else { - container.add(name); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleWriteHandler.java b/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleWriteHandler.java deleted file mode 100644 index feb784da2f1ab9dcc09942a108bcce6bd073e128..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/resource/transaction/handlers/SimpleWriteHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.resource.transaction.handlers; - -import java.util.List; -import org.caosdb.server.entity.container.WritableContainer; -import org.caosdb.server.resource.transaction.EntityResource; -import org.jdom2.Element; - -public class SimpleWriteHandler<T extends WritableContainer> extends RequestHandler<T> { - - /** - * Transform child elements of parameter 'element' to entities and add them to the container. Iff - * parameter 'tag' is not null, only elements with that tag name will be transformed into - * entities. The others will be ignored. - * - * @param container - * @param element - * @param tag - * @throws Exception - */ - protected void processEntities(final WritableContainer container, final Element element) - throws Exception { - - final List<Element> insertEntities; - insertEntities = element.getChildren(); - - for (final Element e : insertEntities) { - container.add(e); - } - } - - @Override - public void handle(final EntityResource t, final T container) throws Exception { - final Element element = t.parseEntity(t.getRequest().getEntity()).getRootElement(); - processEntities(container, element); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/CaosDBTerminal.java b/src/main/java/org/caosdb/server/terminal/CaosDBTerminal.java deleted file mode 100644 index 96bd6315b7e32c03273a8b65bd489bfc061e0ef1..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/CaosDBTerminal.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.TerminalFacade; -import com.googlecode.lanterna.gui.GUIScreen; -import com.googlecode.lanterna.gui.GUIScreen.Position; -import com.googlecode.lanterna.screen.Screen; -import com.googlecode.lanterna.terminal.Terminal; -import com.googlecode.lanterna.terminal.text.UnixTerminal; -import java.nio.charset.Charset; - -/** - * @deprecated Soon to be removed - * @author Timm Fitschen (t.fitschen@indiscale.com) - */ -@Deprecated -public class CaosDBTerminal extends Thread { - - public CaosDBTerminal() { - if ((this.terminal = - TerminalFacade.createTerminal(System.in, System.out, Charset.forName("UTF8"))) - instanceof UnixTerminal) { - this.terminal = - new UnixTerminal( - System.in, - System.out, - Charset.forName("UTF8"), - null, - UnixTerminal.Behaviour.CTRL_C_KILLS_APPLICATION); - } - this.screen = new Screen(this.terminal); - this.guiScreen = new GUIScreen(this.screen); - } - - private Terminal terminal = null; - private Screen screen = null; - private GUIScreen guiScreen = null; - - @Override - public void run() { - this.guiScreen.getScreen().startScreen(); - final MainWindow m = new MainWindow(); - - this.guiScreen.showWindow(m, Position.FULL_SCREEN); - this.guiScreen.getScreen().stopScreen(); - } - - public void shutDown() { - this.guiScreen.getActiveWindow().close(); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/DatabaseAccessPanel.java b/src/main/java/org/caosdb/server/terminal/DatabaseAccessPanel.java deleted file mode 100644 index 6877049605888682bd003f8382a931661e79602c..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/DatabaseAccessPanel.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.component.Table; -import org.caosdb.server.database.DatabaseMonitor; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; - -public final class DatabaseAccessPanel extends Panel implements Observer { - - private static final DatabaseAccessPanel instance = new DatabaseAccessPanel(); - private final Table table = new Table(2); - private final Label label1 = new Label(); - private final Label label2 = new Label(); - private final Label label3 = new Label(); - private final Label label4 = new Label(); - private final Label label5 = new Label(); - - public static final DatabaseAccessPanel getInstance() { - return instance; - } - - private DatabaseAccessPanel() { - super(); - DatabaseMonitor.acceptWeakAccessObserver(this); - DatabaseMonitor.acceptStrongAccessObserver(this); - - this.table.addRow(new Label("Weak access, acquired:"), this.label1); - this.table.addRow(new Label("Weak access, waiting:"), this.label2); - this.table.addRow(new Label("Strong access, allocated:"), this.label3); - this.table.addRow(new Label("Strong access, acquired:"), this.label4); - this.table.addRow(new Label("Strong access, waiting:"), this.label5); - addComponent(this.table); - - notifyObserver(null, null); - } - - @Override - public boolean notifyObserver(final String e, final Observable o) { - try { - synchronized (this) { - if (getParent() != null) { - this.label1.setText(Integer.toString(DatabaseMonitor.acquiredWeakAccess())); - this.label2.setText(Integer.toString(DatabaseMonitor.waitingAcquireWeakAccess())); - this.label3.setText( - DatabaseMonitor.whoHasAllocatedStrongAccess() != null - ? DatabaseMonitor.whoHasAllocatedStrongAccess().getName() - : "None"); - this.label4.setText( - DatabaseMonitor.whoHasAcquiredStrongAccess() != null - ? DatabaseMonitor.whoHasAcquiredStrongAccess().getName() - : "None"); - this.label5.setText(Integer.toString(DatabaseMonitor.waitingAllocateStrongAccess())); - } - } - } catch (final Exception exc) { - exc.printStackTrace(); - } - return true; - } -} diff --git a/src/main/java/org/caosdb/server/terminal/EntitiesPanel.java b/src/main/java/org/caosdb/server/terminal/EntitiesPanel.java deleted file mode 100644 index b2f7489de66eeae9771e911b45e7d4546cb4f5b9..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/EntitiesPanel.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.component.Table; -import org.caosdb.server.utils.Info; - -public final class EntitiesPanel extends Panel implements StatComponent { - - private static final EntitiesPanel instance = new EntitiesPanel(); - private boolean active = false; - private final Table table = new Table(2); - private final Label label1 = new Label(); - private final Label label2 = new Label(); - private final Label label3 = new Label(); - private final Label label4 = new Label(); - - public static final EntitiesPanel getInstance() { - return instance; - } - - private EntitiesPanel() { - super(); - - this.table.addRow(new Label("RecordTypes:"), this.label2); - this.table.addRow(new Label("Properties:"), this.label3); - this.table.addRow(new Label("Records:"), this.label4); - this.table.addRow(new Label("Files:"), this.label1); - addComponent(this.table); - - // init INFO - Info.getInstance().notifyObserver(null, null); - - // adds itself to StatsPanel - StatsPanel.addStat("Entities", this); - start(); - } - - @Override - public void start() { - this.active = true; - update(); - } - - @Override - public void stop() { - this.active = false; - } - - @Override - public void update() { - if (this.active) { - try { - this.label1.setText(Info.getFilesCount().toString()); - this.label2.setText(Info.getRecordTypesCount().toString()); - this.label3.setText(Info.getPropertiesCount().toString()); - this.label4.setText(Info.getRecordsCount().toString()); - } catch (final Exception e) { - e.printStackTrace(); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/MainWindow.java b/src/main/java/org/caosdb/server/terminal/MainWindow.java deleted file mode 100644 index aa22f12b23bed00dfb46db8c7cd140fb05551eec..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/MainWindow.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Action; -import com.googlecode.lanterna.gui.Border; -import com.googlecode.lanterna.gui.Window; -import com.googlecode.lanterna.gui.component.Button; -import com.googlecode.lanterna.gui.component.Label; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.dialog.DialogButtons; -import com.googlecode.lanterna.gui.dialog.DialogResult; -import com.googlecode.lanterna.gui.dialog.MessageBox; -import com.googlecode.lanterna.gui.layout.LinearLayout; -import com.googlecode.lanterna.input.Key; -import com.googlecode.lanterna.input.Key.Kind; -import org.caosdb.server.utils.Observer; - -public class MainWindow extends Window { - - // A panel at the top of the window which contains buttons to switch - // between several data panels which are to be shown in the dataPanel. - final Panel dataSelectorPanel = new Panel(new Border.Invisible(), Panel.Orientation.HORISONTAL); - - // A panel right below the dataSelectorPanel. Here the interesting - // data is to be show. The contained panels can be made visible or - // invisible via the buttons in the dataSelectorPanel. - final Panel dataPanel = new Panel(new Border.Bevel(true), Panel.Orientation.HORISONTAL); - - public final synchronized void setVisibleDataPanel(final Panel visibleDataPanel) { - this.dataPanel.removeAllComponents(); - this.dataPanel.addComponent(visibleDataPanel); - } - - public MainWindow() { - super("Caos DB"); - addComponent(this.dataSelectorPanel, LinearLayout.MAXIMIZES_HORIZONTALLY); - addComponent( - this.dataPanel, LinearLayout.MAXIMIZES_HORIZONTALLY, LinearLayout.MAXIMIZES_VERTICALLY); - - // initializing stream output panels. These pipe the output of - // System.out and System.err to a TextArea. - final SystemOutPanel systemOutPanel = SystemOutPanel.getInstance(); - final Thread systemOutThread = new Thread(systemOutPanel); - systemOutThread.setName("systemOutPanel"); - - final SystemErrPanel systemErrPanel = SystemErrPanel.getInstance(); - final Thread systemErrThread = new Thread(systemErrPanel); - systemErrThread.setName("systemErrPanel"); - - systemOutThread.start(); - systemErrThread.start(); - - // initializing DatabaseAccessPanel that shows which thread is accessing - // the database and how many threads are waiting for access. - final DatabaseAccessPanel databaseAccessPanel = DatabaseAccessPanel.getInstance(); - - // init StatsPanel - StatsPanel.getPanel(); - - // init entities stats - EntitiesPanel.getInstance(); - - // add all initialized panels to the main window - addDataPanel("System.out", systemOutPanel); - addDataPanel("System.err", systemErrPanel); - addDataPanel("DB Access", databaseAccessPanel); - addDataPanel("Misc", StatsPanel.getPanel()); - - // add a welcome panel - this.dataPanel.addComponent( - new Label("Welcome. This is CaosDB - An Open Scientific DataBase.")); - - // add shutdown button - this.dataSelectorPanel.addComponent(new ShutdownButton()); - - // set focus on the dataSelectorPanel - setFocus(this.dataSelectorPanel.nextFocus(null)); - } - - /** - * Add a panel to the dataPanel. A button is created within the dataSelectorPanel which shows the - * name of the panel - * - * @param name - * @param dataPanel - */ - private void addDataPanel(final String name, final Panel dataPanel) { - this.dataSelectorPanel.addComponent(new DataPanelSelectorButton(name, dataPanel)); - } - - /** - * A Button that causes the dataPanel to show a certain panel. - * - * @author tf - */ - class DataPanelSelectorButton extends Button { - public DataPanelSelectorButton(final String name, final Panel panel) { - super( - name, - new Action() { - - @Override - public void doAction() { - // show panel in dataPanel - setVisibleDataPanel(panel); - - // update panel if necessary - if (panel instanceof Observer) { - ((Observer) panel).notifyObserver(null, null); - } - - // move focus to panel - setFocus(panel.nextFocus(null)); - } - }); - } - } - - @Override - public void onKeyPressed(final Key key) { - if (key.getKind() == Kind.Escape) { - setFocus(this.dataSelectorPanel.nextFocus(null)); - } else { - super.onKeyPressed(key); - } - } - - /** - * A button that causes the server to shut down when pressed. - * - * @author tf - */ - class ShutdownButton extends Button { - public ShutdownButton() { - super( - "Shutdown Server", - new Action() { - @Override - public void doAction() { - final DialogResult result = - MessageBox.showMessageBox( - getOwner(), - "Server shutdown", - "Select [OK] to shut down the server now.", - DialogButtons.OK_CANCEL); - if (result == DialogResult.OK) { - System.exit(0); - } - } - }); - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/OutputStreamPanel.java b/src/main/java/org/caosdb/server/terminal/OutputStreamPanel.java deleted file mode 100644 index 50284f8a88e7d30596cc901be24108f3ea201d0a..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/OutputStreamPanel.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Action; -import com.googlecode.lanterna.gui.component.Button; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.component.TextArea; -import com.googlecode.lanterna.gui.layout.LinearLayout; -import com.googlecode.lanterna.input.Key; -import com.googlecode.lanterna.input.Key.Kind; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.io.PrintStream; - -class PipedPrintStream extends PrintStream { - private final TextArea out; - int lines = 0; - - public PipedPrintStream(final TextArea out) { - super(new NullOutputStream()); - this.out = out; - } - - @Override - public void println(final String x) { - if (open) { - synchronized (this) { - out.appendLine(x); - if (lines >= 4000) { - out.removeLine(0); - } else { - lines++; - } - } - } - } - - @Override - public void print(final String x) {} - - private final boolean open = false; -} - -class NullOutputStream extends OutputStream { - - @Override - public void write(final int b) throws IOException {} -} - -public abstract class OutputStreamPanel extends Panel implements Runnable { - - private final TextArea logArea = new TextArea(); - private BufferedReader reader = null; - protected final PipedOutputStream panelOutputStream = new PipedOutputStream(); - - protected final PipedOutputStream getPanelOutputStream() { - return panelOutputStream; - } - - Button toogle = - new Button( - "on/off", - new Action() { - - @Override - public void doAction() { - toogle(); - } - }); - - private boolean on = true; - - void toogle() { - on = !on; - } - - public OutputStreamPanel() { - super(); - // addComponent(toogle); - addComponent(logArea, LinearLayout.MAXIMIZES_HORIZONTALLY, LinearLayout.MAXIMIZES_VERTICALLY); - } - - protected void init() throws IOException { - final PipedInputStream pIn = new PipedInputStream(panelOutputStream); - reader = new BufferedReader(new InputStreamReader(pIn)); - } - - @Override - public final void run() { - int lines = 0; - while (true) { - while (on) { - try { - if (reader.ready()) { - logArea.appendLine(reader.readLine()); - if (lines >= 4000) { - logArea.removeLine(0); - } else { - lines++; - } - try { - logArea.keyboardInteraction(new Key(Kind.End)); - } catch (final NullPointerException e) { - // this usually happens when the logArea is updated - // but - // not painted (while not being an active panel) - } - } else { - Thread.sleep(1000); - } - - } catch (final Exception e2) { - e2.printStackTrace(); - } - } - } - } - - @Override - protected void finalize() throws Throwable { - if (reader != null) { - reader.close(); - } - super.finalize(); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/StatComponent.java b/src/main/java/org/caosdb/server/terminal/StatComponent.java deleted file mode 100644 index 90a9ac1125425bd23415abf3e4186f00695b9b2f..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatComponent.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Component; - -public interface StatComponent extends Component { - - public void start(); - - public void stop(); - - public void update(); -} diff --git a/src/main/java/org/caosdb/server/terminal/StatLabel.java b/src/main/java/org/caosdb/server/terminal/StatLabel.java deleted file mode 100644 index 73a03ac31ed8874fc936807393701995130eaf7f..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatLabel.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; - -public class StatLabel extends Label implements Observer, StatComponent { - - boolean active = false; - Object obj; - String name; - - @Override - public boolean notifyObserver(final String e, final Observable o) { - update(); - return true; - } - - public StatLabel(final Object obj) { - this(null, obj); - } - - public StatLabel(final String name, final Object obj) { - this.name = name; - this.obj = obj; - if (obj instanceof Observable) { - ((Observable) obj).acceptObserver(this); - } - start(); - update(); - } - - @Override - public void stop() { - this.active = false; - } - - @Override - public void start() { - this.active = true; - } - - @Override - public void update() { - if (this.active) { - setText((this.name == null ? "" : this.name + ": ") + this.obj.toString()); - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/StatTable.java b/src/main/java/org/caosdb/server/terminal/StatTable.java deleted file mode 100644 index e7234cdb8000a7c4dcc98463c3e9ca6cc8c9f19c..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatTable.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Component; -import com.googlecode.lanterna.gui.component.Panel; - -public class StatTable extends Panel implements StatComponent { - - @Override - public void start() { - for (final Component c : components()) { - if (c instanceof StatComponent) { - ((StatComponent) c).start(); - } - } - } - - @Override - public void stop() { - for (final Component c : components()) { - if (c instanceof StatComponent) { - ((StatComponent) c).stop(); - } - } - } - - @Override - public void update() { - for (final Component c : components()) { - if (c instanceof StatComponent) { - ((StatComponent) c).update(); - } - } - } -} diff --git a/src/main/java/org/caosdb/server/terminal/StatsPanel.java b/src/main/java/org/caosdb/server/terminal/StatsPanel.java deleted file mode 100644 index 3b41ac6a9c4ae7366e85cffedbd71f83b30c6438..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/StatsPanel.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.Action; -import com.googlecode.lanterna.gui.Border; -import com.googlecode.lanterna.gui.component.Button; -import com.googlecode.lanterna.gui.component.Panel; -import com.googlecode.lanterna.gui.layout.LinearLayout; -import java.util.HashMap; - -public class StatsPanel extends Panel { - - private static StatsPanel instance = null; - - HashMap<String, StatTable> stats = new HashMap<String, StatTable>(); - Panel dataPanel = new Panel("Data", new Border.Bevel(true), Panel.Orientation.VERTICAL); - Panel selectorPanel = new Panel("Selector", new Border.Bevel(true), Panel.Orientation.VERTICAL); - StatComponent current = null; - - private StatsPanel() { - super(Panel.Orientation.HORISONTAL); - addComponent(this.selectorPanel, LinearLayout.MAXIMIZES_VERTICALLY); - addComponent( - this.dataPanel, LinearLayout.MAXIMIZES_VERTICALLY, LinearLayout.MAXIMIZES_HORIZONTALLY); - } - - public static void addStat(final String name, final Object obj) { - if (instance != null) { - instance.addStatComponent(name, new StatLabel(obj)); - } - } - - public static void addStat(final String name, final StatComponent statComponent) { - if (instance != null) { - instance.addStatComponent(name, statComponent); - } - } - - private void addStatComponent(final String name, final StatComponent statComponent) { - final StatTable c = this.stats.get(name); - if (c != null) { - c.addComponent(statComponent); - } else { - final StatTable table = new StatTable(); - table.addComponent(statComponent); - this.selectorPanel.addComponent(new SelectorButton(name, table)); - this.stats.put(name, table); - } - } - - private void setVisibleDataPanel(final StatComponent panel) { - this.dataPanel.removeAllComponents(); - - if (this.current != null) { - this.current.stop(); - } - this.current = panel; - this.current.start(); - this.current.update(); - - this.dataPanel.addComponent(this.current); - } - - class SelectorButton extends Button { - public SelectorButton(final String name, final StatComponent panel) { - super( - name, - new Action() { - - @Override - public void doAction() { - // show panel in dataPanel - setVisibleDataPanel(panel); - } - }); - } - } - - public static Panel getPanel() { - if (instance == null) { - instance = new StatsPanel(); - } - return instance; - } -} diff --git a/src/main/java/org/caosdb/server/terminal/SystemErrPanel.java b/src/main/java/org/caosdb/server/terminal/SystemErrPanel.java deleted file mode 100644 index 9bf800fdb3c755cd8ea2b2a55119cf4e093fde09..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/SystemErrPanel.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import java.io.IOException; -import java.io.PrintStream; - -public final class SystemErrPanel extends OutputStreamPanel { - - private static final SystemErrPanel instance = new SystemErrPanel(); - private PrintStream err; - - public static final SystemErrPanel getInstance() { - return instance; - } - - private SystemErrPanel() { - super(); - try { - init(); - this.err = System.err; - final PrintStream newErr = new PrintStream(getPanelOutputStream()); - System.setErr(newErr); - } catch (final IOException e) { - addComponent(new Label("Sorry, could not initialize this SystemErrPanel")); - } - } - - public static void close() { - System.setErr(instance.err); - } -} diff --git a/src/main/java/org/caosdb/server/terminal/SystemOutPanel.java b/src/main/java/org/caosdb/server/terminal/SystemOutPanel.java deleted file mode 100644 index b34cdaf97d8088a97ea66932d9abc96d6e1589be..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/terminal/SystemOutPanel.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.terminal; - -import com.googlecode.lanterna.gui.component.Label; -import java.io.IOException; -import java.io.PrintStream; - -public final class SystemOutPanel extends OutputStreamPanel { - - private static final SystemOutPanel instance = new SystemOutPanel(); - - public static final SystemOutPanel getInstance() { - return instance; - } - - private SystemOutPanel() { - super(); - try { - init(); - System.setOut(new PrintStream(getPanelOutputStream())); - } catch (final IOException e) { - addComponent(new Label("Sorry, could not initialize this SystemOutPanel")); - } - } -} diff --git a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java index 0a2d1363427d9e21504b421c88e7605af59634ac..4897cd65620577e36a98f2db5e5530a42b3f43d0 100644 --- a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java @@ -22,7 +22,7 @@ */ package org.caosdb.server.transaction; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.misc.RollBackHandler; import org.caosdb.server.entity.Message; @@ -33,7 +33,7 @@ public abstract class AccessControlTransaction implements TransactionInterface { @Override public final void execute() throws Exception { - this.access = DatabaseMonitor.getAccountAccess(this); + this.access = DatabaseAccessManager.getAccountAccess(this); try { transaction(); diff --git a/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java b/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java index b9d350719f13b5bd26e44f7d2b959a428adf5793..56fc07da22d9fac5bfbe713d1a9cd36b2d25a9ba 100644 --- a/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java +++ b/src/main/java/org/caosdb/server/transaction/ChecksumUpdater.java @@ -25,7 +25,7 @@ package org.caosdb.server.transaction; import java.io.IOException; import java.security.NoSuchAlgorithmException; import org.caosdb.server.CaosDBException; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.GetUpdateableChecksums; import org.caosdb.server.database.backend.transaction.RetrieveSparseEntity; @@ -34,7 +34,7 @@ import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.FileProperties; import org.caosdb.server.entity.Message; -import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.container.WritableContainer; import org.caosdb.server.utils.FileUtils; /** @@ -43,14 +43,15 @@ import org.caosdb.server.utils.FileUtils; * * @author tf */ -public class ChecksumUpdater extends WriteTransaction<TransactionContainer> implements Runnable { +public class ChecksumUpdater extends WriteTransaction + implements Runnable, WriteTransactionInterface { private Boolean running = false; private static final ChecksumUpdater instance = new ChecksumUpdater(); private ChecksumUpdater() { - super(new TransactionContainer()); + super(new WritableContainer()); } /** Retrieves all file without a checksum, calculates one and stores it to the database */ @@ -76,14 +77,14 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl Access strongAccess = null; try { - strongAccess = DatabaseMonitor.getInstance().allocateStrongAccess(this); + strongAccess = DatabaseAccessManager.getInstance().reserveWriteAccess(this); if (wasChangedInTheMeantime(fileEntity, strongAccess, lastModified)) { continue; } fileEntity.getFileProperties().setChecksum(checksum); - DatabaseMonitor.getInstance().acquireStrongAccess(this); + DatabaseAccessManager.getInstance().acquireWriteAccess(this); // update execute(new SetFileChecksum(fileEntity), strongAccess); @@ -130,8 +131,8 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl return null; } - private EntityInterface getNextUpdateableFileEntity() { - final Access weakAccess = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + private EntityInterface getNextUpdateableFileEntity() throws InterruptedException { + final Access weakAccess = DatabaseAccessManager.getInstance().acquireReadAccess(this); try { synchronized (instance.running) { diff --git a/src/main/java/org/caosdb/server/transaction/Delete.java b/src/main/java/org/caosdb/server/transaction/Delete.java deleted file mode 100644 index 44e68b3afe6225f286ab76329b699f0cf0f10670..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/transaction/Delete.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.transaction; - -import org.apache.shiro.authz.AuthorizationException; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.DeleteEntity; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.container.DeleteContainer; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.jobs.JobExecutionTime; -import org.caosdb.server.permissions.EntityPermission; -import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.ServerMessages; - -public class Delete extends WriteTransaction<DeleteContainer> { - - public Delete(final DeleteContainer container) { - super(container); - } - - @Override - protected void init() throws Exception {} - - @Override - protected void preCheck() throws InterruptedException, Exception { - // allocate strong (write) access. Only one thread can do this - // at a time. But weak access can still be acquired by other - // thread until the allocated strong access is actually - // acquired. - setAccess(getMonitor().allocateStrongAccess(this)); - - // retrieve all entities which are to be deleted. - execute(new RetrieveFullEntity(getContainer()), getAccess()); - - for (final EntityInterface e : getContainer()) { - if (e.getEntityStatus() == EntityStatus.NONEXISTENT) { - e.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); - continue; - } - - // check permissions - try { - e.checkPermission(EntityPermission.DELETE); - } catch (final AuthorizationException exc) { - e.setEntityStatus(EntityStatus.UNQUALIFIED); - e.addError(ServerMessages.AUTHORIZATION_ERROR); - e.addInfo(exc.getMessage()); - } - - // no standard entities are to be deleted. - if (e.hasId() && e.getId() < 100) { - e.setEntityStatus(EntityStatus.UNQUALIFIED); - e.addInfo("This entity cannot be deleted"); - } - } - - // make schedule of existing entities which are to be deleted. - makeSchedule(); - getSchedule().runJobs(JobExecutionTime.INIT); - } - - @Override - protected void postCheck() {} - - @Override - protected void postTransaction() { - // set entityStatus to DELETED and add deletion info message - if (getContainer().getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - for (final EntityInterface entity : getContainer()) { - if (entity.getEntityStatus() == EntityStatus.VALID) { - entity.setEntityStatus(EntityStatus.DELETED); - entity.addInfo(ServerMessages.ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY); - } - } - } - } - - @Override - public void transaction() throws Exception { - // delete entities from database - delete(getContainer(), getAccess()); - } - - private void delete(final TransactionContainer container, final Access access) throws Exception { - if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - execute(new DeleteEntity(container), access); - } - } - - @Override - public boolean logHistory() { - return true; - } -} diff --git a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java index 5d2a04b1e9f45360f47a93889f638eab23fbd512..ed93812d22eaad6bf448fa1dede2d92cea1d50a6 100644 --- a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java +++ b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.TimeZone; import org.caosdb.datetime.UTCDateTime; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.FileConsistencyCheck; import org.caosdb.server.database.backend.transaction.GetFileIterator; @@ -64,8 +64,8 @@ public class FileStorageConsistencyCheck extends Thread @Override public void run() { - this.access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); try { + this.access = DatabaseAccessManager.getInstance().acquireReadAccess(this); // test all files in file system. final Iterator<String> iterator = @@ -73,12 +73,12 @@ public class FileStorageConsistencyCheck extends Thread this.ts = System.currentTimeMillis(); while (iterator != null && iterator.hasNext()) { - if (DatabaseMonitor.whoHasAllocatedStrongAccess() != null) { + if (DatabaseAccessManager.whoHasReservedWriteAccess() != null) { // there is a thread waiting to write. pause this one and // apply for a new weak access which will be granted when // the write thread is ready. this.access.release(); - this.access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + this.access = DatabaseAccessManager.getInstance().acquireReadAccess(this); } final String path = iterator.next(); diff --git a/src/main/java/org/caosdb/server/transaction/Insert.java b/src/main/java/org/caosdb/server/transaction/Insert.java deleted file mode 100644 index d37f7ce516f36252466555ea18fe249f2730fea1..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/transaction/Insert.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.transaction; - -import org.apache.shiro.SecurityUtils; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.InsertEntity; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.FileProperties; -import org.caosdb.server.entity.container.InsertContainer; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.permissions.EntityACL; -import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.ServerMessages; - -public class Insert extends WriteTransaction<InsertContainer> { - - public Insert(final InsertContainer container) { - super(container); - } - - @Override - protected void init() throws Exception { - // allocate strong (write) access. Only one thread can do this - // at a time. But weak access can still be acquired by other - // thread until the allocated strong access is actually - // acquired. - setAccess(getMonitor().allocateStrongAccess(this)); - - // make schedule for all parsed entities; - makeSchedule(); - - // dereference files (file upload only) - for (final EntityInterface entity : getContainer()) { - if (entity.hasFileProperties() && !entity.getFileProperties().isPickupable()) { - if (entity.getFileProperties().getTmpIdentifyer() != null) { - final FileProperties f = - getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer()); - if (f != null) { - entity.getFileProperties().setFile(f.getFile()); - if (f.getThumbnail() != null) { - entity.getFileProperties().setThumbnail(f.getThumbnail()); - } else { - final FileProperties thumbnail = - getContainer() - .getFiles() - .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); - if (thumbnail != null) { - entity.getFileProperties().setThumbnail(thumbnail.getFile()); - } - } - } else { - entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); - entity.setEntityStatus(EntityStatus.UNQUALIFIED); - } - } - } - } - } - - @Override - protected void preCheck() throws InterruptedException { - for (final EntityInterface entity : getContainer()) { - // set default EntityACL if none present - if (entity.getEntityACL() == null) { - entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject())); - } - } - } - - @Override - protected void postCheck() {} - - @Override - protected void postTransaction() {} - - @Override - public void transaction() throws Exception { - // write new entities to database - insert(getContainer(), getAccess()); - } - - public void insert(final TransactionContainer container, final Access access) throws Exception { - if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - execute(new InsertEntity(container), access); - } - } - - @Override - public boolean logHistory() { - return true; - } -} diff --git a/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java index be01d93086e65694313c0d97c6b1603aef6a6bc7..c1128135b8620ebfe08328cd4901e9c0e6062053 100644 --- a/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java @@ -24,7 +24,7 @@ package org.caosdb.server.transaction; import java.util.List; import java.util.logging.LogRecord; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.InsertLogRecord; @@ -38,7 +38,7 @@ public class InsertLogRecordTransaction implements TransactionInterface { @Override public void execute() throws Exception { - final Access access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + final Access access = DatabaseAccessManager.getInstance().acquireReadAccess(this); try { execute(new InsertLogRecord(this.toBeFlushed), access); } finally { diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java index 5b01657a9c0df55f2b93fe83532b074543debff9..fd7bae8a82e95554d527135f76124d397c23f0c4 100644 --- a/src/main/java/org/caosdb/server/transaction/Retrieve.java +++ b/src/main/java/org/caosdb/server/transaction/Retrieve.java @@ -24,7 +24,7 @@ package org.caosdb.server.transaction; import org.apache.shiro.authz.AuthorizationException; import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.container.RetrieveContainer; import org.caosdb.server.entity.xml.SetFieldStrategy; @@ -47,7 +47,7 @@ public class Retrieve extends Transaction<RetrieveContainer> { @Override protected void init() throws Exception { // acquire weak access - setAccess(getMonitor().acquiredWeakAccess(this)); + setAccess(getAccessManager().acquireReadAccess(this)); // resolve names final ResolveNames r = new ResolveNames(); @@ -137,7 +137,7 @@ public class Retrieve extends Transaction<RetrieveContainer> { private void retrieveFullEntities(final RetrieveContainer container, final Access access) throws Exception { - execute(new RetrieveFullEntity(container), access); + execute(new RetrieveFullEntityTransaction(container), access); } @Override diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java index 49d850dbcb672767da33a7c855063b04e1099f29..7e6c527865566795c397377409b1fc63020ca1dd 100644 --- a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java @@ -27,7 +27,7 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import org.apache.shiro.SecurityUtils; import org.caosdb.server.accessControl.ACMPermissions; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.RetrieveLogRecord; @@ -60,7 +60,7 @@ public class RetrieveLogRecordTransaction implements TransactionInterface { @Override public void execute() throws Exception { SecurityUtils.getSubject().checkPermission(ACMPermissions.PERMISSION_RETRIEVE_SERVERLOGS); - final Access access = DatabaseMonitor.getInstance().acquiredWeakAccess(this); + final Access access = DatabaseAccessManager.getInstance().acquireReadAccess(this); try { this.logRecords = execute(new RetrieveLogRecord(this.logger, this.level, this.message), access) diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java b/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java index d82985fe8060b3919d48c17a6bbd2994fc92c5bf..520787105388d8f1408c2d3b05090c761c8f687c 100644 --- a/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java +++ b/src/main/java/org/caosdb/server/transaction/RetrieveSparseEntityByPath.java @@ -41,7 +41,7 @@ public class RetrieveSparseEntityByPath extends Transaction<TransactionContainer @Override protected void init() throws Exception { // acquire weak access - setAccess(getMonitor().acquiredWeakAccess(this)); + setAccess(getAccessManager().acquireReadAccess(this)); } @Override diff --git a/src/main/java/org/caosdb/server/transaction/Transaction.java b/src/main/java/org/caosdb/server/transaction/Transaction.java index 0235b8a25cd1d7df9f923023b1be2d5597038b0e..c2bd7e0feb62fa31dadd5580990350b829ff4591 100644 --- a/src/main/java/org/caosdb/server/transaction/Transaction.java +++ b/src/main/java/org/caosdb/server/transaction/Transaction.java @@ -29,7 +29,7 @@ import java.util.List; import org.apache.shiro.subject.Subject; import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.accessControl.Principal; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.InsertTransactionHistory; import org.caosdb.server.database.exceptions.TransactionException; @@ -61,7 +61,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra return getContainer().getTransactionBenchmark(); } - private static final DatabaseMonitor monitor = DatabaseMonitor.getInstance(); + private static final DatabaseAccessManager monitor = DatabaseAccessManager.getInstance(); public static final String CLEAN_UP = "TransactionCleanUp"; private final C container; @@ -77,7 +77,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra if (o != null) acceptObserver(o); } - public static DatabaseMonitor getMonitor() { + public static DatabaseAccessManager getAccessManager() { return monitor; } @@ -231,8 +231,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra String realm = ((Principal) getTransactor().getPrincipal()).getRealm(); String username = ((Principal) getTransactor().getPrincipal()).getUsername(); execute( - new InsertTransactionHistory( - getContainer(), this.getClass().getSimpleName(), realm, username, getTimestamp()), + new InsertTransactionHistory(getContainer(), realm, username, getTimestamp()), getAccess()); } } diff --git a/src/main/java/org/caosdb/server/transaction/Update.java b/src/main/java/org/caosdb/server/transaction/Update.java deleted file mode 100644 index f5a9a7ffd8b32e4b8d220ea6bba44731b51047ef..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/transaction/Update.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.transaction; - -import com.google.common.base.Objects; -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.util.HashSet; -import java.util.Set; -import org.apache.shiro.authz.AuthorizationException; -import org.caosdb.server.CaosDBException; -import org.caosdb.server.database.access.Access; -import org.caosdb.server.database.backend.transaction.RetrieveFullEntity; -import org.caosdb.server.database.backend.transaction.UpdateEntity; -import org.caosdb.server.entity.Entity; -import org.caosdb.server.entity.EntityInterface; -import org.caosdb.server.entity.FileProperties; -import org.caosdb.server.entity.RetrieveEntity; -import org.caosdb.server.entity.container.TransactionContainer; -import org.caosdb.server.entity.container.UpdateContainer; -import org.caosdb.server.entity.wrapper.Parent; -import org.caosdb.server.entity.wrapper.Property; -import org.caosdb.server.permissions.EntityPermission; -import org.caosdb.server.permissions.Permission; -import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.ServerMessages; - -public class Update extends WriteTransaction<UpdateContainer> { - - public Update(final UpdateContainer container) { - super(container); - } - - @Override - protected void preCheck() throws Exception {} - - @Override - protected void init() throws Exception { - // collect all ids of the entities which are to be updated. - final TransactionContainer oldContainer = new TransactionContainer(); - for (final EntityInterface entity : getContainer()) { - // entity has no id -> it cannot be updated. - if (!entity.hasId()) { - entity.addError(ServerMessages.ENTITY_HAS_NO_ID); - entity.setEntityStatus(EntityStatus.UNQUALIFIED); - continue; - } - - // add entity with this id to the updateContainer - if (entity.getEntityStatus() == EntityStatus.QUALIFIED) { - final Entity oldEntity = new RetrieveEntity(entity.getId()); - oldContainer.add(oldEntity); - } - } - - // allocate strong (write) access. Only one thread can do this - // at a time. But weak access can still be acquired by other - // thread until the allocated strong access is actually - // acquired. - setAccess(getMonitor().allocateStrongAccess(this)); - - // retrieve a container which contains all id of those entities - // which are to be updated. - execute(new RetrieveFullEntity(oldContainer), getAccess()); - - // Check if any updates are to be processed. - for (final EntityInterface newEntity : getContainer()) { - if (newEntity.getEntityStatus() == EntityStatus.QUALIFIED) { - innerLoop: - for (final EntityInterface oldEntity : oldContainer) { - if (oldEntity.getId().equals(newEntity.getId())) { - if (oldEntity.getEntityStatus() == EntityStatus.NONEXISTENT) { - newEntity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - } else { - // dereference files (upload only) - if (newEntity.hasFileProperties() && !newEntity.getFileProperties().isPickupable()) { - - if (newEntity.getFileProperties().getTmpIdentifyer() != null) { - // get file by tmpIdentifier - final FileProperties f = - getContainer() - .getFiles() - .get(newEntity.getFileProperties().getTmpIdentifyer()); - - // is it there? - if (f != null) { - newEntity.getFileProperties().setFile(f.getFile()); - - // has it a thumbnail? - if (f.getThumbnail() != null) { - newEntity.getFileProperties().setThumbnail(f.getThumbnail()); - } else { - final FileProperties thumbnail = - getContainer() - .getFiles() - .get(newEntity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); - if (thumbnail != null) { - newEntity.getFileProperties().setThumbnail(thumbnail.getFile()); - } else { - newEntity.addWarning(ServerMessages.THUMBNAIL_HAS_NOT_BEEN_UPLOAED); - } - } - } else { - newEntity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - } - - } else { - // in case no file has been uploaded, - // the file is expected to stay - // unchanged. Therefore we let the file - // object point to the original file in - // the file system. - newEntity - .getFileProperties() - .setFile(oldEntity.getFileProperties().retrieveFromFileSystem()); - } - } - - try { - checkPermissions(newEntity, deriveUpdate(newEntity, oldEntity)); - } catch (final AuthorizationException exc) { - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - newEntity.addError(ServerMessages.AUTHORIZATION_ERROR); - newEntity.addInfo(exc.getMessage()); - } - } - break innerLoop; - } - } - } - } - - // make schedule of those entities which are to be updated. - makeSchedule(); - } - - @Override - protected void postCheck() {} - - @Override - protected void postTransaction() {} - - @Override - public void transaction() throws Exception { - // write new entities to database - update(getContainer(), getAccess()); - } - - private void update(final TransactionContainer container, final Access access) throws Exception { - if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { - execute(new UpdateEntity(container), access); - } - } - - /** Check if the user has all permissions */ - public void checkPermissions(final EntityInterface entity, final Set<Permission> permissions) { - for (final Permission p : permissions) { - entity.checkPermission(p); - } - } - - /** - * The entity is set to VALID iff there are no updates to be processed. The entity is set to - * QUALIFIED otherwise. - * - * @param newEntity - * @param oldEntity - * @throws CaosDBException - * @throws IOException - * @throws NoSuchAlgorithmException - */ - public static HashSet<Permission> deriveUpdate( - final EntityInterface newEntity, final EntityInterface oldEntity) - throws NoSuchAlgorithmException, IOException, CaosDBException { - final HashSet<Permission> needPermissions = new HashSet<>(); - boolean updatetable = false; - - // new acl? - if (newEntity.hasEntityACL() && !newEntity.getEntityACL().equals(oldEntity.getEntityACL())) { - oldEntity.checkPermission(EntityPermission.EDIT_ACL); - if (!newEntity - .getEntityACL() - .getPriorityEntityACL() - .equals(oldEntity.getEntityACL().getPriorityEntityACL())) { - // priority acl is to be changed? - oldEntity.checkPermission(Permission.EDIT_PRIORITY_ACL); - } - updatetable = true; - } else if (!newEntity.hasEntityACL()) { - newEntity.setEntityACL(oldEntity.getEntityACL()); - } - - // new query template definition? - if (!Objects.equal( - newEntity.getQueryTemplateDefinition(), oldEntity.getQueryTemplateDefinition())) { - needPermissions.add(EntityPermission.UPDATE_QUERY_TEMPLATE_DEFINITION); - updatetable = true; - } - - // new datatype? - if (newEntity.hasDatatype() - && oldEntity.hasDatatype() - && !newEntity.getDatatype().equals(oldEntity.getDatatype()) - || newEntity.hasDatatype() ^ oldEntity.hasDatatype()) { - needPermissions.add(EntityPermission.UPDATE_DATA_TYPE); - updatetable = true; - } - - // entity role - if (newEntity.hasRole() - && oldEntity.hasRole() - && !newEntity.getRole().equals(oldEntity.getRole()) - || newEntity.hasRole() ^ oldEntity.hasRole()) { - needPermissions.add(EntityPermission.UPDATE_ROLE); - updatetable = true; - } - - // entity value - if (newEntity.hasValue() - && oldEntity.hasValue() - && !newEntity.getValue().equals(oldEntity.getValue()) - || newEntity.hasValue() ^ oldEntity.hasValue()) { - needPermissions.add(EntityPermission.UPDATE_VALUE); - updatetable = true; - } - - // entity name - if (newEntity.hasName() - && oldEntity.hasName() - && !newEntity.getName().equals(oldEntity.getName()) - || newEntity.hasName() ^ oldEntity.hasName()) { - needPermissions.add(EntityPermission.UPDATE_NAME); - updatetable = true; - } - - // entity description - if (newEntity.hasDescription() - && oldEntity.hasDescription() - && !newEntity.getDescription().equals(oldEntity.getDescription()) - || newEntity.hasDescription() ^ oldEntity.hasDescription()) { - needPermissions.add(EntityPermission.UPDATE_DESCRIPTION); - updatetable = true; - } - - // file properties - if (newEntity.hasFileProperties() || oldEntity.hasFileProperties()) { - if (newEntity.hasFileProperties() && !oldEntity.hasFileProperties()) { - // add a file - needPermissions.add(EntityPermission.UPDATE_ADD_FILE); - updatetable = true; - } else if (!newEntity.hasFileProperties() && oldEntity.hasFileProperties()) { - // remove a file - needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); - updatetable = true; - } else { - // change file - final FileProperties newFile = newEntity.getFileProperties(); - final FileProperties oldFile = oldEntity.getFileProperties(); - - // file path - if (newFile.hasPath() && oldFile.hasPath() && !newFile.getPath().equals(oldFile.getPath()) - || newFile.hasPath() ^ oldFile.hasPath()) { - // this means, the location of the file is to be changed - needPermissions.add(EntityPermission.UPDATE_MOVE_FILE); - updatetable = true; - } - - // change actual file (different byte code!) - if (!oldFile.retrieveFromFileSystem().equals(newFile.getFile())) { - // CHANGE A FILE is like REMOVE AND ADD - needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); - needPermissions.add(EntityPermission.UPDATE_ADD_FILE); - updatetable = true; - } - } - } - - // properties - outerLoop: - for (final EntityInterface newProperty : newEntity.getProperties()) { - - // find corresponding oldProperty for this new property and make a - // diff. - if (newProperty.hasId()) { - for (final EntityInterface oldProperty : oldEntity.getProperties()) { - if (newProperty.getId().equals(oldProperty.getId())) { - // do not check again. - oldEntity.getProperties().remove(oldProperty); - - if (((Property) oldProperty).getPIdx() != ((Property) newProperty).getPIdx()) { - // change order of properties - needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); - needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); - updatetable = true; - } - - deriveUpdate(newProperty, oldProperty); - if (newProperty.getEntityStatus() == EntityStatus.QUALIFIED) { - needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); - needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); - updatetable = true; - } - - continue outerLoop; - } - } - } else { - newProperty.setEntityStatus(EntityStatus.UNQUALIFIED); - newProperty.addError(ServerMessages.ENTITY_HAS_NO_ID); - newProperty.addInfo("On updates, allways specify the id not just the name."); - newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - return needPermissions; - } - - // no corresponding property found -> this property is new. - needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); - updatetable = true; - } - - // some old properties left (and not matched with new ones) -> there are - // properties to be deleted. - if (!oldEntity.getProperties().isEmpty()) { - needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); - updatetable = true; - } - - // update parents - outerLoop: - for (final Parent newParent : newEntity.getParents()) { - - // find corresponding oldParent - if (newParent.hasId()) { - for (final Parent oldParent : oldEntity.getParents()) { - if (oldParent.getId().equals(newParent.getId())) { - // still there! do not check this one again - oldEntity.getParents().remove(oldParent); - continue outerLoop; - } - } - } else { - newParent.setEntityStatus(EntityStatus.UNQUALIFIED); - newParent.addError(ServerMessages.ENTITY_HAS_NO_ID); - newParent.addInfo("On updates, allways specify the id not just the name."); - newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); - newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); - return needPermissions; - } - - // no corresponding parent found -> this parent is new. - needPermissions.add(EntityPermission.UPDATE_ADD_PARENT); - updatetable = true; - } - - // some old parents left (and not matched with new ones) -> there are - // parents to be deleted. - if (!oldEntity.getParents().isEmpty()) { - needPermissions.add(EntityPermission.UPDATE_REMOVE_PARENT); - updatetable = true; - } - - // nothing to be updated - if (!updatetable) { - newEntity.setEntityStatus(EntityStatus.VALID); - newEntity.addInfo("Nothing to be updated."); - } - - return needPermissions; - } - - @Override - public boolean logHistory() { - return true; - } -} diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java index 04c3834caad3264d6c1b8e5c99cf5b17a513e403..abda78b6feeb0de7104de5365877b28d474058fe 100644 --- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,13 +24,53 @@ */ package org.caosdb.server.transaction; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.AuthorizationException; +import org.caosdb.server.CaosDBException; +import org.caosdb.server.database.BackendTransaction; +import org.caosdb.server.database.access.Access; +import org.caosdb.server.database.backend.transaction.DeleteEntityTransaction; +import org.caosdb.server.database.backend.transaction.InsertEntityTransaction; +import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction; +import org.caosdb.server.database.backend.transaction.UpdateEntityTransaction; import org.caosdb.server.database.misc.RollBackHandler; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.FileProperties; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.RetrieveEntity; +import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.entity.container.TransactionContainer; +import org.caosdb.server.entity.container.WritableContainer; +import org.caosdb.server.entity.wrapper.Parent; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.jobs.Schedule; +import org.caosdb.server.permissions.EntityACL; +import org.caosdb.server.permissions.EntityPermission; +import org.caosdb.server.permissions.Permission; +import org.caosdb.server.query.Query; +import org.caosdb.server.utils.EntityStatus; +import org.caosdb.server.utils.ServerMessages; -public abstract class WriteTransaction<C extends TransactionContainer> extends Transaction<C> { +/** + * This class is responsible for inserting, updating and deleting entities which are held in the + * {@link TransactionContainer}. + * + * <p>This class initializes and runs the {@link Schedule} of {@link Job}s, calls the {@link + * BackendTransaction}s for each entity, and handles exceptions, roll-back (if necessary) and clean + * up afterwards. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class WriteTransaction extends Transaction<WritableContainer> + implements WriteTransactionInterface { - protected WriteTransaction(final C container) { + public WriteTransaction(final WritableContainer container) { super(container); } @@ -36,12 +78,13 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T protected final void preTransaction() throws InterruptedException { // acquire strong access. No other thread can have access until // it this strong access is released. - setAccess(getMonitor().acquireStrongAccess(this)); + setAccess(getAccessManager().acquireWriteAccess(this)); } @Override protected void commit() throws Exception { getAccess().commit(); + Query.clearCache(); } @Override @@ -51,6 +94,18 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T super.rollBack(); } + private void update(final TransactionContainer container, final Access access) throws Exception { + execute(new UpdateEntityTransaction(container), access); + } + + private void insert(final TransactionContainer container, final Access access) throws Exception { + execute(new InsertEntityTransaction(container), access); + } + + private void delete(final TransactionContainer container, final Access access) throws Exception { + execute(new DeleteEntityTransaction(container), access); + } + @Override protected final void cleanUp() { // release strong access. Other threads can acquire or allocate @@ -65,6 +120,437 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T } } + @Override + public boolean logHistory() { + return true; + } + + @Override + protected void init() throws Exception { + // collect all ids of the entities which are to be updated. + final TransactionContainer oldContainer = new TransactionContainer(); + // collect all ids of the entities which are to be deleted. + final TransactionContainer deleteContainer = new TransactionContainer(); + for (final EntityInterface entity : getContainer()) { + if (entity instanceof UpdateEntity) { + // entity has no id -> it cannot be updated. + if (!entity.hasId()) { + entity.addError(ServerMessages.ENTITY_HAS_NO_ID); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + continue; + } + + // add entity with this id to the updateContainer + if (entity.getEntityStatus() == EntityStatus.QUALIFIED) { + final EntityInterface oldEntity = new RetrieveEntity(entity.getId()); + oldContainer.add(oldEntity); + } + } else if (entity instanceof DeleteEntity) { + deleteContainer.add(entity); + } + } + + // Reserve write access. Only one thread can do this at a time. But read access can still be + // acquired by other threads until the reserved write access is actually acquired. + setAccess(getAccessManager().reserveWriteAccess(this)); + + // Retrieve a container which contains all IDs of those entities + // which are to be updated. + execute(new RetrieveFullEntityTransaction(oldContainer), getAccess()); + + // Retrieve all entities which are to be deleted. + execute(new RetrieveFullEntityTransaction(deleteContainer), getAccess()); + + // Check if any updates are to be processed. + for (final EntityInterface entity : getContainer()) { + if (entity instanceof UpdateEntity && entity.getEntityStatus() == EntityStatus.QUALIFIED) { + innerLoop: + for (final EntityInterface oldEntity : oldContainer) { + if (oldEntity.getId().equals(entity.getId())) { + if (oldEntity.getEntityStatus() == EntityStatus.NONEXISTENT) { + entity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + } else { + // dereference files (upload only) + if (entity.hasFileProperties() + && entity.getFileProperties().hasTmpIdentifier() + && !entity.getFileProperties().isPickupable()) { + + // get file by tmpIdentifier + final FileProperties f = + getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer()); + + // is it there? + if (f != null) { + entity.getFileProperties().setFile(f.getFile()); + + // has it a thumbnail? + if (f.getThumbnail() != null) { + entity.getFileProperties().setThumbnail(f.getThumbnail()); + } else { + final FileProperties thumbnail = + getContainer() + .getFiles() + .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); + if (thumbnail != null) { + entity.getFileProperties().setThumbnail(thumbnail.getFile()); + } else { + entity.addWarning(ServerMessages.THUMBNAIL_HAS_NOT_BEEN_UPLOAED); + } + } + } else { + entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + } + + } else if (entity.hasFileProperties() + && !entity.getFileProperties().hasTmpIdentifier()) { + // in case no file has been uploaded, + // the file is expected to stay + // unchanged. Therefore we let the file + // object point to the original file in + // the file system. + entity + .getFileProperties() + .setFile(oldEntity.getFileProperties().retrieveFromFileSystem()); + } + + try { + checkPermissions(entity, deriveUpdate(entity, oldEntity)); + } catch (final AuthorizationException exc) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addError(ServerMessages.AUTHORIZATION_ERROR); + entity.addInfo(exc.getMessage()); + } + } + break innerLoop; + } + } + } else if (entity instanceof DeleteEntity) { + if (entity.getEntityStatus() == EntityStatus.NONEXISTENT) { + entity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST); + continue; + } + + // check permissions + try { + entity.checkPermission(EntityPermission.DELETE); + } catch (final AuthorizationException exc) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addError(ServerMessages.AUTHORIZATION_ERROR); + entity.addInfo(exc.getMessage()); + } + + // no standard entities are to be deleted. + if (entity.hasId() && entity.getId() < 100) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addInfo("This entity cannot be deleted"); + } + + } else if (entity instanceof InsertEntity + && entity.hasFileProperties() + && entity.getFileProperties().hasTmpIdentifier() + && !entity.getFileProperties().isPickupable()) { + // dereference files (file upload only) + final FileProperties f = + getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer()); + if (f != null) { + entity.getFileProperties().setFile(f.getFile()); + if (f.getThumbnail() != null) { + entity.getFileProperties().setThumbnail(f.getThumbnail()); + } else { + final FileProperties thumbnail = + getContainer() + .getFiles() + .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail"); + if (thumbnail != null) { + entity.getFileProperties().setThumbnail(thumbnail.getFile()); + } else { + entity.addWarning(ServerMessages.THUMBNAIL_HAS_NOT_BEEN_UPLOAED); + } + } + } else { + entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED); + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + } + } + } + + makeSchedule(); + } + + /** Check if the user has all permissions */ + public static void checkPermissions( + final EntityInterface entity, final Set<Permission> permissions) { + for (final Permission p : permissions) { + entity.checkPermission(p); + } + } + + @Override + protected void preCheck() throws InterruptedException, Exception { + for (final EntityInterface entity : getContainer()) { + // set default EntityACL if none present + if (entity.getEntityACL() == null) { + entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject())); + } + } + } + + @Override + protected void postCheck() {} + + @Override + protected void transaction() throws Exception { + if (getContainer().getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { + + // split up the TransactionContainer into three containers, one for each + // type of writing transaction. + TransactionContainer inserts = new TransactionContainer(); + TransactionContainer updates = new TransactionContainer(); + TransactionContainer deletes = new TransactionContainer(); + for (EntityInterface entity : getContainer()) { + if (entity instanceof InsertEntity) { + inserts.add(entity); + } else if (entity instanceof UpdateEntity) { + updates.add(entity); + } else if (entity instanceof DeleteEntity) { + deletes.add(entity); + } + } + + // The order is crucial: 1) Update-entities may reference new entities which + // have to be inserted first. 2) Update-Entities which (originally) + // reference old entities which are to be deleted, have be updated before + // the others can be deleted. + insert(inserts, getAccess()); + update(updates, getAccess()); + delete(deletes, getAccess()); + } + } + + @Override + protected void postTransaction() throws Exception { + // set entityStatus to DELETED and add deletion info message for deleted entities. + if (getContainer().getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { + for (final EntityInterface entity : getContainer()) { + if (entity instanceof DeleteEntity && entity.getEntityStatus() == EntityStatus.VALID) { + entity.setEntityStatus(EntityStatus.DELETED); + entity.addInfo(ServerMessages.ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY); + } + } + } + } + + /** + * The entity is set to VALID iff there are no updates to be processed. The entity is set to + * QUALIFIED otherwise. + * + * @param newEntity + * @param oldEntity + * @throws CaosDBException + * @throws IOException + * @throws NoSuchAlgorithmException + */ + public static HashSet<Permission> deriveUpdate( + final EntityInterface newEntity, final EntityInterface oldEntity) + throws NoSuchAlgorithmException, IOException, CaosDBException { + final HashSet<Permission> needPermissions = new HashSet<>(); + boolean updatetable = false; + + // new acl? + if (newEntity.hasEntityACL() && !newEntity.getEntityACL().equals(oldEntity.getEntityACL())) { + oldEntity.checkPermission(EntityPermission.EDIT_ACL); + if (!newEntity + .getEntityACL() + .getPriorityEntityACL() + .equals(oldEntity.getEntityACL().getPriorityEntityACL())) { + // priority acl is to be changed? + oldEntity.checkPermission(Permission.EDIT_PRIORITY_ACL); + } + updatetable = true; + } else if (!newEntity.hasEntityACL()) { + newEntity.setEntityACL(oldEntity.getEntityACL()); + } + + // new query template definition? + if (!Objects.equals( + newEntity.getQueryTemplateDefinition(), oldEntity.getQueryTemplateDefinition())) { + needPermissions.add(EntityPermission.UPDATE_QUERY_TEMPLATE_DEFINITION); + updatetable = true; + } + + // new datatype? + if (newEntity.hasDatatype() + && oldEntity.hasDatatype() + && !newEntity.getDatatype().equals(oldEntity.getDatatype()) + || newEntity.hasDatatype() ^ oldEntity.hasDatatype()) { + needPermissions.add(EntityPermission.UPDATE_DATA_TYPE); + updatetable = true; + } + + // entity role + if (newEntity.hasRole() + && oldEntity.hasRole() + && !newEntity.getRole().equals(oldEntity.getRole()) + || newEntity.hasRole() ^ oldEntity.hasRole()) { + needPermissions.add(EntityPermission.UPDATE_ROLE); + updatetable = true; + } + + // entity value + if (newEntity.hasValue() + && oldEntity.hasValue() + && !newEntity.getValue().equals(oldEntity.getValue()) + || newEntity.hasValue() ^ oldEntity.hasValue()) { + needPermissions.add(EntityPermission.UPDATE_VALUE); + updatetable = true; + } + + // entity name + if (newEntity.hasName() + && oldEntity.hasName() + && !newEntity.getName().equals(oldEntity.getName()) + || newEntity.hasName() ^ oldEntity.hasName()) { + needPermissions.add(EntityPermission.UPDATE_NAME); + updatetable = true; + } + + // entity description + if (newEntity.hasDescription() + && oldEntity.hasDescription() + && !newEntity.getDescription().equals(oldEntity.getDescription()) + || newEntity.hasDescription() ^ oldEntity.hasDescription()) { + needPermissions.add(EntityPermission.UPDATE_DESCRIPTION); + updatetable = true; + } + + // file properties + if (newEntity.hasFileProperties() || oldEntity.hasFileProperties()) { + if (newEntity.hasFileProperties() && !oldEntity.hasFileProperties()) { + // add a file + needPermissions.add(EntityPermission.UPDATE_ADD_FILE); + updatetable = true; + } else if (!newEntity.hasFileProperties() && oldEntity.hasFileProperties()) { + // remove a file + needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); + updatetable = true; + } else { + // change file + final FileProperties newFile = newEntity.getFileProperties(); + final FileProperties oldFile = oldEntity.getFileProperties(); + + // file path + if (newFile.hasPath() && oldFile.hasPath() && !newFile.getPath().equals(oldFile.getPath()) + || newFile.hasPath() ^ oldFile.hasPath()) { + // this means, the location of the file is to be changed + needPermissions.add(EntityPermission.UPDATE_MOVE_FILE); + updatetable = true; + } + + // change actual file (different byte code!) + if (!oldFile.retrieveFromFileSystem().equals(newFile.getFile())) { + // CHANGE A FILE is like REMOVE AND ADD + needPermissions.add(EntityPermission.UPDATE_REMOVE_FILE); + needPermissions.add(EntityPermission.UPDATE_ADD_FILE); + updatetable = true; + } + } + } + + // properties + outerLoop: + for (final EntityInterface newProperty : newEntity.getProperties()) { + + // find corresponding oldProperty for this new property and make a + // diff. + if (newProperty.hasId()) { + for (final EntityInterface oldProperty : oldEntity.getProperties()) { + if (newProperty.getId().equals(oldProperty.getId())) { + // do not check again. + oldEntity.getProperties().remove(oldProperty); + + if (((Property) oldProperty).getPIdx() != ((Property) newProperty).getPIdx()) { + // change order of properties + needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); + needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); + updatetable = true; + } + + deriveUpdate(newProperty, oldProperty); + if (newProperty.getEntityStatus() == EntityStatus.QUALIFIED) { + needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); + needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); + updatetable = true; + } + + continue outerLoop; + } + } + } else { + newProperty.setEntityStatus(EntityStatus.UNQUALIFIED); + newProperty.addError(ServerMessages.ENTITY_HAS_NO_ID); + newProperty.addInfo("On updates, allways specify the id not just the name."); + newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); + newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); + return needPermissions; + } + + // no corresponding property found -> this property is new. + needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY); + updatetable = true; + } + + // some old properties left (and not matched with new ones) -> there are + // properties to be deleted. + if (!oldEntity.getProperties().isEmpty()) { + needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY); + updatetable = true; + } + + // update parents + outerLoop: + for (final Parent newParent : newEntity.getParents()) { + + // find corresponding oldParent + if (newParent.hasId()) { + for (final Parent oldParent : oldEntity.getParents()) { + if (oldParent.getId().equals(newParent.getId())) { + // still there! do not check this one again + oldEntity.getParents().remove(oldParent); + continue outerLoop; + } + } + } else { + newParent.setEntityStatus(EntityStatus.UNQUALIFIED); + newParent.addError(ServerMessages.ENTITY_HAS_NO_ID); + newParent.addInfo("On updates, allways specify the id not just the name."); + newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES); + newEntity.setEntityStatus(EntityStatus.UNQUALIFIED); + return needPermissions; + } + + // no corresponding parent found -> this parent is new. + needPermissions.add(EntityPermission.UPDATE_ADD_PARENT); + updatetable = true; + } + + // some old parents left (and not matched with new ones) -> there are + // parents to be deleted. + if (!oldEntity.getParents().isEmpty()) { + needPermissions.add(EntityPermission.UPDATE_REMOVE_PARENT); + updatetable = true; + } + + // nothing to be updated + if (!updatetable) { + newEntity.setEntityStatus(EntityStatus.VALID); + newEntity.addInfo("Nothing to be updated."); + } + + return needPermissions; + } + public String getSRID() { return getContainer().getRequestId(); } diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java b/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..165acb408776a720c6887d31b550cf57e3d6fa0c --- /dev/null +++ b/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java @@ -0,0 +1,8 @@ +package org.caosdb.server.transaction; + +import org.caosdb.server.database.access.Access; + +public interface WriteTransactionInterface extends TransactionInterface { + + public Access getAccess(); +} diff --git a/src/main/java/org/caosdb/server/utils/EntityStatus.java b/src/main/java/org/caosdb/server/utils/EntityStatus.java index ef8bdc101c61e216c9aa448d667b59dfa5107672..d6658ef12535bca15a6af7b1d16a4d3d8c0b5e58 100644 --- a/src/main/java/org/caosdb/server/utils/EntityStatus.java +++ b/src/main/java/org/caosdb/server/utils/EntityStatus.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> + * Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,17 +25,24 @@ package org.caosdb.server.utils; /** - * IGNORE - This instance must be ignored during further processing. <br> - * VALID - This instance has a final ID and has been synchronized with the database. <br> - * WRITTEN - This instance has been written successfully to the database. It has a final ID. <br> - * QUALIFIED - This instance has a provisional or final ID, represents a well-formed entity, and any - * referenced entities (by a reference property) have status QUALIFIED or VALID. QUALIFIED - This - * instance has been checked and is not qualified to be processed further (i.e. to be inserted, - * updated, deleted) DELETED - This instance has been deleted recently. CORRUPT - This instance has - * been retrieved from the database, but though something turned out to be wrong with it. - * NONEXISTENT - This instance has been called (via id or something) but it doesn't exist. - * - * @author Timm Fitschen + * The order of the EntityStatus values matters, in that everything later than "QUALIFIED" also + * counts as qualified. + * + * <p> + * + * <ul> + * <li>IGNORE - This instance must be ignored during further processing. + * <li>UNQUALIFIED - This instance has been checked and is not qualified to be processed further + * (i.e. to be inserted, updated, deleted) + * <li>DELETED - This instance has been deleted recently. + * <li>NONEXISTENT - This instance has been called (via id or something) but it doesn't exist. + * <li>QUALIFIED - This instance has a provisional or final ID, represents a well-formed entity, + * and any referenced entities (by a reference property) have status QUALIFIED or VALID. Every + * status after this one also counts as QUALIFIED. + * <li>VALID - This instance has a final ID and has been synchronized with the database. + * </ul> + * + * @author Timm Fitschen <t.fitschen@indiscale.com> */ public enum EntityStatus { IGNORE, diff --git a/src/main/java/org/caosdb/server/utils/Info.java b/src/main/java/org/caosdb/server/utils/Info.java index 19f792cf625765924f02a7b734f2e57849584ffc..18ee09828006c0394dec315ca77483f8298ba86b 100644 --- a/src/main/java/org/caosdb/server/utils/Info.java +++ b/src/main/java/org/caosdb/server/utils/Info.java @@ -30,7 +30,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.caosdb.server.CaosDBServer; import org.caosdb.server.FileSystem; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.transaction.GetInfo; import org.caosdb.server.database.backend.transaction.SyncStats; @@ -58,7 +58,7 @@ public class Info extends AbstractObservable implements Observer, TransactionInt } private Info() { - this.access = DatabaseMonitor.getInfoAccess(this); + this.access = DatabaseAccessManager.getInfoAccess(this); try { syncDatabase(); } catch (final Exception exc) { diff --git a/src/main/java/org/caosdb/server/utils/Initialization.java b/src/main/java/org/caosdb/server/utils/Initialization.java index baae53804f57433aa58da1ae1b9c58060c3db160..cb1a36307928a72b9da95b48737196e569c43346 100644 --- a/src/main/java/org/caosdb/server/utils/Initialization.java +++ b/src/main/java/org/caosdb/server/utils/Initialization.java @@ -24,7 +24,7 @@ */ package org.caosdb.server.utils; -import org.caosdb.server.database.DatabaseMonitor; +import org.caosdb.server.database.DatabaseAccessManager; import org.caosdb.server.database.access.Access; import org.caosdb.server.transaction.TransactionInterface; @@ -34,7 +34,7 @@ public final class Initialization implements TransactionInterface, AutoCloseable private static final Initialization instance = new Initialization(); private Initialization() { - this.access = DatabaseMonitor.getInitAccess(this); + this.access = DatabaseAccessManager.getInitAccess(this); } public static final Initialization setUp() { diff --git a/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java b/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java index 1aff4de329bf3f32ba599a449c101b7fbe30423f..375d6b5e7bbced84cec5bdcdee26a9e436f0215a 100644 --- a/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java +++ b/src/main/java/org/caosdb/server/utils/WebinterfaceUtils.java @@ -33,6 +33,7 @@ import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.caosdb.server.resource.AbstractCaosDBServerResource; import org.caosdb.server.resource.Webinterface; +import org.restlet.Request; import org.restlet.data.Reference; /** @@ -61,14 +62,19 @@ public class WebinterfaceUtils { private static final Map<String, WebinterfaceUtils> instances = new HashMap<>(); /** - * Retrieve an instance of {@link WebinterfaceUtils} for the host. The instance can be shared with - * other callers. + * Retrieve an instance of {@link WebinterfaceUtils} for the request. The instance can be shared + * with other callers. * - * @param host + * @param request * @return a shared instance of {@link WebinterfaceUtils}. */ - public static WebinterfaceUtils getInstance(Reference host) { - return getInstance(host.getHostIdentifier()); + public static WebinterfaceUtils getInstance(Request request) { + String hostStr = request.getHostRef().getHostIdentifier(); + String scheme = request.getHeaders().getFirstValue("X-Forwarded-Proto", true); + if (scheme != null) { + hostStr = hostStr.replaceFirst("^" + request.getHostRef().getScheme(), scheme); + } + return getInstance(hostStr); } /** diff --git a/src/test/java/org/caosdb/server/database/DatabaseMonitorTest.java b/src/test/java/org/caosdb/server/database/DatabaseAccessManagerTest.java similarity index 52% rename from src/test/java/org/caosdb/server/database/DatabaseMonitorTest.java rename to src/test/java/org/caosdb/server/database/DatabaseAccessManagerTest.java index 554baec079ed17d332dff47f0afb57405913dc33..8e08131f3b18d9e4a495301ee570683facb4d9c5 100644 --- a/src/test/java/org/caosdb/server/database/DatabaseMonitorTest.java +++ b/src/test/java/org/caosdb/server/database/DatabaseAccessManagerTest.java @@ -25,61 +25,61 @@ package org.caosdb.server.database; import org.junit.Assert; import org.junit.Test; -public class DatabaseMonitorTest { - public static final WeakAccessSemaphore wa = new WeakAccessSemaphore(); - public static final StrongAccessLock sa = new StrongAccessLock(wa); +public class DatabaseAccessManagerTest { + public static final ReadAccessSemaphore readAccess = new ReadAccessSemaphore(); + public static final WriteAccessLock writeAccess = new WriteAccessLock(readAccess); /** - * Two read-, two write-threads. The read-threads request weak access, the write-threads request - * strong access.<br> - * The read-thread rt1 is started and gets weak access. Then the write-thread wt1 starts and - * allocates strong access.<br> - * A second read-thread rt2 starts and also gets weak access. A second write thread wt2 starts and - * requests allocation of strong access. It has to wait until wt2 releases it. <br> - * wt1 acquires strong access as soon as both read-threads released their weak-accesss.<br> + * Two read-, two write-threads. The read-threads request read access, the write-threads request + * write access.<br> + * The read-thread rt1 is started and gets read access. Then the write-thread wt1 starts and + * reserves write access.<br> + * A second read-thread rt2 starts and also gets read access. A second write thread wt2 starts and + * requests allocation of write access. It has to wait until wt2 releases it. <br> + * wt1 acquires write access as soon as both read-threads released their read-accesss.<br> * * @throws InterruptedException */ @Test public void test1() throws InterruptedException { // first invoke a read thread - final ReadThread rt1 = new ReadThread(); - rt1.start(); // paused, has acquired weak access now + final ReadThread rt1 = new ReadThread("rt1"); + rt1.start(); // paused, has acquired read access now // invoke a write thread - final WriteThread wt1 = new WriteThread(); - wt1.start(); // paused, has allocated strong access now + final WriteThread wt1 = new WriteThread("wt1"); + wt1.start(); // paused, has reserved write access now synchronized (this) { - this.wait(1000); + this.wait(500); } // waiting means any processing Assert.assertEquals(wt1.getState(), Thread.State.WAITING); - final ReadThread rt2 = new ReadThread(); - rt2.start(); // paused, has acquired a second weak access now + final ReadThread rt2 = new ReadThread("rt2"); + rt2.start(); // paused, has acquired a second read access now synchronized (this) { - this.wait(1000); + this.wait(500); } synchronized (rt2) { rt2.notify(); } synchronized (this) { - this.wait(1000); + this.wait(500); } - // rt2 was processed while wt1 has allocated but not yet acquired strong - // access. rt2 terminated after releasing its weak access. + // rt2 was processed while wt1 has reserved but not yet acquired write + // access. rt2 terminated after releasing its read access. Assert.assertEquals(rt2.getState(), Thread.State.TERMINATED); - final WriteThread wt2 = new WriteThread(); - wt2.start(); // wt2 immediatelly allocates strong access and is block + final WriteThread wt2 = new WriteThread("wt2"); + wt2.start(); // wt2 immediatelly reserves write access and is block // since wt1 already yields it. synchronized (this) { - this.wait(1000); + this.wait(500); } - // Assert.assertEquals(wt2.getState(), Thread.State.BLOCKED); + Assert.assertEquals(wt2.getState(), Thread.State.WAITING); - // wt1 request strong access. + // wt1 request write access. synchronized (wt1) { wt1.notify(); } @@ -92,10 +92,10 @@ public class DatabaseMonitorTest { rt1.notify(); } synchronized (this) { - this.wait(1000); + this.wait(500); } - // rt1 was notified an terminated, releasing the weak acccess. - // so wt1 acquires strong access and pauses the second time. + // rt1 was notified an terminated, releasing the read acccess. + // so wt1 acquires write access and pauses the second time. Assert.assertEquals(rt1.getState(), Thread.State.TERMINATED); synchronized (wt1) { @@ -104,16 +104,16 @@ public class DatabaseMonitorTest { synchronized (this) { this.wait(1000); } - // wt2 allocates strong access as wt1 released it now. + // wt2 reserves write access as wt1 released it now. Assert.assertEquals(wt1.getState(), Thread.State.TERMINATED); Assert.assertEquals(wt2.getState(), Thread.State.WAITING); - // while wt2 has not yet acquired strong access, rt3 acquires weak + // while wt2 has not yet acquired write access, rt3 acquires read // access - final ReadThread rt3 = new ReadThread(); + final ReadThread rt3 = new ReadThread("rt3"); rt3.start(); synchronized (this) { - this.wait(1000); + this.wait(500); } Assert.assertEquals(rt3.getState(), Thread.State.WAITING); @@ -121,24 +121,23 @@ public class DatabaseMonitorTest { wt2.notify(); } synchronized (this) { - this.wait(1000); + this.wait(500); } - // Assert.assertEquals(wt2.getState(), Thread.State.BLOCKED); + Assert.assertEquals(wt2.getState(), Thread.State.WAITING); synchronized (rt3) { rt3.notify(); } synchronized (this) { - this.wait(1000); + this.wait(500); } Assert.assertEquals(rt3.getState(), Thread.State.TERMINATED); - Assert.assertEquals(wt2.getState(), Thread.State.WAITING); synchronized (wt2) { wt2.notify(); } synchronized (this) { - this.wait(1000); + this.wait(500); } Assert.assertEquals(wt2.getState(), Thread.State.TERMINATED); } @@ -147,34 +146,35 @@ public class DatabaseMonitorTest { public void test2() throws InterruptedException { // start a write-thread - final WriteThread wt1 = new WriteThread(); + final WriteThread wt1 = new WriteThread("wt1"); wt1.start(); // start another write-thread. It is blocked until wt1 releases the - // strong access. - final WriteThread wt2 = new WriteThread(); + // write access. + final WriteThread wt2 = new WriteThread("wt2"); wt2.start(); synchronized (this) { - this.wait(1000); + this.wait(500); } - // and interrupt wt1 after allocating, but before acquiring the strong + // and interrupt wt1 after allocating, but before acquiring the write // access. wt1.interrupt(); synchronized (this) { - this.wait(1000); + this.wait(500); } synchronized (wt2) { wt2.notify(); } // read access should still be blocked. - final ReadThread rt1 = new ReadThread(); + final ReadThread rt1 = new ReadThread("rt1"); rt1.start(); synchronized (this) { - this.wait(1000); + this.wait(500); } + Assert.assertEquals(rt1.getState(), Thread.State.WAITING); synchronized (wt2) { wt2.notify(); } @@ -184,23 +184,29 @@ public class DatabaseMonitorTest { @Override public void run() { try { - System.out.println("T" + currentThread().getId() + " request allocation sa"); - sa.allocate(); - System.out.println("T" + currentThread().getId() + " allocates sa"); - pause(); - sa.lockInterruptibly(); - System.out.println("T" + currentThread().getId() + " acquires sa"); - pause(); - sa.unlock(); - System.out.println("T" + currentThread().getId() + " releases sa"); + System.out.println(currentThread().getName() + " request to reserve write access"); + writeAccess.reserve(); + System.out.println(currentThread().getName() + " has reserved write access "); + process("reserved write access"); + writeAccess.lockInterruptibly(); + System.out.println(currentThread().getName() + " acquires write access"); + process("acquired write access"); + writeAccess.unlock(); + System.out.println(currentThread().getName() + " releases write access"); } catch (final InterruptedException e) { - System.out.println("T" + currentThread().getId() + " was interrupted"); - sa.unlock(); + System.out.println(currentThread().getName() + " was interrupted"); + writeAccess.unlock(); } } - private synchronized void pause() throws InterruptedException { + private synchronized void process(String access) throws InterruptedException { + System.out.println(currentThread().getName() + " processes with " + access); this.wait(); + System.out.println(currentThread().getName() + " is ready with " + access); + } + + public WriteThread(String name) { + super(name); } } @@ -208,19 +214,25 @@ public class DatabaseMonitorTest { @Override public void run() { try { - System.out.println("T" + currentThread().getId() + " requests wa"); - wa.acquire(); - System.out.println("T" + currentThread().getId() + " acquires wa"); + System.out.println(currentThread().getName() + " requests read access"); + readAccess.acquire(); + System.out.println(currentThread().getName() + " acquires read access"); pause(); - wa.release(); - System.out.println("T" + currentThread().getId() + " releases wa"); + readAccess.release(); + System.out.println(currentThread().getName() + " releases read access"); } catch (final InterruptedException e) { e.printStackTrace(); } } private synchronized void pause() throws InterruptedException { + System.out.println(currentThread().getName() + " waits "); this.wait(); + System.out.println(currentThread().getName() + " goes on"); + } + + public ReadThread(String name) { + super(name); } } } diff --git a/src/test/java/org/caosdb/server/database/backend/transaction/BackendTransactionTest.java b/src/test/java/org/caosdb/server/database/backend/transaction/BackendTransactionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..93402bf0d06c487451d9219b4487f301eb855f49 --- /dev/null +++ b/src/test/java/org/caosdb/server/database/backend/transaction/BackendTransactionTest.java @@ -0,0 +1,24 @@ +package org.caosdb.server.database.backend.transaction; + +import static org.junit.Assert.assertEquals; + +import org.caosdb.server.database.BackendTransaction; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.RetrieveEntity; +import org.caosdb.server.entity.Role; +import org.caosdb.server.entity.UpdateEntity; +import org.jdom2.Element; +import org.junit.Test; + +public class BackendTransactionTest { + + @Test + public void testGetTransactionType() { + BackendTransaction transaction = new InsertTransactionHistory(null, null, null, null); + assertEquals("Retrieve", transaction.getTransactionType(new RetrieveEntity("test"))); + assertEquals("Insert", transaction.getTransactionType(new InsertEntity("test", Role.Record))); + assertEquals("Delete", transaction.getTransactionType(new DeleteEntity(1234))); + assertEquals("Update", transaction.getTransactionType(new UpdateEntity(new Element("Record")))); + } +} diff --git a/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java b/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java index 52b4c951d939f097258ec40ed16b6fd99784f9bd..4b6429b73946110b0fd173de9c53a3b30fd83e18 100644 --- a/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java +++ b/src/test/java/org/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java @@ -40,8 +40,8 @@ public class RetrieveFullEntityTest { @Test public void testRetrieveSubEntities() { - RetrieveFullEntity r = - new RetrieveFullEntity(0) { + RetrieveFullEntityTransaction r = + new RetrieveFullEntityTransaction(0) { /** Mock-up */ @Override diff --git a/src/test/java/org/caosdb/server/entity/EntityTest.java b/src/test/java/org/caosdb/server/entity/EntityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3bc85dfbf81ba865e2ebb543a28b72c5e468a485 --- /dev/null +++ b/src/test/java/org/caosdb/server/entity/EntityTest.java @@ -0,0 +1,59 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +package org.caosdb.server.entity; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.caosdb.server.datatype.IntegerDatatype; +import org.junit.Test; + +public class EntityTest { + + @Test + public void testIsReference() { + EntityInterface entity = new RetrieveEntity("test"); + assertFalse(entity.isReference()); + + entity.setDatatype(new IntegerDatatype()); + assertFalse(entity.isReference()); + + entity.setDatatype("Person"); + assertTrue(entity.isReference()); + } + + @Test + public void testIsReferenceList() { + EntityInterface entity = new RetrieveEntity("test"); + assertFalse(entity.isReferenceList()); + + entity.setDatatype(new IntegerDatatype()); + assertFalse(entity.isReferenceList()); + + entity.setDatatype("Person"); + assertFalse(entity.isReferenceList()); + + entity.setDatatype("LIST<Person>"); + assertTrue(entity.isReferenceList()); + } +} diff --git a/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java b/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java index 99cf2706937dce10b8db3a1305053356587e139a..436278ad640663ba42f71b824924b2f1293a5132 100644 --- a/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java +++ b/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java @@ -77,6 +77,7 @@ public class PropertyToElementStrategyTest { house.addProperty(houseHeight); windowProperty = new Property(2345); windowProperty.setName("window"); + windowProperty.setDatatype("window"); windowProperty.setValue(new ReferenceValue(window.getId())); house.addProperty(windowProperty); diff --git a/src/test/java/org/caosdb/server/query/QueryTest.java b/src/test/java/org/caosdb/server/query/QueryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..092ebfcf833e6e60ff4edbb095a0759739975141 --- /dev/null +++ b/src/test/java/org/caosdb/server/query/QueryTest.java @@ -0,0 +1,51 @@ +package org.caosdb.server.query; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import org.caosdb.server.CaosDBServer; +import org.junit.BeforeClass; +import org.junit.Test; + +public class QueryTest { + + @BeforeClass + public static void initServerProperties() throws IOException { + CaosDBServer.initServerProperties(); + } + + String getCacheKey(String query) { + Query q = new Query(query); + q.parse(); + return q.getCacheKey(); + } + + @Test + public void testGetKey() { + assertEquals("enamePOV(pname,=,val1)", getCacheKey("FIND ename WITH pname = val1")); + assertEquals("enamePOV(pname,=,val1)", getCacheKey("COUNT ename WITH pname = val1")); + assertEquals("enamePOV(pname,=,val1)", getCacheKey("SELECT bla FROM ename WITH pname = val1")); + assertEquals("enamePOV(pname,null,null)", getCacheKey("SELECT bla FROM ename WITH pname")); + assertEquals( + "enamemaxPOV(pname,null,null)", + getCacheKey("SELECT bla FROM ename WITH THE GREATEST pname")); + + assertEquals( + "RECORDenamePOV(pname,=,val1)", getCacheKey("FIND RECORD ename WITH pname = val1")); + assertEquals("ENTITYPOV(pname,=,val1)", getCacheKey("COUNT ENTITY WITH pname = val1")); + assertEquals( + "enameConj(POV(pname,=,val1)POV(ename2,=,val2))", + getCacheKey("SELECT bla FROM ename WITH pname = val1 AND ename2 = val2")); + + assertEquals("versionedENTITYID(,>,2)", getCacheKey("FIND ANY VERSION OF ENTITY WITH ID > 2")); + assertEquals("ENTITYID(min,,)", getCacheKey("FIND ENTITY WITH THE SMALLEST ID")); + assertEquals("ENTITYSAT(asdf/%%)", getCacheKey("FIND ENTITY WHICH IS STORED AT /asdf/*")); + assertEquals("ENTITYSAT(asdf/asdf)", getCacheKey("FIND ENTITY WHICH IS STORED AT asdf/asdf")); + assertEquals( + "enamePOV(ref1,null,null)SUB(POV(pname,>,val1)", + getCacheKey("FIND ename WITH ref1 WITH pname > val1 ")); + assertEquals( + "ename@(ref1,null)SUB(POV(pname,>,val1)", + getCacheKey("FIND ename WHICH IS REFERENCED BY ref1 WITH pname > val1 ")); + } +} diff --git a/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java b/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java index 1dcf71d8f330ab6807e65bd23022138e941fca1c..5d81ca738d507bcfd75dbe137756135cede3989b 100644 --- a/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java +++ b/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java @@ -26,6 +26,7 @@ import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.TransactionBenchmark; import org.caosdb.server.permissions.PermissionRule; +import org.caosdb.server.utils.WebinterfaceUtils; import org.jdom2.Element; import org.junit.BeforeClass; import org.junit.Rule; @@ -125,6 +126,11 @@ public class TestAbstractCaosDBServerResource { // TODO Auto-generated method stub return user; } + + @Override + public WebinterfaceUtils getUtils() { + return WebinterfaceUtils.getInstance(getRootRef().toString()); + } }; provideUserSourcesFile(); Element response = s.generateRootElement(); diff --git a/src/test/java/org/caosdb/server/transaction/UpdateTest.java b/src/test/java/org/caosdb/server/transaction/UpdateTest.java index 6b0e7b3f988f18142c6ebdb535212cd7454233f0..d22fbea8982b1f8d88c9ccdf069a0c8a816add25 100644 --- a/src/test/java/org/caosdb/server/transaction/UpdateTest.java +++ b/src/test/java/org/caosdb/server/transaction/UpdateTest.java @@ -44,7 +44,7 @@ public class UpdateTest { throws NoSuchAlgorithmException, IOException, CaosDBException { final Entity newEntity = new Entity("Name"); final Entity oldEntity = new Entity("Name"); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), EntityStatus.VALID); } @@ -53,7 +53,7 @@ public class UpdateTest { throws NoSuchAlgorithmException, IOException, CaosDBException { final Entity newEntity = new Entity("NewName"); final Entity oldEntity = new Entity("OldName"); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), EntityStatus.QUALIFIED); } @@ -67,7 +67,7 @@ public class UpdateTest { final Entity oldEntity = new Entity(); oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), VALID); } @@ -83,7 +83,7 @@ public class UpdateTest { final Entity oldEntity = new Entity(); oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), QUALIFIED); assertEquals(newProperty.getEntityStatus(), VALID); assertEquals(newProperty2.getEntityStatus(), QUALIFIED); @@ -124,7 +124,7 @@ public class UpdateTest { oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newUnit.getEntityStatus(), VALID); assertEquals(newProperty.getEntityStatus(), VALID); assertEquals(newEntity.getEntityStatus(), VALID); @@ -165,7 +165,7 @@ public class UpdateTest { oldEntity.addProperty(oldProperty); - Update.deriveUpdate(newEntity, oldEntity); + WriteTransaction.deriveUpdate(newEntity, oldEntity); assertEquals(newEntity.getEntityStatus(), QUALIFIED); assertEquals(newProperty.getEntityStatus(), QUALIFIED); }