Implemented FCS Group queries and added tests.

Change-Id: Id98662146a13fa3ee2b2dc019e273c501454027c
diff --git a/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/ExpressionParser.java b/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/ExpressionParser.java
index 5d767a9..1dc4a4b 100644
--- a/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/ExpressionParser.java
+++ b/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/ExpressionParser.java
@@ -64,7 +64,8 @@
         //
         // }
         else if (queryNode instanceof ExpressionNot) {
-            return parseExpression(queryNode.getChild(0), true, true);
+            boolean negation = isNot ? false : true;
+            return parseExpression(queryNode.getChild(0), negation, isToken);
         }
         else if (queryNode instanceof ExpressionOr) {
             List<QueryNode> operands = queryNode.getChildren();
diff --git a/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/FCSSRUQueryParser.java b/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/FCSSRUQueryParser.java
index a98cc6e..ba440a3 100644
--- a/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/FCSSRUQueryParser.java
+++ b/src/main/java/de/ids_mannheim/korap/query/parse/fcsql/FCSSRUQueryParser.java
@@ -8,6 +8,7 @@
 import de.ids_mannheim.korap.query.serialize.FCSQLQueryProcessor;
 import de.ids_mannheim.korap.query.serialize.util.StatusCodes;
 import eu.clarin.sru.server.fcs.parser.QueryDisjunction;
+import eu.clarin.sru.server.fcs.parser.QueryGroup;
 import eu.clarin.sru.server.fcs.parser.QueryNode;
 import eu.clarin.sru.server.fcs.parser.QuerySegment;
 import eu.clarin.sru.server.fcs.parser.QuerySequence;
@@ -26,8 +27,9 @@
 
         if (queryNode instanceof QuerySegment) {
             return parseQuerySegment((QuerySegment) queryNode);
-            // } else if (queryNode instanceof QueryGroup) {
-            //
+        }
+        else if (queryNode instanceof QueryGroup) {
+            return parseQueryNode(queryNode.getChild(0));
         }
         else if (queryNode instanceof QuerySequence) {
             return parseGroupQuery(queryNode.getChildren(),
diff --git a/src/main/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessor.java b/src/main/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessor.java
index 3862664..1e973d8 100644
--- a/src/main/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessor.java
+++ b/src/main/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessor.java
@@ -56,8 +56,10 @@
     public void process(String query) {
         if (isVersionValid()) {
             FCSSRUQuery fcsSruQuery = parseQueryStringtoFCSQuery(query);
-            QueryNode fcsQueryNode = fcsSruQuery.getParsedQuery();
-            parseFCSQueryToKoralQuery(fcsQueryNode);
+            if (fcsSruQuery != null) {
+                QueryNode fcsQueryNode = fcsSruQuery.getParsedQuery();
+                parseFCSQueryToKoralQuery(fcsQueryNode);
+            }
         }
     }
 
@@ -83,13 +85,19 @@
         try {
             QueryNode parsedQuery = fcsParser.parse(query);
             fcsQuery = new FCSSRUQuery(query, parsedQuery);
+            if (fcsQuery == null) {
+                addError(StatusCodes.UNKNOWN_QUERY_ERROR,
+                        "FCS diagnostic 10: Unexpected error while parsing query.");
+            }
         }
         catch (QueryParserException e) {
-            addError(StatusCodes.UNKNOWN_QUERY_ERROR, "FCS diagnostic 10: +"
-                    + e.getMessage());
+            addError(
+                    StatusCodes.UNKNOWN_QUERY_ERROR,
+                    "FCS diagnostic 10: Query cannot be parsed, "
+                            + e.getMessage());
         }
         catch (Exception e) {
-            addError(StatusCodes.UNKNOWN_QUERY_ERROR, "FCS diagnostic 10: +"
+            addError(StatusCodes.UNKNOWN_QUERY_ERROR, "FCS diagnostic 10: "
                     + "Unexpected error while parsing query.");
         }
         return fcsQuery;
diff --git a/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessorTest.java b/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessorTest.java
index cea843f..b9026cb 100644
--- a/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessorTest.java
+++ b/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLQueryProcessorTest.java
@@ -25,12 +25,34 @@
         assertEquals(jsonLD.replace(" ", ""), serializedQuery.replace("\"", ""));
     }
 
+    private void validateNode(String query, String path, String jsonLd)
+            throws JsonProcessingException, IOException {
+        qs.setQuery(query, "fcsql", "2.0");
+        node = mapper.readTree(qs.toJSON());
+        String serializedQuery = mapper.writeValueAsString(node.at(path));
+        assertEquals(jsonLd.replace(" ", ""), serializedQuery.replace("\"", ""));
+    }
+
     private List<Object> getError(FCSQLQueryProcessor processor) {
         List<Object> errors = (List<Object>) processor.requestMap.get("errors");
         return (List<Object>) errors.get(0);
     }
 
     @Test
+    public void testVersion() throws JsonProcessingException {
+        List<Object> error = getError(new FCSQLQueryProcessor("\"Sonne\"",
+                "1.0"));
+        assertEquals(309, error.get(0));
+        assertEquals("SRU diagnostic 5: Only supports SRU version 2.0.",
+                error.get(1));
+
+        error = getError(new FCSQLQueryProcessor("\"Sonne\"", null));
+        assertEquals(309, error.get(0));
+        assertEquals("SRU diagnostic 7: Version number is missing.",
+                error.get(1));
+    }
+
+    @Test
     public void testTermQuery() throws JsonProcessingException {
         String query = "\"Sonne\"";
         String jsonLd = "{@type:koral:token, wrap:{@type:koral:term, key:Sonne, "
@@ -127,7 +149,7 @@
     }
 
     @Test
-    public void testNot() throws JsonProcessingException {
+    public void testNot() throws IOException {
         String query = "[cnx:pos != \"N\"]";
         String jsonLd = "{@type:koral:token, wrap:{@type:koral:term, key:N, "
                 + "foundry:cnx, layer:p, type:type:regex, match:match:ne}}";
@@ -137,15 +159,47 @@
                 + "foundry:tt, layer:p, type:type:regex, match:match:eq}}";
         query = "[!pos != \"NN\"]";
         runAndValidate(query, jsonLd);
+        query = "[!!pos = \"NN\"]";
+        runAndValidate(query, jsonLd);
+        query = "[!!!pos != \"NN\"]";
+        runAndValidate(query, jsonLd);
 
-        // Not possible
-        // query = "![pos != \"NN\"]";
+        query = "[mate:lemma=\"sein\" & !mate:pos=\"PPOSS\"]";
+        jsonLd = "{@type: koral:token,"
+                + " wrap: { "
+                + "@type: koral:termGroup,"
+                + "relation: relation:and,"
+                + " operands:["
+                + "{@type: koral:term, key: sein, foundry: mate, layer: l, type:type:regex, match: match:eq},"
+                + "{@type: koral:term, key: PPOSS, foundry: mate, layer: p, type:type:regex, match: match:ne}]}}";
+        runAndValidate(query, jsonLd);
     }
 
     @Test
-    public void testSequenceQuery() throws JsonProcessingException {
+    public void testWrongQuery() throws IOException {
+        String query = "!(mate:lemma=\"sein\" | mate:pos=\"PPOSS\")";
+        List<Object> error = getError(new FCSQLQueryProcessor(query, "2.0"));
+        assertEquals(399, error.get(0));
+        assertEquals(true,
+                error.get(1).toString().startsWith("FCS diagnostic 10"));
+
+        query = "![mate:lemma=\"sein\" | mate:pos=\"PPOSS\"]";
+        error = getError(new FCSQLQueryProcessor(query, "2.0"));
+        assertEquals(true,
+                error.get(1).toString().startsWith("FCS diagnostic 10"));
+
+        query = "(\"blaue\"&\"grüne\")";
+        error = getError(new FCSQLQueryProcessor(query, "2.0"));
+        assertEquals(true,
+                error.get(1).toString().startsWith("FCS diagnostic 10"));
+    }
+
+    @Test
+    public void testSequenceQuery() throws IOException {
         String query = "\"blaue|grüne\" [pos = \"NN\"]";
-        String jsonLd = "{@type:koral:group, operation:operation:sequence, operands:["
+        String jsonLd = "{@type:koral:group, "
+                + "operation:operation:sequence, "
+                + "operands:["
                 + "{@type:koral:token, wrap:{@type:koral:term, key:blaue|grüne, foundry:opennlp, layer:orth, type:type:regex, match:match:eq}},"
                 + "{@type:koral:token, wrap:{@type:koral:term, key:NN, foundry:tt, layer:p, type:type:regex, match:match:eq}}"
                 + "]}";
@@ -153,6 +207,11 @@
 
         query = "[text=\"blaue|grüne\"][pos = \"NN\"]";
         runAndValidate(query, jsonLd);
+
+        query = "\"blaue\" \"grüne\" [pos = \"NN\"]";
+        jsonLd = "{@type:koral:token, wrap:{@type:koral:term, key:grüne, foundry:opennlp, layer:orth, type:type:regex, match:match:eq}}";
+        validateNode(query, "/query/operands/1", jsonLd);
+
     }
 
     @Test
@@ -167,10 +226,13 @@
         runAndValidate(query, jsonLd);
 
         query = "[mate:lemma=\"sein\" | mate:pos=\"PPOSS\"]";
-        qs.setQuery(query, "fcsql", "2.0");
-        node = mapper.readTree(qs.toJSON());
-        assertEquals("relation:or", node.at("/query/wrap/relation").asText());
+        validateNode(query, "/query/wrap/relation", "relation:or");
 
+        query = "[cnx:lemma=\"sein\" | mate:lemma=\"sein\" | mate:pos=\"PPOSS\"]";
+        jsonLd = "{@type: koral:term, key: sein, foundry: cnx, layer: l, type:type:regex, match: match:eq}";
+        validateNode(query, "/query/wrap/operands/0", jsonLd);
+
+        // group with two tokens
         query = "[pos=\"NN\"]|[text=\"Mann\"]";
         jsonLd = "{@type:koral:group,"
                 + "operation:operation:disjunction,"
@@ -178,27 +240,52 @@
                 + "{@type:koral:token, wrap:{@type:koral:term,key:NN,foundry:tt,layer:p,type:type:regex,match:match:eq}},"
                 + "{@type:koral:token, wrap:{@type:koral:term,key:Mann,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}}]}";
         runAndValidate(query, jsonLd);
+
+        query = "[pos=\"NN\"]&[text=\"Mann\"]";
+        List<Object> error = getError(new FCSQLQueryProcessor(query, "2.0"));
+        assertEquals(399, error.get(0));
+        String msg = (String) error.get(1);
+        assertEquals(true, msg.startsWith("FCS diagnostic 10"));
     }
 
     @Test
-    public void testGroupQuery() throws JsonProcessingException {
-        // String query = "(\"blaue\"|\"grüne\")";
-        // runAndValidate(query, jsonLd);
+    public void testGroupQuery() throws IOException {
+        String query = "(\"blaue\"|\"grüne\")";
+        String jsonLd = "{@type:koral:group,"
+                + "operation:operation:disjunction,"
+                + "operands:["
+                + "{@type:koral:token, wrap:{@type:koral:term,key:blaue,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}},"
+                + "{@type:koral:token, wrap:{@type:koral:term,key:grüne,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}}]}";;
+        runAndValidate(query, jsonLd);
 
-    }
+        // group and disjunction
+        query = "([pos=\"NN\"]|[cnx:pos=\"N\"]|[text=\"Mann\"])";
+        jsonLd = "{@type:koral:token,wrap:{@type:koral:term,key:N,foundry:cnx,layer:p,type:type:regex,match:match:eq}}";
+        validateNode(query, "/query/operands/1", jsonLd);
 
-    @Test
-    public void testVersion() throws JsonProcessingException {
-        List<Object> error = getError(new FCSQLQueryProcessor("\"Sonne\"",
-                "1.0"));
-        assertEquals(309, error.get(0));
-        assertEquals("SRU diagnostic 5: Only supports SRU version 2.0.",
-                error.get(1));
+        // sequence and disjunction
+        query = "([pos=\"NN\"]|[cnx:pos=\"N\"])[text=\"Mann\"]";
+        jsonLd = "{@type:koral:group,"
+                + "operation:operation:sequence,"
+                + "operands:["
+                + "{@type:koral:group,"
+                + "operation:operation:disjunction,"
+                + "operands:[{@type:koral:token,wrap:{@type:koral:term,key:NN,foundry:tt,layer:p,type:type:regex,match:match:eq}},"
+                + "{@type:koral:token,wrap:{@type:koral:term,key:N,foundry:cnx,layer:p,type:type:regex,match:match:eq}}"
+                + "]},"
+                + "{@type:koral:token,wrap:{@type:koral:term,key:Mann,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}}"
+                + "]}";
+        runAndValidate(query, jsonLd);
 
-        error = getError(new FCSQLQueryProcessor("\"Sonne\"", null));
-        assertEquals(309, error.get(0));
-        assertEquals("SRU diagnostic 7: Version number is missing.",
-                error.get(1));
+        // group and sequence
+        query = "([text=\"blaue\"][pos=\"NN\"])";
+        jsonLd = "{@type:koral:group,"
+                + "operation:operation:sequence,"
+                + "operands:["
+                + "{@type:koral:token,wrap:{@type:koral:term,key:blaue,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}},"
+                + "{@type:koral:token,wrap:{@type:koral:term,key:NN,foundry:tt,layer:p,type:type:regex,match:match:eq}}"
+                + "]}";
+        runAndValidate(query, jsonLd);
     }
 
 }