Merge "Improve test suite"
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 df1590f..96e12b1 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
@@ -44,14 +44,13 @@
             return parseQuerySegment((QuerySegment) queryNode);
         }
         else if (queryNode instanceof QueryGroup) {
-            return parseQueryNode(queryNode.getChild(0));
+            return parseQueryGroup((QueryGroup) queryNode);
         }
         else if (queryNode instanceof QuerySequence) {
             return parseSequenceQuery(queryNode.getChildren());
         }
         else if (queryNode instanceof QueryDisjunction) {
-            return parseGroupQuery(queryNode.getChildren(),
-                    KoralOperation.DISJUNCTION);
+            return parseQueryDisjunction(queryNode.getChildren());
         }
         else if (queryNode instanceof QueryWithWithin) {
             return parseWithinQuery((QueryWithWithin) queryNode);
@@ -69,23 +68,28 @@
     
     private KoralObject parseQuerySegment(QuerySegment segment)
             throws KoralException {
-        int minOccurs = segment.getMinOccurs();
-        int maxOccurs = segment.getMaxOccurs();
-
+        KoralObject object = expressionParser.parseExpression(segment.getExpression());
+        return handleQuantifier(object, segment.getMinOccurs(), segment.getMaxOccurs());
+    }
+    
+    private KoralObject parseQueryGroup(QueryGroup group) throws KoralException {
+        KoralObject object = parseQueryNode(group.getFirstChild());
+        return handleQuantifier(object, group.getMinOccurs(), group.getMaxOccurs());
+    }
+    
+    private KoralObject handleQuantifier(KoralObject object, int minOccurs, int maxOccurs){
         if ((minOccurs == 1) && (maxOccurs == 1)) {
-            return expressionParser.parseExpression(segment.getExpression());
+            return object;
         }
-        else {
-            KoralBoundary boundary = new KoralBoundary(minOccurs, maxOccurs);
-            List<KoralObject> operand = new ArrayList<KoralObject>(1);
-            operand.add(expressionParser.parseExpression(segment
-                    .getExpression()));
+        
+        KoralBoundary boundary = new KoralBoundary(minOccurs, maxOccurs);
+        List<KoralObject> operand = new ArrayList<KoralObject>(1);
+        operand.add(object);
 
-            KoralGroup koralGroup = new KoralGroup(KoralOperation.REPETITION);
-            koralGroup.setBoundary(boundary);
-            koralGroup.setOperands(operand);
-            return koralGroup;
-        }
+        KoralGroup koralGroup = new KoralGroup(KoralOperation.REPETITION);
+        koralGroup.setBoundary(boundary);
+        koralGroup.setOperands(operand);
+        return koralGroup;
     }
     
     private KoralObject parseWithinQuery(QueryWithWithin queryNode)
@@ -125,9 +129,8 @@
         return new KoralSpan(new KoralTerm(contextSpan));
     }
 
-    private KoralGroup parseGroupQuery(List<QueryNode> children,
-            KoralOperation operation) throws KoralException {
-        KoralGroup koralGroup = new KoralGroup(operation);
+    private KoralGroup parseQueryDisjunction(List<QueryNode> children) throws KoralException {
+        KoralGroup koralGroup = new KoralGroup(KoralOperation.DISJUNCTION);
         List<KoralObject> operands = new ArrayList<KoralObject>();
         for (QueryNode child : children) {
             operands.add(parseQueryNode(child));
diff --git a/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLComplexTest.java b/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLComplexTest.java
index 47a174e..093d715 100644
--- a/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLComplexTest.java
+++ b/src/test/java/de/ids_mannheim/korap/query/serialize/FCSQLComplexTest.java
@@ -44,7 +44,7 @@
         FCSQLQueryProcessorTest
                 .validateNode(query, "/query/operands/1", jsonLd);
 
-        // group and sequence
+        // a group contains a sequence
         query = "([text=\"blaue\"][pos=\"NN\"])";
         jsonLd = "{@type:koral:group,"
                 + "operation:operation:sequence,"
@@ -122,11 +122,12 @@
                 + "]}";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
 
+        // sequence of groups
     }
 
     // | simple-query quantifier /* quatification */
     @Test
-    public void testQueryWithQuantifier() throws IOException {
+    public void testSimpleQueryWithQuantifier() throws IOException {
         // repetition
         query = "\"die\"{2}";
         jsonLd = "{@type:koral:group,"
@@ -163,6 +164,30 @@
         query = "\"die\"{0}";
         jsonLd = "{@type:koral:boundary,min:0, max:0}";
         FCSQLQueryProcessorTest.validateNode(query, "/query/boundary", jsonLd);
+
+        // segment query with quantifier
+        query = "[cnx:pos=\"A\"]*";
+        jsonLd = "{@type:koral:group,"
+                + "operation:operation:repetition,"
+                + "operands:[{@type:koral:token,wrap:{@type:koral:term,key:A,foundry:cnx,layer:p,type:type:regex,match:match:eq}}],"
+                + "boundary:{@type:koral:boundary,min:0}}";
+        FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
+    }
+
+    @Test
+    public void testGroupQueryWithQuantifier() throws JsonProcessingException {
+        // group with quantifier
+        query = "(\"blaue\"|\"grüne\")*";
+        jsonLd = "{@type:koral:group,"
+                + "operation:operation:repetition,"
+                + "operands:["
+                + "{@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}}"
+                + "]}]," + "boundary:{@type:koral:boundary,min:0}}";
+        FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
     }
 
     // wildcards
@@ -185,7 +210,7 @@
 
         query = "\"Hund\"[]{2}";
         jsonLd = "{@type:koral:group," + "operation:operation:repetition,"
-                + "operands:[" + "{@type:koral:token}],"
+                + "operands:[{@type:koral:token}],"
                 + "boundary:{@type:koral:boundary,min:2,max:2}}";
         FCSQLQueryProcessorTest
                 .validateNode(query, "/query/operands/1", jsonLd);
@@ -206,24 +231,55 @@
     }
 
     @Test
-    public void testQueryWithDistance() throws IOException {
+    public void testDistanceQuery() throws IOException {
         // distance query
-        query = "\"Katze\" []{3} \"Hund\"";
+        query = "\"Katze\" [] \"Hund\"";
         jsonLd = "{@type:koral:group,operation:operation:sequence,inOrder:true,"
                 + "distances:["
-                + "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:3,max:3}}"
+                + "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:1,max:1}}"
                 + "],"
                 + "operands:["
                 + "{@type:koral:token,wrap:{@type:koral:term,key:Katze,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}},"
                 + "{@type:koral:token,wrap:{@type:koral:term,key:Hund,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}}]}";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
+    }
 
+    @Test
+    public void testDistanceQueryWithQuantifier() throws IOException {
+        query = "\"Katze\" []* \"Hund\"";
+        jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:0}}";
+        FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
+                jsonLd);
+
+        query = "\"Katze\" []+ \"Hund\"";
+        jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:1}}";
+        FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
+                jsonLd);
+
+        query = "\"Katze\" []{3} \"Hund\"";
+        jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:3,max:3}}";
+        FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
+                jsonLd);
+
+        query = "\"Katze\" []{2,3} \"Hund\"";
+        jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:2,max:3}}";
+        FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
+                jsonLd);
+    }
+
+    @Test
+    public void testDistanceQueryWithMultipleWildcards() throws IOException {
         // sequences of wildcards
         query = "\"Katze\" []{3}[] \"Hund\"";
         jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:4,max:4}}";
         FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
                 jsonLd);
 
+        query = "\"Katze\" []{3}[]? \"Hund\"";
+        jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:3,max:4}}";
+        FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
+                jsonLd);
+
         query = "\"Katze\" []{2}[]{3}[] \"Hund\"";
         jsonLd = "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:6,max:6}}";
         FCSQLQueryProcessorTest.validateNode(query, "/query/distances/0",
@@ -240,15 +296,15 @@
                 + "operands:["
                 + "{@type:koral:token,wrap:{@type:koral:term,key:Katze,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}},"
                 + "{@type:koral:group,"
-                    + "operation:operation:sequence,"
-                    + "inOrder:true,"
-                    + "distances:["
-                    + "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:1,max:2}}"
-                    + "],"
-                    + "operands:["
-                    + "{@type:koral:token,wrap:{@type:koral:term,key:Hund,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}},"
-                    + "{@type:koral:token,wrap:{@type:koral:term,key:V,foundry:cnx,layer:p,type:type:regex,match:match:eq}}]}" 
-                +"]}";
+                + "operation:operation:sequence,"
+                + "inOrder:true,"
+                + "distances:["
+                + "{@type:koral:distance,key:w,boundary:{@type:koral:boundary,min:1,max:2}}"
+                + "],"
+                + "operands:["
+                + "{@type:koral:token,wrap:{@type:koral:term,key:Hund,foundry:opennlp,layer:orth,type:type:regex,match:match:eq}},"
+                + "{@type:koral:token,wrap:{@type:koral:term,key:V,foundry:cnx,layer:p,type:type:regex,match:match:eq}}]}"
+                + "]}";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
     }
 
@@ -290,17 +346,43 @@
                 "FCS diagnostic 11: Within scope UTTERANCE is currently unsupported.",
                 (String) error.get(1));
     }
-    
+
     @Test
-    public void testWithinQueryWithEmptyTokens() throws IOException {        
+    public void testWithinQueryWithEmptyTokens() throws IOException {
         query = "[] within s";
         jsonLd = "{@type:koral:group,"
                 + "operation:operation:position,"
                 + "operands:["
                 + "{@type:koral:span,wrap:{@type:koral:term,key:s,foundry:base,layer:s}},"
-                + "{@type:koral:token}"
-                + "]}";
+                + "{@type:koral:token}" + "]}";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
+
+        query = "[]+ within s";
+        jsonLd = "{@type:koral:group," + "operation:operation:repetition,"
+                + "operands:[{@type:koral:token}],"
+                + "boundary:{@type:koral:boundary,min:1}}";
+        FCSQLQueryProcessorTest
+                .validateNode(query, "/query/operands/1", jsonLd);
+    }
+
+    @Test
+    public void testWithinQueryWithGroupQuery() throws IOException {
+        query = "(\"blaue\"|\"grüne\")+ within s";
+        jsonLd = "{@type:koral:span,wrap:{@type:koral:term,key:s,foundry:base,layer:s}}";
+        FCSQLQueryProcessorTest
+                .validateNode(query, "/query/operands/0", jsonLd);
+        jsonLd = "{@type:koral:group,"
+                + "operation:operation:repetition,"
+                + "operands:["
+                + "{@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}}"
+                + "]}]," + "boundary:{@type:koral:boundary,min:1}}";
+        FCSQLQueryProcessorTest
+                .validateNode(query, "/query/operands/1", jsonLd);
+
     }
 
     @Test
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 9c030de..666bff1 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
@@ -47,8 +47,7 @@
 
     @Test
     public void testVersion() throws JsonProcessingException {
-        error = getError(new FCSQLQueryProcessor("\"Sonne\"",
-                "1.0"));
+        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));
@@ -103,7 +102,7 @@
                 + "key:Fliegen, foundry:opennlp, layer:orth, type:type:regex, match:match:eq}}";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
 
-        query = "\"Fliegen\" /i";
+        query = "[text = \"Fliegen\" /i]";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
 
         query = "\"Fliegen\" /C";
@@ -114,8 +113,8 @@
         FCSQLQueryProcessorTest.validateNode(query, "/query/wrap", jsonLd);
 
         query = "\"Fliegen\" /l";
-        error = FCSQLQueryProcessorTest
-                .getError(new FCSQLQueryProcessor(query, "2.0"));
+        error = FCSQLQueryProcessorTest.getError(new FCSQLQueryProcessor(query,
+                "2.0"));
         assertEquals(306, error.get(0));
         String msg = (String) error.get(1);
         assertEquals(true, msg.startsWith("SRU diagnostic 48: Regexflags"));
@@ -139,7 +138,6 @@
         runAndValidate(query, jsonLd);
     }
 
-
     // attribute operator flagged-regexp
     // -------------------------------------------------------------------------
     // attribute ::= simple-attribute | qualified-attribute
@@ -178,7 +176,6 @@
         runAndValidate(query, jsonLd);
     }
 
-
     // segment-query ::= "[" expression? "]"
     // -------------------------------------------------------------------------
     // expression ::= basic-expression
@@ -238,6 +235,19 @@
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
     }
 
+    @Test
+    public void testMultipleBooleanExpressions() throws IOException {
+        query = "[mate:lemma=\"sein\" & (mate:pos=\"PPOSS\"|mate:pos=\"VAFIN\")]";
+        jsonLd = "{@type: koral:token,"
+                + " wrap: { @type: koral:termGroup,"
+                + "relation: relation:or,"
+                + " operands:["
+                + "{@type: koral:term, key: PPOSS, foundry: mate, layer: p, type:type:regex, match: match:eq},"
+                + "{@type: koral:term, key: VAFIN, foundry: mate, layer: p, type:type:regex, match: match:eq}]}}";
+        FCSQLQueryProcessorTest.validateNode(query, "/query/wrap/operands/1",
+                jsonLd);
+    }
+
     // "!" expression /* not */
     @Test
     public void testExpressionNot() throws IOException {
@@ -259,6 +269,9 @@
                 + "{@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}]}}";
         FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
+        
+        query = "[!(mate:lemma=\"sein\" & mate:pos=\"PPOSS\")]";
+        FCSQLQueryProcessorTest.runAndValidate(query, jsonLd);
     }
 
     @Test
@@ -274,10 +287,9 @@
         query = "[tt:morph = \"sein\"]";
         error = getError(new FCSQLQueryProcessor(query, "2.0"));
         assertEquals(306, error.get(0));
-        assertEquals(
-                "SRU diagnostic 48: Layer morph is unsupported.",
+        assertEquals("SRU diagnostic 48: Layer morph is unsupported.",
                 error.get(1));
-        
+
         // unsupported qualifier
         query = "[malt:lemma = \"sein\"]";
         error = getError(new FCSQLQueryProcessor(query, "2.0"));