Handled empty koral:span for relation queries.

Change-Id: I81d2e931cb16a5eb39c4caf300e66ab047410f2f
diff --git a/src/main/java/de/ids_mannheim/korap/KrillQuery.java b/src/main/java/de/ids_mannheim/korap/KrillQuery.java
index 3cfd7ee..5fb8c0e 100644
--- a/src/main/java/de/ids_mannheim/korap/KrillQuery.java
+++ b/src/main/java/de/ids_mannheim/korap/KrillQuery.java
@@ -213,8 +213,10 @@
         return this._fromKoral(json);
     };
 
-
     private SpanQueryWrapper _fromKoral (JsonNode json) throws QueryException {
+        return _fromKoral(json, false);
+    }
+    private SpanQueryWrapper _fromKoral (JsonNode json, boolean isOperationRelation) throws QueryException {
         int number = 0;
 
         // Only accept @typed objects for the moment
@@ -311,6 +313,11 @@
                 return this._segFromJson(json.get("wrap"));
 
             case "koral:span":
+                // EM: what to do with empty koral:span? 
+                // it is allowed only in relation queries
+                if (isOperationRelation && !json.has("key") && !json.has("wrap") && !json.has("attr")) {
+                    return new SpanRepetitionQueryWrapper();
+                }
                 if (!json.has("wrap"))
                     return this._termFromJson(json);
 
@@ -432,6 +439,7 @@
                 if (json.has("relType"))
                     return _operationRelationFromJson(operands,
                             json.get("relType"));
+                // EM: legacy
                 else if (json.has("relation")) {
                     return _operationRelationFromJson(operands,
                             json.get("relation"));
@@ -442,12 +450,13 @@
                 /*throw new QueryException(765,
                   "Relations are currently not supported");*/
 
-			// Gracefully warn on merge support
-			case "operation:merge":
-				this.addWarning(774, "Merge operation is currently not supported");
-				return _fromKoral(operands.get(0));
-				
-                // Deprecated in favor of operation:junction
+                // Gracefully warn on merge support
+            case "operation:merge":
+                this.addWarning(774,
+                        "Merge operation is currently not supported");
+                return _fromKoral(operands.get(0));
+
+            // Deprecated in favor of operation:junction
             case "operation:or":
                 return this._operationJunctionFromJson(operands);
             /*
@@ -553,8 +562,8 @@
                     "Number of operands is not acceptable");
         }
 
-        SpanQueryWrapper operand1 = this._fromKoral(operands.get(0));
-        SpanQueryWrapper operand2 = this._fromKoral(operands.get(1));
+        SpanQueryWrapper operand1 = this._fromKoral(operands.get(0), true);
+        SpanQueryWrapper operand2 = this._fromKoral(operands.get(1), true);
 
         String direction = ">:";
         if (operand1.isEmpty() && !operand2.isEmpty()) {
@@ -569,8 +578,9 @@
             if (!relation.has("wrap")) {
                 throw new QueryException(718, "Missing relation term");
             }
-            SpanQueryWrapper relationWrapper = _termFromJson(
-                    relation.get("wrap"), direction);
+            // fix me: termgroup relation
+            SpanQueryWrapper relationWrapper =
+                    _termFromJson(relation.get("wrap"), direction);
             return new SpanRelationWrapper(relationWrapper, operand1, operand2);
         }
         else {
@@ -974,10 +984,14 @@
         return sseqqw;
     };
 
-
-    // Deserialize koral:token
     private SpanQueryWrapper _segFromJson (JsonNode json)
             throws QueryException {
+        return _segFromJson(json, null);
+    }
+    
+    // Deserialize koral:token
+    private SpanQueryWrapper _segFromJson (JsonNode json, String direction)
+            throws QueryException {
 
         if (!json.has("@type"))
             throw new QueryException(701,
@@ -1009,7 +1023,7 @@
                 //                return this.seg().without(ssqw);
                 //
                 //            case "match:eq":
-                return this._termFromJson(json);
+                return this._termFromJson(json, direction);
             //            };
             //
             //            throw new QueryException(741, "Match relation unknown");
@@ -1075,22 +1089,26 @@
     private SpanQueryWrapper _termFromJson (JsonNode json, String direction)
             throws QueryException {
 
-        if (!json.has("key") || json.get("key").asText().length() < 1) {
-            if (!json.has("attr"))
-                throw new QueryException(740,
-                        "Key definition is missing in term or span");
-        };
-
         if (!json.has("@type")) {
             throw new QueryException(701,
                     "JSON-LD group has no @type attribute");
         };
-
-        Boolean isTerm = json.get("@type").asText().equals("koral:term") ? true
+        
+        String termType = json.get("@type").asText();
+        
+        Boolean isTerm = termType.equals("koral:term") ? true
                 : false;
         Boolean isCaseInsensitive = false;
 
-
+        if (!json.has("key") || json.get("key").asText().length() < 1) {
+            // why must it have an attr?
+            if (!json.has("attr")) {
+//                return new SpanRepetitionQueryWrapper();
+                throw new QueryException(740,
+                        "Key definition is missing in term or span");
+            }
+        };
+        
         // Ugly direction hack
         if (direction != null && direction.equals("<>:")) {
             isTerm = false;
diff --git a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanQueryWrapper.java b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanQueryWrapper.java
index c6e1355..a3fa932 100644
--- a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanQueryWrapper.java
+++ b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanQueryWrapper.java
@@ -1,6 +1,9 @@
 package de.ids_mannheim.korap.query.wrap;
 
 import org.apache.lucene.search.spans.SpanQuery;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.query.SpanFocusQuery;
 import de.ids_mannheim.korap.query.SpanClassQuery;
@@ -26,6 +29,8 @@
  */
 public class SpanQueryWrapper {
 
+    private static Logger log = LoggerFactory.getLogger(SpanQueryWrapper.class);
+    
     // Boundaries, e.g. for repetitions
     protected int min = 1, max = 1;
 
@@ -71,8 +76,10 @@
                     new SpanClassQuery(this.toFragmentQuery(), (byte) 254)),
                     (byte) 254);
         };
-
-        return this.toFragmentQuery();
+        
+        SpanQuery sq = this.toFragmentQuery();
+        log.info(sq.toString());
+        return sq;
     };
 
 
diff --git a/src/test/java/de/ids_mannheim/korap/TestSimple.java b/src/test/java/de/ids_mannheim/korap/TestSimple.java
index 7f4f8a1..d2681bc 100644
--- a/src/test/java/de/ids_mannheim/korap/TestSimple.java
+++ b/src/test/java/de/ids_mannheim/korap/TestSimple.java
@@ -20,6 +20,8 @@
 import org.apache.lucene.search.spans.Spans;
 import org.apache.lucene.search.spans.SpanQuery;
 import org.apache.lucene.util.Bits;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Helper class for testing the KrillIndex framework (Simple).
@@ -28,6 +30,8 @@
  */
 public class TestSimple {
 
+    private static Logger log  = LoggerFactory.getLogger(TestSimple.class);
+    
     // Add document
     public static void addDoc (IndexWriter w, Map<String, String> m)
             throws IOException {
@@ -92,17 +96,18 @@
 
 
     // Get query wrapper based on json file
-    public static SpanQueryWrapper getJSONQuery (String jsonFile) {
+    public static SpanQueryWrapper getJSONQuery (String jsonFile) throws QueryException {
         SpanQueryWrapper sqwi;
 
-        try {
+//        try {
             String json = getJsonString(jsonFile);
             sqwi = new KrillQuery("tokens").fromKoral(json);
-        }
-        catch (QueryException e) {
-            fail(e.getMessage());
-            sqwi = new QueryBuilder("tokens").seg("???");
-        };
+//        }
+//        catch (QueryException e) {
+//            //fail(e.getMessage());
+//            log.error(e.getMessage());
+//            sqwi = new QueryBuilder("tokens").seg("???");
+//        };        
         return sqwi;
     };
 
diff --git a/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java b/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java
index 3122b4d..701d06f 100644
--- a/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java
+++ b/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java
@@ -4,13 +4,19 @@
 import static org.junit.Assert.assertEquals;
 
 import org.apache.lucene.search.spans.SpanQuery;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import de.ids_mannheim.korap.query.wrap.SpanQueryWrapper;
 import de.ids_mannheim.korap.util.QueryException;
 
 public class TestSpanRelationQueryJSON {
 
+    @Rule
+    public ExpectedException exception = ExpectedException.none();
+
+
     @Test
     public void testMatchAnyRelationSourceWithAttribute ()
             throws QueryException {
@@ -221,4 +227,52 @@
                 "focus(2: focus(#[1,2]{1: source:{2: target:spanRelation(tokens:>:mate/d:HEAD)}}))",
                 sq.toString());
     }
+
+
+    // EM: should relation term allow empty key?
+    @Test
+    public void testTypedRelationWithoutKey () throws QueryException {
+
+        exception.expectMessage("Key definition is missing in term or span");
+
+        String filepath = getClass()
+                .getResource(
+                        "/queries/relation/typed-relation-without-key.json")
+                .getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals("tokens:???", sq.toString());
+    }
+
+    @Test
+    public void testTypedRelationWithKey () throws QueryException {
+        String filepath = getClass()
+                .getResource("/queries/relation/typed-relation-with-key.json")
+                .getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+
+        assertEquals("focus(#[1,2]spanRelation(tokens:>:malt/d:PP))",
+                sq.toString());
+    }
+
+
+    @Test
+    public void testTypedRelationWithAnnotationNodes () throws QueryException {
+        // query = "corenlp/c=\"VP\" & corenlp/c=\"NP\" & #1 ->malt/d[func=\"PP\"] #2";
+        String filepath = getClass()
+                .getResource(
+                        "/queries/relation/typed-relation-with-annotation-nodes.json")
+                .getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "focus(#[1,2]spanSegment(<tokens:corenlp/c:NP />, "
+                        + "focus(#2: spanSegment("
+                        + "spanRelation(tokens:>:malt/d:PP), <tokens:corenlp/c:VP />))))",
+                sq.toString());
+
+    }
+
+    // EM: handle empty koral:span
 }
diff --git a/src/test/java/de/ids_mannheim/korap/query/TestSpanSequenceQueryJSON.java b/src/test/java/de/ids_mannheim/korap/query/TestSpanSequenceQueryJSON.java
index 6501d77..1360e41 100644
--- a/src/test/java/de/ids_mannheim/korap/query/TestSpanSequenceQueryJSON.java
+++ b/src/test/java/de/ids_mannheim/korap/query/TestSpanSequenceQueryJSON.java
@@ -271,7 +271,7 @@
 
 
     @Test
-    public void queryJSONseqNegativelastConstraint () {
+    public void queryJSONseqNegativelastConstraint () throws QueryException {
         SpanQueryWrapper sqwi = jsonQueryFile(
                 "negative-last-constraint.jsonld");
         try {
@@ -383,7 +383,7 @@
 
 
     // get query wrapper based on json file
-    public SpanQueryWrapper jsonQueryFile (String filename) {
+    public SpanQueryWrapper jsonQueryFile (String filename) throws QueryException {
         return getJSONQuery(getClass().getResource(path + filename).getFile());
     };
 };
diff --git a/src/test/java/de/ids_mannheim/korap/query/TestSpanSubspanQueryJSON.java b/src/test/java/de/ids_mannheim/korap/query/TestSpanSubspanQueryJSON.java
index a1b386d..f4ea2ce 100644
--- a/src/test/java/de/ids_mannheim/korap/query/TestSpanSubspanQueryJSON.java
+++ b/src/test/java/de/ids_mannheim/korap/query/TestSpanSubspanQueryJSON.java
@@ -37,7 +37,7 @@
     }
 
 
-    @Test
+    @Test (expected=NullPointerException.class)
     public void testTermNull () throws QueryException {
         // subspan(tokens:tt/l:Haus, 1, 1)
         String filepath = getClass()
diff --git a/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java b/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java
index 10b80c7..fd4ac0a 100644
--- a/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java
+++ b/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java
@@ -4,12 +4,17 @@
 import static org.junit.Assert.assertEquals;
 
 import org.apache.lucene.search.spans.SpanQuery;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import de.ids_mannheim.korap.query.wrap.SpanQueryWrapper;
 import de.ids_mannheim.korap.util.QueryException;
 
 public class TestSpanWithAttributeJSON {
+    @Rule
+    public ExpectedException exception = ExpectedException.none();
+
 
     @Test
     public void testElementSingleAttribute () throws QueryException {
@@ -116,13 +121,17 @@
     }
 
 
-    @Test(expected = AssertionError.class)
+    @Test
     public void testAnyElementSingleNotAttribute () throws QueryException {
+
+        exception.expectMessage("The query requires a positive attribute.");
         String filepath = getClass()
                 .getResource(
                         "/queries/attribute/any-element-with-single-not-attribute.jsonld")
                 .getFile();
         SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        //        assertEquals("tokens:???", sq.toString());
     }
 
 }
diff --git a/src/test/resources/queries/relation/typed-relation-with-annotation-nodes.json b/src/test/resources/queries/relation/typed-relation-with-annotation-nodes.json
new file mode 100644
index 0000000..cc3ff80
--- /dev/null
+++ b/src/test/resources/queries/relation/typed-relation-with-annotation-nodes.json
@@ -0,0 +1,33 @@
+{
+    "@context": "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
+    "query": {
+        "operation": "operation:relation",
+        "operands": [
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "foundry": "corenlp",
+                "match": "match:eq",
+                "key": "VP"
+            },
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "foundry": "corenlp",
+                "match": "match:eq",
+                "key": "NP"
+            }
+        ],
+        "@type": "koral:group",
+        "relType": {
+            "wrap": {
+                "@type": "koral:term",
+                "layer": "d",
+                "foundry": "malt",
+                "match": "match:eq",
+                "key": "PP"
+            },
+            "@type": "koral:relation"
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/typed-relation-with-key.json b/src/test/resources/queries/relation/typed-relation-with-key.json
new file mode 100644
index 0000000..8417e66
--- /dev/null
+++ b/src/test/resources/queries/relation/typed-relation-with-key.json
@@ -0,0 +1,21 @@
+{
+    "@context": "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
+    "query": {
+        "operation": "operation:relation",
+        "operands": [
+            {"@type": "koral:span"},
+            {"@type": "koral:span"}
+        ],
+        "@type": "koral:group",
+        "relType": {
+            "wrap": {
+                "@type": "koral:term",
+                "layer": "d",
+                "foundry": "malt",
+                "match": "match:eq",
+                "key": "PP"
+            },
+            "@type": "koral:relation"
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/typed-relation-without-key.json b/src/test/resources/queries/relation/typed-relation-without-key.json
new file mode 100644
index 0000000..29ec80f
--- /dev/null
+++ b/src/test/resources/queries/relation/typed-relation-without-key.json
@@ -0,0 +1,19 @@
+{
+    "@context": "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
+    "query": {
+        "operation": "operation:relation",
+        "operands": [
+            {"@type": "koral:span"},
+            {"@type": "koral:span"}
+        ],
+        "@type": "koral:group",
+        "relType": {
+            "wrap": {
+                "@type": "koral:term",
+                "layer": "d",
+                "foundry": "malt"
+            },
+            "@type": "koral:relation"
+        }
+    }
+}
\ No newline at end of file