Updated the distance query with optionality.

Change-Id: I27a2722a1d985de4867013b2a5dd4e4b3652f4f5
diff --git a/src/main/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessor.java b/src/main/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessor.java
index 89fe13f..10268a1 100644
--- a/src/main/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessor.java
+++ b/src/main/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessor.java
@@ -264,28 +264,30 @@
             sequence.put("distances", distances);
 
             // Check optionality
-            ParseTree lastChildNode = node.getChild(node.getChildCount() - 1);
-            if (getNodeCat(lastChildNode).equals("segment")) {
-                ParseTree quantification = getFirstChildWithCat(lastChildNode,
-                        "repetition");
-                if (quantification != null) {
-                    minmax = parseRepetition(quantification);
-                    if (minmax[0] == 0 && minmax[1] == 1) {
-                        visited.add(quantification);
-                        processDisjunction();
-                        // create expansion
-                        isExpansion = true;
-                        LinkedHashMap<String, Object> expansionSequence = KoralObjectGenerator
-                                .makeGroup("sequence");
-                        putIntoSuperObject(expansionSequence);
-                        objectStack.push(expansionSequence);
-                        processNode(node.getChild(0));
-                        processNode(distanceNode.getChild(0));
-                        isExpansion = false;
-                        objectStack.pop();
-                    }
+            ParseTree operand1 = node.getChild(0);
+            ParseTree operand2 = node.getChild(node.getChildCount() - 1);
+            isExpansion = true;
+            if (checkOptionality(operand1)) {
+                processDisjunction();
+                // create expansion                
+                if (checkOptionality(operand2)) {
+                    createDisjunctionForDistanceWithOptionality(operand2, operand1,
+                            distanceNode, false);
+                    objectStack.pop();
+                    putIntoSuperObject(KoralObjectGenerator.makeToken());
                 }
+                createDisjunctionForDistanceWithOptionality(operand1, operand2,
+                        distanceNode, true);
+                objectStack.pop();
+                
             }
+            else if (checkOptionality(operand2)){
+                processDisjunction();
+                createDisjunctionForDistanceWithOptionality(operand2, operand1,
+                        distanceNode, false);
+                objectStack.pop();
+            }
+            isExpansion = false;
             // don't re-visit the emptyTokenSequence node
             visited.add(distanceNode.getChild(0));
         }
@@ -295,6 +297,37 @@
     }
 
 
+    private boolean checkOptionality (ParseTree node) {
+        ParseTree quantification = getFirstChildWithCat(node, "repetition");
+        if (quantification != null) {
+            Integer[] minmax = parseRepetition(quantification);
+            if (minmax[0] == 0 && minmax[1] == 1) {
+                visited.add(quantification);
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    private void createDisjunctionForDistanceWithOptionality (
+            ParseTree operand1, ParseTree operand2, ParseTree distanceNode,
+            boolean isCheckingFirstOperand) {
+            LinkedHashMap<String, Object> expansionSequence = KoralObjectGenerator
+                    .makeGroup("sequence");
+            putIntoSuperObject(expansionSequence);
+            objectStack.push(expansionSequence);
+            if (isCheckingFirstOperand) {
+                processNode(distanceNode.getChild(0));
+                processNode(operand2);
+            }
+            else {
+                processNode(operand2);
+                processNode(distanceNode.getChild(0));
+            }
+    }
+
+
     @SuppressWarnings("unchecked")
     /**
      * empty tokens at beginning/end of sequence
diff --git a/src/test/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessorTest.java b/src/test/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessorTest.java
index 57ea313..1f16a02 100644
--- a/src/test/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessorTest.java
+++ b/src/test/java/de/ids_mannheim/korap/query/serialize/PoliqarpPlusQueryProcessorTest.java
@@ -827,7 +827,7 @@
 
 
     @Test
-    public void testDistanceWithEmptyTokenAndOptionality ()
+    public void testDistanceWithOptionality ()
             throws JsonProcessingException, IOException {
         query = "[base=der][][base=Mann]?";
         qs.setQuery(query, "poliqarpplus");
@@ -841,6 +841,7 @@
         assertEquals("operation:sequence", res.at(disjunctionOperand1+"operation").asText());
         assertEquals("der", res.at(disjunctionOperand1+"operands/0/wrap/key").asText());
         assertEquals("koral:token", res.at(disjunctionOperand1+"operands/1/@type").asText());
+        assertEquals(true, res.at(disjunctionOperand1+"operands/1/wrap/key").isMissingNode());
         
         String disjunctionOperand2 = "/query/operands/1/";
         assertEquals("operation:sequence", res.at(disjunctionOperand2+"operation").asText());
@@ -852,7 +853,7 @@
     }
 
     @Test
-    public void testDistanceWithEmptyTokenAndOptionality2 ()
+    public void testDistanceWithOptionalityAndMultipleSequences ()
             throws JsonProcessingException, IOException {
         query = "[base=der][base=bose][][base=Mann]?";
         qs.setQuery(query, "poliqarpplus");
@@ -872,6 +873,7 @@
         assertEquals("operation:sequence", res.at(disjunctionOperand1+"operation").asText());
         assertEquals("bose", res.at(disjunctionOperand1+"operands/0/wrap/key").asText());
         assertEquals("koral:token", res.at(disjunctionOperand1+"operands/1/@type").asText());
+        assertEquals(true, res.at(disjunctionOperand1+"operands/1/wrap/key").isMissingNode());
         
         String disjunctionOperand2 = "/query/operands/1/operands/1/";
         assertEquals("operation:sequence", res.at(disjunctionOperand2+"operation").asText());
@@ -880,7 +882,97 @@
         assertEquals("1", res.at(disjunctionOperand2+"distances/0/boundary/min").asText());
         assertEquals("1", res.at(disjunctionOperand2+"distances/0/boundary/max").asText());
     }
+    
+    @Test
+    public void testDistanceWithOptionalityAndMultipleEmptyToken ()
+            throws JsonProcessingException, IOException {
+        query = "[base=der][][][base=Mann]?";
+        qs.setQuery(query, "poliqarpplus");
+        String koralQuery = qs.toJSON();
+        res = mapper.readTree(koralQuery);
 
+        assertEquals("koral:group", res.at("/query/@type").asText());
+        assertEquals("operation:disjunction", res.at("/query/operation").asText());
+        
+        String disjunctionOperand1 = "/query/operands/0/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand1+"operation").asText());
+        assertEquals("der", res.at(disjunctionOperand1+"operands/0/wrap/key").asText());
+        
+        String emptyTokenOperand = disjunctionOperand1+"operands/1/";
+        assertEquals("koral:group", res.at(emptyTokenOperand+"@type").asText());
+        assertEquals("koral:token", res.at(emptyTokenOperand+"operands/0/@type").asText());
+        assertEquals(true, res.at(emptyTokenOperand+"operands/0/wrap/key").isMissingNode());
+        assertEquals("2", res.at(emptyTokenOperand+"boundary/min").asText());
+        assertEquals("2", res.at(emptyTokenOperand+"boundary/max").asText());
+        
+        String disjunctionOperand2 = "/query/operands/1/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand2+"operation").asText());
+        assertEquals("der", res.at(disjunctionOperand2+"operands/0/wrap/key").asText());
+        assertEquals("Mann", res.at(disjunctionOperand2+"operands/1/wrap/key").asText());
+        assertEquals("2", res.at(disjunctionOperand2+"distances/0/boundary/min").asText());
+        assertEquals("2", res.at(disjunctionOperand2+"distances/0/boundary/max").asText());
+        
+    }
+    
+    @Test
+    public void testDistanceWithOptionalityOnTheLeft ()
+            throws JsonProcessingException, IOException {
+        query = "[base=der][base=bose]?[][base=Mann]";
+        qs.setQuery(query, "poliqarpplus");
+        String koralQuery = qs.toJSON();
+        res = mapper.readTree(koralQuery);
+        
+        assertEquals("koral:group", res.at("/query/operands/1/@type").asText());
+        assertEquals("operation:disjunction", res.at("/query/operands/1/operation").asText());
+        
+        String disjunctionOperand1 = "/query/operands/1/operands/0/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand1+"operation").asText());
+        assertEquals("koral:token", res.at(disjunctionOperand1+"operands/0/@type").asText());
+        assertEquals(true, res.at(disjunctionOperand1+"operands/0/key").isMissingNode());
+        assertEquals("Mann", res.at(disjunctionOperand1+"operands/1/wrap/key").asText());
+                
+        String disjunctionOperand2 = "/query/operands/1/operands/1/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand2+"operation").asText());
+        assertEquals("bose", res.at(disjunctionOperand2+"operands/0/wrap/key").asText());
+        assertEquals("Mann", res.at(disjunctionOperand2+"operands/1/wrap/key").asText());
+        assertEquals("1", res.at(disjunctionOperand2+"distances/0/boundary/min").asText());
+        assertEquals("1", res.at(disjunctionOperand2+"distances/0/boundary/max").asText());
+    }
+    @Test
+    public void testDistanceWithOptionalityOnBothOperands()
+            throws JsonProcessingException, IOException {
+        query = "[base=der]?[][base=Mann]?";
+        qs.setQuery(query, "poliqarpplus");
+        String koralQuery = qs.toJSON();
+        res = mapper.readTree(koralQuery);
+        
+        assertEquals("koral:group", res.at("/query/@type").asText());
+        assertEquals("operation:disjunction", res.at("/query/operation").asText());
+        
+        String disjunctionOperand1 = "/query/operands/0/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand1+"operation").asText());
+        assertEquals("der", res.at(disjunctionOperand1+"operands/0/wrap/key").asText());
+        assertEquals("koral:token", res.at(disjunctionOperand1+"operands/1/@type").asText());
+        assertEquals(true, res.at(disjunctionOperand1+"operands/1/key").isMissingNode());
+                        
+        String disjunctionOperand2 = "/query/operands/1/";
+        assertEquals("koral:token", res.at(disjunctionOperand2+"@type").asText());
+        assertEquals(true, res.at(disjunctionOperand2+"key").isMissingNode());
+        
+        String disjunctionOperand3 = "/query/operands/2/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand3+"operation").asText());
+        assertEquals("koral:token", res.at(disjunctionOperand3+"operands/0/@type").asText());
+        assertEquals(true, res.at(disjunctionOperand3+"operands/0/key").isMissingNode());
+        assertEquals("Mann", res.at(disjunctionOperand3+"operands/1/wrap/key").asText());
+                        
+        String disjunctionOperand4 = "/query/operands/3/";
+        assertEquals("operation:sequence", res.at(disjunctionOperand4+"operation").asText());
+        assertEquals("der", res.at(disjunctionOperand4+"operands/0/wrap/key").asText());
+        assertEquals("Mann", res.at(disjunctionOperand4+"operands/1/wrap/key").asText());
+        assertEquals("1", res.at(disjunctionOperand4+"distances/0/boundary/min").asText());
+        assertEquals("1", res.at(disjunctionOperand4+"distances/0/boundary/max").asText());
+    }
+    
     @Test
     public void testRepetition () throws JsonProcessingException, IOException {
         query = "der{3}";