diff --git a/CHANGELOG.md b/CHANGELOG.md index 09aa1b2604d414a9b5c510d9292f539e48b98fcc..3907b4e320916caa7abd5d900dbb882215cfa1fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Wrong serialization of date time values in the GRPC-API (resulting in org.caosdb.server.datatime@12347abcd or similar). * Missing serialization of file descriptors in the GRPC-API during retrievals. +* [caosdb-server#131](https://gitlab.com/caosdb/caosdb-server/-/issues/131) + Query: AND does not work with sub-properties ### Security + ## [v0.7.1] - 2021-12-13 (Timm Fitschen) diff --git a/README_SETUP.md b/README_SETUP.md index 5065e3383c4ac2f2d549bfedb15687d0b4124ec0..d46722d26458757a53f081dd1ce9af3db2688283 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -58,12 +58,14 @@ libpam0g-dev`. Then try again. After a fresh clone of the repository, this is what you need to setup the server: -1. Compile the server with `make compile`. This may take a while and there +1. Install the `proto` submodule (and submodules for those extensions you want, see above): + `git submodule update --init caosdb-proto` +2. Compile the server with `make compile`. This may take a while and there needs to be an internet connection as packages are downloaded to be integrated in the java file. 1. It is recommended to run the unit tests with `make test`. It may take a while. -2. Create an SSL certificate somewhere with a `Java Key Store` file. For +3. Create an SSL certificate somewhere with a `Java Key Store` file. For self-signed certificates (not recommended for production use) you can do: - `mkdir certificates; cd certificates` - `keytool -genkey -keyalg RSA -alias selfsigned -keystore caosdb.jks -validity 375 -keysize 2048 -ext san=dns:localhost` @@ -77,11 +79,11 @@ server: Alternatively, you can create a keystore from certificate files that you already have: - `openssl pkcs12 -export -inkey privkey.pem -in fullchain.pem -out all-certs.pkcs12` - `keytool -importkeystore -srckeystore all-certs.pkcs12 -srcstoretype PKCS12 -deststoretype pkcs12 -destkeystore caosdb.jks` -3. Install/configure the MySQL back-end: see the `README_SETUP.md` of the +4. Install/configure the MySQL back-end: see the `README_SETUP.md` of the `caosdb-mysqlbackend` repository -4. Create an authtoken config (e.g. copy `conf/core/authtoken.example.yaml` to +5. Create an authtoken config (e.g. copy `conf/core/authtoken.example.yaml` to `conf/ext/authtoken.yml` and change it) -5. Copy `conf/core/server.conf` to `conf/ext/server.conf` and change it +6. Copy `conf/core/server.conf` to `conf/ext/server.conf` and change it appropriately: * Setup for MySQL back-end: specify the fields `MYSQL_USER_NAME`, `MYSQL_USER_PASSWORD`, @@ -94,7 +96,7 @@ server: `CERTIFICATES_KEY_STORE_PATH`, and `CERTIFICATES_KEY_STORE_PASSWORD`. Make sure that the conf file is not readable by other users because the certificate passwords are stored in plaintext. - - Set the path to the authtoken config (see step 4) + * Set the path to the authtoken config (see step 4) * Set the file system paths: - `FILE_SYSTEM_ROOT`: The root for all the files managed by CaosDB. - `DROP_OFF_BOX`: Files can be put here for insertion into CaosDB. @@ -112,8 +114,8 @@ server: - `INSERT_FILES_IN_DIR_ALLOWED_DIRS`: add mounted filesystems here that shall be accessible by CaosDB * Maybe set another `SESSION_TIMEOUT_MS`. - * See also [CONFIGURATION.rst](src/doc/administration/configuration.rst) -6. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`. + * See also [CONFIGURATION.rst](src/doc/administration/configuration.rst) +7. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`. * You can skip this if you do not want to use an external authentication. Local users (CaosDB realm) are always available. * Define the users/groups who you want to include/exclude. @@ -128,7 +130,7 @@ server: Especially that there are no `properties` (aka `keys`) without a `value`. An emtpy value can be represented by `""`. Comments are everything from `#` or `;` to the end of the line. -7. Possibly install the PAM caller in `misc/pam_authentication/` if you have +8. Possibly install the PAM caller in `misc/pam_authentication/` if you have not do so already. See above. Done! diff --git a/src/doc/index.rst b/src/doc/index.rst index 4f63fe07f37a4432a442040ff2f614f01c959472..b5ce9f3235277b613b357dca5dfc3334d80aeb3f 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -10,6 +10,7 @@ Welcome to caosdb-server's documentation! Getting started <README_SETUP> Concepts <concepts> + tutorials Query Language <CaosDB-Query-Language> administration Development <development/devel> diff --git a/src/doc/tutorials.rst b/src/doc/tutorials.rst new file mode 100644 index 0000000000000000000000000000000000000000..27aacfb3eb9a4f04ab261b12f904e652c4daf3d3 --- /dev/null +++ b/src/doc/tutorials.rst @@ -0,0 +1,10 @@ +Tutorials +============== + +.. toctree:: + :maxdepth: 1 + :glob: + + tutorials/* + + diff --git a/src/doc/tutorials/setup_state_model.py b/src/doc/tutorials/setup_state_model.py new file mode 100755 index 0000000000000000000000000000000000000000..0a1a7daa3b14d7c3e7a5b8eac093857bddfad330 --- /dev/null +++ b/src/doc/tutorials/setup_state_model.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com> +# Copyright (C) 2021 Henrik tom Wörden <h.tomwoerden@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/>. +# +""" +This is a utility script to setup a publication process in LinkAhead using +states. + +If you start from scratch you should perform the following actions in that +order: + +1. setup_roles +2. setup_state_data_model +4. setup_model_publication_cycle +""" +from argparse import ArgumentParser, RawDescriptionHelpFormatter + +import caosdb as db +from caosdb.common.administration import generate_password + + +def teardown(args): + """fully clears the database""" + + if "yes" != input( + "Are you really sure that you want to delete ALL " + "ENTITIES in LinkAhead? [yes/No]" + ): + + print("Nothing done.") + + return + d = db.execute_query("FIND ENTITY WITH ID > 99") + + if len(d) > 0: + d.delete(flags={"forceFinalState": "true"}) + + +def soft_teardown(args): + """ allows to remove state data only """ + recs = db.execute_query("FIND Entity WITH State") + + for rec in recs: + rec.state = None + recs.update(flags={"forceFinalState": "true"}) + db.execute_query("FIND StateModel").delete() + db.execute_query("FIND Transition").delete() + db.execute_query("FIND State").delete() + db.execute_query( + "FIND Property WITH name=from or name=to or name=initial or name=final or name=color").delete() + + +def setup_user(args): + """Creates a user with given username and adds the given role. + + If the user exists, it is deleted first. A random password is generated + and printed in clear text in the console output. + + """ + + username, role = args.username, args.role + try: + db.administration._delete_user(name=username) + except Exception: + pass + + password = generate_password(10) + print("new password for {}:\n{}".format(username, password)) + db.administration._insert_user( + name=username, password=password, status="ACTIVE") + db.administration._set_roles(username=username, roles=[role]) + + +def remove_user(args): + """deletes the given user""" + db.administration._delete_user(name=args.username) + + +def setup_role_permissions(): + """ + Adds the appropriate permissions to the 'normal' and 'publisher' role. + + The permissions are such that they suit the publication life cycle. + """ + db.administration._set_permissions( + role="normal", + permission_rules=[ + db.administration.PermissionRule("Grant", "TRANSACTION:*"), + db.administration.PermissionRule( + "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?" + ), + db.administration.PermissionRule("Grant", "STATE:TRANSITION:Edit"), + db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"), + db.administration.PermissionRule( + "Grant", "UPDATE:PROPERTY:REMOVE"), + db.administration.PermissionRule( + "Grant", "STATE:TRANSITION:Start Review"), + db.administration.PermissionRule( + "Grant", "STATE:ASSIGN:Publish Life-cycle" + ), + ], + ) + + db.administration._set_permissions( + role="publisher", + permission_rules=[ + db.administration.PermissionRule( + "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?" + ), + db.administration.PermissionRule("Grant", "TRANSACTION:*"), + db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"), + db.administration.PermissionRule( + "Grant", "UPDATE:PROPERTY:REMOVE"), + db.administration.PermissionRule("Grant", "STATE:*"), + ], + ) + + +def setup_roles(args): + """Creates 'publisher' and 'normla' roles and assigns appropriate + permissions + + If those roles exist they are deleted first. + """ + + for role in ["publisher", "normal"]: + try: + db.administration._delete_role(name=role) + except Exception: + print("Could not delete role {}".format(role)) + + for role in ["publisher", "normal"]: + db.administration._insert_role(name=role, description="") + + setup_role_permissions() + + +def setup_state_data_model(args): + """Creates the data model for using states + + RecordTypes: State, StateModel, Transition + Properties: from, to, initial, final, color + """ + cont = db.Container().extend( + [ + db.RecordType("State"), + db.RecordType("StateModel"), + db.RecordType("Transition"), + db.Property(name="from", datatype="State"), + db.Property(name="to", datatype="State"), + db.Property(name="initial", datatype="State"), + db.Property(name="final", datatype="State"), + db.Property(name="color", datatype=db.TEXT), + ] + ) + cont.insert() + + +def setup_model_publication_cycle(args): + """Creates States and Transitions for the Publication Life Cycle""" + unpublished_acl = db.ACL() + unpublished_acl.grant(role="publisher", permission="*") + unpublished_acl.grant(role="normal", permission="UPDATE:*") + unpublished_acl.grant(role="normal", permission="RETRIEVE:ENTITY") + unpublished_acl = db.State.create_state_acl(unpublished_acl) + + unpublished_state = ( + db.Record( + "Unpublished", + description="Unpublished entries are only visible to the team " + "and may be edited by any team member.", + ) + .add_parent("State") + .add_property("color", "#5bc0de") + ) + unpublished_state.acl = unpublished_acl + unpublished_state.insert() + + review_acl = db.ACL() + review_acl.grant(role="publisher", permission="*") + review_acl.grant(role="normal", permission="RETRIEVE:ENTITY") + + review_state = ( + db.Record( + "Under Review", + description="Entries under review are not publicly available yet, " + "but they can only be edited by the members of the publisher " + "group.", + ) + .add_parent("State") + .add_property("color", "#FFCC33") + ) + review_state.acl = db.State.create_state_acl(review_acl) + review_state.insert() + + published_acl = db.ACL() + published_acl.grant(role="guest", permission="RETRIEVE:ENTITY") + + published_state = ( + db.Record( + "Published", + description="Published entries are publicly available and " + "cannot be edited unless they are unpublished again.", + ) + .add_parent("State") + .add_property("color", "#333333") + ) + published_state.acl = db.State.create_state_acl(published_acl) + published_state.insert() + + # 1->2 + ( + db.Record( + "Start Review", + description="This transitions denies the permissions to edit an " + "entry for anyone but the members of the publisher group. " + "However, the entry is not yet publicly available.", + ) + .add_parent("Transition") + .add_property("from", "unpublished") + .add_property("to", "under review") + .add_property("color", "#FFCC33") + .insert() + ) + + # 2->3 + ( + db.Record( + "Publish", + description="Published entries are visible for the public and " + "cannot be changed unless they are unpublished again. Only members" + " of the publisher group can publish or unpublish entries.", + ) + .add_parent("Transition") + .add_property("from", "under review") + .add_property("to", "published") + .add_property("color", "red") + .insert() + ) + + # 3->1 + ( + db.Record( + "Unpublish", + description="Unpublish this entry to hide it from " + "the public. Unpublished entries can be edited by any team " + "member.", + ) + .add_parent("Transition") + .add_property("from", "published") + .add_property("to", "unpublished") + .insert() + ) + + # 2->1 + ( + db.Record( + "Reject", + description="Reject the publishing of this entity. Afterwards, " + "the entity is editable for any team member again.", + ) + .add_parent("Transition") + .add_property("from", "under review") + .add_property("to", "unpublished") + .insert() + ) + + # 1->1 + ( + db.Record( + "Edit", + description="Edit this entity. The changes are not publicly " + "available until this entity will have been reviewed and " + "published.", + ) + .add_parent( + "Transition", + ) + .add_property("from", "unpublished") + .add_property("to", "unpublished") + .insert() + ) + + ( + db.Record( + "Publish Life-cycle", + description="The publish life-cycle is a quality assurance tool. " + "Database entries can be edited without being publicly available " + "until the changes have been reviewed and explicitely published by" + " an eligible user.", + ) + .add_parent("StateModel") + .add_property( + "Transition", + datatype=db.LIST("Transition"), + value=[ + "Edit", + "Start Review", + "Reject", + "Publish", + "Unpublish", + ], + ) + .add_property("initial", "Unpublished") + .add_property("final", "Unpublished") + .insert() + ) + + +def parse_args(): + parser = ArgumentParser( + description=__doc__, formatter_class=RawDescriptionHelpFormatter + ) + subparsers = parser.add_subparsers( + title="action", + metavar="ACTION", + description=( + "You can perform the following actions. " + "Print the detailed help for each command with " + "#> setup_state_model ACTION -h" + ), + ) + + subparser = subparsers.add_parser( + "setup_state_data_model", help=setup_state_data_model.__doc__ + ) + subparser.set_defaults(call=setup_state_data_model) + + subparser = subparsers.add_parser( + "setup_model_publication_cycle", help=setup_model_publication_cycle.__doc__ + ) + subparser.set_defaults(call=setup_model_publication_cycle) + + subparser = subparsers.add_parser("setup_roles", help=setup_roles.__doc__) + subparser.set_defaults(call=setup_roles) + + subparser = subparsers.add_parser("remove_user", help=remove_user.__doc__) + subparser.set_defaults(call=remove_user) + subparser.add_argument("username") + + subparser = subparsers.add_parser("setup_user", help=setup_user.__doc__) + subparser.set_defaults(call=setup_user) + subparser.add_argument("username") + subparser.add_argument("role") + + subparser = subparsers.add_parser( + "teardown", help="Removes ALL ENTITIES from LinkAhead!" + ) + subparser.set_defaults(call=teardown) + + subparser = subparsers.add_parser( + "soft_teardown", help=soft_teardown.__doc__ + ) + subparser.set_defaults(call=soft_teardown) + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + args.call(args) diff --git a/src/doc/tutorials/statemachine.rst b/src/doc/tutorials/statemachine.rst new file mode 100644 index 0000000000000000000000000000000000000000..317620423e6221ccaf29297fc1f71f0c008f78a0 --- /dev/null +++ b/src/doc/tutorials/statemachine.rst @@ -0,0 +1,34 @@ + +State Machine +============= + +Prerequisites +------------- + +In order to use the state machine functionality you have to set the +corresponding server setting: ``EXT_ENTITY_STATE=ENABLED``. + +Also, a few RecordTypes and Properties are required. You can use the +script `setup_state_model.py <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/src/doc/tutorials/setup_state_model.py>`_ +to create those or you may have a look at it to see what is needed (``setup_state_data_model`` function). + +Defining the State Machine +-------------------------- + +Now you are setup to create your own state machine. You can define States and Transitions +and bundle it all to a StateModel. The above mentioned ``setup_state_model.py`` script defines +a publication cycle with the state "Unpublished", "UnderReview" and "Published". +Again, the ``setup_state_model.py`` script provides orientation on how this +can be setup (``setup_model_publication_cycle`` function). + +Note, that you can provide ACL to the state definition which will be applied to an entity once +the state is reached. This is for example useful to change the visibility depending on a state change. + +If you assign a state to a RecordType, this state will be the initial state +of Records that have that parent. For example by executing: + +.. code-block:: Python + + rt = db.RecordType("Article").retrieve() + rt.state = db.State(name="UnPublished", model="Publish Life-cycle")`` + rt.update() diff --git a/src/main/java/org/caosdb/server/query/CQLParser.g4 b/src/main/java/org/caosdb/server/query/CQLParser.g4 index c4311ac1cbe03f490b79456e091cc0e73b94c506..f8c2a3b5ea4e7c34f3cc8fd07cdc77c912a5cc7b 100644 --- a/src/main/java/org/caosdb/server/query/CQLParser.g4 +++ b/src/main/java/org/caosdb/server/query/CQLParser.g4 @@ -287,15 +287,36 @@ pov returns [POV filter] locals [Query.Pattern p, String o, String v, String a] ; -subproperty returns [SubProperty subp] locals [String p] +subproperty returns [SubProperty subp] @init{ - $p = null; $subp = null; } : - entity_filter {$subp = new SubProperty($entity_filter.filter);} + subproperty_filter {$subp = new SubProperty($subproperty_filter.filter);} ; +subproperty_filter returns [EntityFilterInterface filter] + @init{ + $filter = null; + } +: + which_exp + ( + ( + LPAREN WHITE_SPACE? + ( + filter_expression {$filter = $filter_expression.efi;} + | conjunction {$filter = $conjunction.c;} + | disjunction {$filter = $disjunction.d;} + ) + RPAREN + ) | ( + filter_expression {$filter = $filter_expression.efi;} + ) + )? +; + + backreference returns [Backreference ref] locals [Query.Pattern e, Query.Pattern p] @init{ $e = null; @@ -328,7 +349,7 @@ storedat returns [StoredAt filter] locals [String loc] WHITE_SPACE? ; -conjunction returns [Conjunction c] locals [Conjunction dummy] +conjunction returns [Conjunction c] @init{ $c = new Conjunction(); } @@ -493,7 +514,7 @@ number_with_unit unit : - (~(WHITE_SPACE | WHICH | HAS_A | WITH | WHERE | DOT | AND | OR )) + (~(WHITE_SPACE | WHICH | HAS_A | WITH | WHERE | DOT | AND | OR | RPAREN )) (~(WHITE_SPACE))* | NUM SLASH (~(WHITE_SPACE))+ @@ -510,7 +531,7 @@ atom returns [Query.Pattern ep] : double_quoted {$ep = $double_quoted.ep;} | single_quoted {$ep = $single_quoted.ep;} - | (~(WHITE_SPACE | DOT ))+ {$ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL);} + | (~(WHITE_SPACE | DOT | RPAREN | LPAREN ))+ {$ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL);} ; single_quoted returns [Query.Pattern ep] locals [StringBuffer sb, int patternType] diff --git a/src/test/java/org/caosdb/server/query/TestCQL.java b/src/test/java/org/caosdb/server/query/TestCQL.java index cf54bf71b69ea5f539744bd4bbe30fd1c37edcf4..ff1be776b041490aac7c434acdee73c96a9e88f9 100644 --- a/src/test/java/org/caosdb/server/query/TestCQL.java +++ b/src/test/java/org/caosdb/server/query/TestCQL.java @@ -264,6 +264,13 @@ public class TestCQL { String queryMR56 = "FIND ENTITY WITH ((p0 = v0 OR p1=v1) AND p2=v2)"; String versionedQuery1 = "FIND ANY VERSION OF ENTITY e1"; + // https://gitlab.com/caosdb/caosdb-server/-/issues/131 + String issue131a = "FIND ename WITH pname1.x AND pname2"; + String issue131b = "FIND ename WITH (pname1.x < 10) AND (pname1.x)"; + String issue131c = "FIND ename WITH pname2 AND pname1.x "; + String issue131d = "FIND ename WITH (pname1.x) AND pname2"; + String issue131e = "FIND ename WITH (pname1.pname2 > 30) AND (pname1.pname2 < 40)"; + String issue131f = "FIND ename WITH (pname1.pname2 > 30) AND pname1.pname2 < 40"; @Test public void testQuery1() @@ -4341,7 +4348,7 @@ public class TestCQL { assertEquals("THE GREATEST ID", conjunction.getChild(3).getText()); } - /** String query31 = "FIND PROPERTIES WHICH ARE INSERTED TODAY"; */ + /** String query31 = "FIND PROPERTIES WHICH WERE INSERTED TODAY"; */ @Test public void testQuery31() { CQLLexer lexer; @@ -6740,4 +6747,163 @@ public class TestCQL { // must not throw ParsingException new Query(this.queryIssue131).parse(); } + + /** String issue131a = "FIND ename WITH pname1.x AND pname2"; */ + @Test + public void testIssue131a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue131a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof Conjunction); + LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters(); + assertEquals(filters.size(), 2); + assertTrue(filters.get(0) instanceof POV); + assertTrue(filters.get(1) instanceof POV); + POV pov1 = ((POV) filters.get(0)); + POV pov2 = ((POV) filters.get(1)); + assertEquals("POV(pname1,null,null)", pov1.toString()); + assertEquals("POV(pname2,null,null)", pov2.toString()); + assertTrue(pov1.hasSubProperty()); + assertFalse(pov2.hasSubProperty()); + assertEquals("POV(x,null,null)", pov1.getSubProperty().getFilter().toString()); + } + + /** String issue131b = "FIND ename WITH (pname1.x < 10) AND (pname1.x)"; */ + @Test + public void testIssue131b() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue131b)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof Conjunction); + LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters(); + assertEquals(filters.size(), 2); + assertTrue(filters.get(0) instanceof POV); + assertTrue(filters.get(1) instanceof POV); + POV pov1 = ((POV) filters.get(0)); + POV pov2 = ((POV) filters.get(1)); + assertEquals("POV(pname1,null,null)", pov1.toString()); + assertEquals("POV(pname1,null,null)", pov2.toString()); + assertTrue(pov1.hasSubProperty()); + assertTrue(pov2.hasSubProperty()); + assertEquals("POV(x,<,10)", pov1.getSubProperty().getFilter().toString()); + assertEquals("POV(x,null,null)", pov2.getSubProperty().getFilter().toString()); + } + + /** String issue131c = "FIND ename WITH pname2 AND pname1.x "; */ + @Test + public void testIssue131c() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue131c)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof Conjunction); + LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters(); + assertEquals(filters.size(), 2); + assertTrue(filters.get(0) instanceof POV); + assertTrue(filters.get(1) instanceof POV); + POV pov1 = ((POV) filters.get(0)); + POV pov2 = ((POV) filters.get(1)); + assertEquals("POV(pname2,null,null)", pov1.toString()); + assertEquals("POV(pname1,null,null)", pov2.toString()); + assertFalse(pov1.hasSubProperty()); + assertTrue(pov2.hasSubProperty()); + assertEquals("POV(x,null,null)", pov2.getSubProperty().getFilter().toString()); + } + + /** String issue131d = "FIND ename WITH (pname1.x) AND pname2"; */ + @Test + public void testIssue131d() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue131d)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof Conjunction); + LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters(); + assertEquals(filters.size(), 2); + assertTrue(filters.get(0) instanceof POV); + assertTrue(filters.get(1) instanceof POV); + POV pov1 = ((POV) filters.get(0)); + POV pov2 = ((POV) filters.get(1)); + assertEquals("POV(pname1,null,null)", pov1.toString()); + assertEquals("POV(pname2,null,null)", pov2.toString()); + assertTrue(pov1.hasSubProperty()); + assertFalse(pov2.hasSubProperty()); + assertEquals("POV(x,null,null)", pov1.getSubProperty().getFilter().toString()); + } + + /** String issue131e = "FIND ename WITH (pname1.pname2 > 30) AND (pname1.pname2 < 40)"; */ + @Test + public void testIssue131e() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue131e)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof Conjunction); + LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters(); + assertEquals(filters.size(), 2); + assertTrue(filters.get(0) instanceof POV); + assertTrue(filters.get(1) instanceof POV); + POV pov1 = ((POV) filters.get(0)); + POV pov2 = ((POV) filters.get(1)); + assertEquals("POV(pname1,null,null)", pov1.toString()); + assertEquals("POV(pname1,null,null)", pov2.toString()); + assertTrue(pov1.hasSubProperty()); + assertTrue(pov2.hasSubProperty()); + assertEquals("POV(pname2,>,30)", pov1.getSubProperty().getFilter().toString()); + assertEquals("POV(pname2,<,40)", pov2.getSubProperty().getFilter().toString()); + } + + /** String issue131f = "FIND ename WITH (pname1.pname2 > 30) AND pname1.pname2 < 40"; */ + @Test + public void testIssue131f() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.issue131f)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + assertTrue(sfq.filter instanceof Conjunction); + LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters(); + assertEquals(filters.size(), 2); + assertTrue(filters.get(0) instanceof POV); + assertTrue(filters.get(1) instanceof POV); + POV pov1 = ((POV) filters.get(0)); + POV pov2 = ((POV) filters.get(1)); + assertEquals("POV(pname1,null,null)", pov1.toString()); + assertEquals("POV(pname1,null,null)", pov2.toString()); + assertTrue(pov1.hasSubProperty()); + assertTrue(pov2.hasSubProperty()); + assertEquals("POV(pname2,>,30)", pov1.getSubProperty().getFilter().toString()); + assertEquals("POV(pname2,<,40)", pov2.getSubProperty().getFilter().toString()); + } }