Added span relation deserialization and its examples.

Change-Id: I4dbd07ecf767f3bef9feb13812fe649c891b001c
diff --git a/Errorcodes b/Errorcodes
index 524924a..a481a51 100644
--- a/Errorcodes
+++ b/Errorcodes
@@ -30,6 +30,8 @@
 714: "Span references expect a start position and a length parameter"
 715: "Attribute type is not supported"
 716: "Unknown relation"
+717: "Missing relation node"
+718: "Missing relation term"
 740: "Key definition is missing in term or span"
 741: "Match relation unknown"
 742: "Term group needs operand list"
diff --git a/src/main/java/de/ids_mannheim/korap/KrillQuery.java b/src/main/java/de/ids_mannheim/korap/KrillQuery.java
index b5390e3..45290e8 100644
--- a/src/main/java/de/ids_mannheim/korap/KrillQuery.java
+++ b/src/main/java/de/ids_mannheim/korap/KrillQuery.java
@@ -7,14 +7,25 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.lucene.util.automaton.RegExp;
-
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
-import de.ids_mannheim.korap.query.SpanWithinQuery;
 import de.ids_mannheim.korap.query.QueryBuilder;
-import de.ids_mannheim.korap.query.wrap.*;
+import de.ids_mannheim.korap.query.SpanWithinQuery;
+import de.ids_mannheim.korap.query.wrap.SpanAlterQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanAttributeQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanClassQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanFocusQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanRegexQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanRelationWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanRepetitionQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanSegmentQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanSequenceQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanSimpleQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanSubspanQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanWithAttributeQueryWrapper;
+import de.ids_mannheim.korap.query.wrap.SpanWithinQueryWrapper;
 import de.ids_mannheim.korap.response.Notifications;
 import de.ids_mannheim.korap.util.QueryException;
 
@@ -166,6 +177,9 @@
         return this.fromJson(jsonN);
     };
 
+    public SpanQueryWrapper fromJson(JsonNode json) throws QueryException {
+        return fromJson(json, null);
+    }
 
     /**
      * <p>Deserialize JSON-LD query as a {@link JsonNode} object
@@ -180,7 +194,8 @@
     // TODO: Use the shortcuts implemented in the builder
     //       instead of the wrapper constructors
     // TODO: Rename this span context!
-    public SpanQueryWrapper fromJson (JsonNode json) throws QueryException {
+    public SpanQueryWrapper fromJson(JsonNode json, String direction)
+            throws QueryException {
         int number = 0;
 
         // Only accept @typed objects for the moment
@@ -281,6 +296,12 @@
                 // Get wrapped token
                 return this._segFromJson(json.get("wrap"));
 
+            case "koral:relation":
+                if (!json.has("wrap")) {
+                    throw new QueryException(718, "Missing relation term");
+                }
+                return this._termFromJson(json.get("wrap"), direction);
+
             case "koral:span":
                 return this._termFromJson(json);
         };
@@ -386,8 +407,14 @@
                 return this._operationRepetitionFromJson(json, operands);
 
             case "operation:relation":
-                throw new QueryException(765,
-                        "Relations are currently not supported");
+                if (!json.has("relation")) {
+                    throw new QueryException(717, "Missing relation node");
+                }
+                
+                return _operationRelationFromJson(operands,
+                        json.get("relation"));
+                /*throw new QueryException(765,
+                        "Relations are currently not supported");*/
 
             case "operation:or": // Deprecated in favor of operation:junction
                 return this._operationJunctionFromJson(operands);
@@ -401,6 +428,32 @@
         throw new QueryException(711, "Unknown group operation");
     };
 
+    private SpanQueryWrapper _operationRelationFromJson(JsonNode operands,
+            JsonNode relation)
+            throws QueryException {
+
+        if (operands.size() < 2) {
+            throw new QueryException(705,
+                    "Number of operands is not acceptable");
+        }
+
+        String direction = null;
+        SpanQueryWrapper operand1 = fromJson(operands.get(0));
+        SpanQueryWrapper operand2 = fromJson(operands.get(1));
+
+        if (operand1.isEmpty()) {
+            direction = "<:";
+        }
+        else {// if (operand2.isEmpty()) {
+            direction = ">:";
+        }
+
+        SpanQueryWrapper relationWrapper = fromJson(relation, direction);
+
+        return new SpanRelationWrapper(relationWrapper,
+                operand1, operand2);
+
+    }
 
     // Deserialize operation:junction
     private SpanQueryWrapper _operationJunctionFromJson (JsonNode operands)
@@ -861,9 +914,13 @@
         throw new QueryException(745, "Token type is not supported");
     };
 
+    private SpanQueryWrapper _termFromJson (JsonNode json)
+            throws QueryException {
+        return _termFromJson(json, null);
+    }
 
     // Deserialize koral:term
-    private SpanQueryWrapper _termFromJson (JsonNode json)
+    private SpanQueryWrapper _termFromJson(JsonNode json, String direction)
             throws QueryException {
 
         if (!json.has("key") || json.get("key").asText().length() < 1) {
@@ -887,6 +944,10 @@
 
         StringBuilder value = new StringBuilder();
 
+        if (direction != null) {
+            value.append(direction);
+        }
+
         // expect orth? expect lemma? 
         // s:den | i:den | cnx/l:die | mate/m:mood:ind | cnx/syn:@PREMOD |
         // mate/m:number:sg | opennlp/p:ART
diff --git a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanAttributeQueryWrapper.java b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanAttributeQueryWrapper.java
index 90e9a2b..968d544 100644
--- a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanAttributeQueryWrapper.java
+++ b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanAttributeQueryWrapper.java
@@ -15,10 +15,12 @@
 
 
     public SpanAttributeQueryWrapper (SpanQueryWrapper sqw) {
-        if (sqw == null) {
-            isNull = true;
-            return;
+        if (sqw != null) {
+            isNull = false;
         }
+        else
+            return;
+
         if (sqw.isEmpty()) {
             isEmpty = true;
             return;
diff --git a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanRelationWrapper.java b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanRelationWrapper.java
new file mode 100644
index 0000000..9748ed3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanRelationWrapper.java
@@ -0,0 +1,86 @@
+package de.ids_mannheim.korap.query.wrap;
+
+import java.util.ArrayList;
+
+import org.apache.lucene.search.spans.SpanQuery;
+
+import de.ids_mannheim.korap.query.SpanFocusQuery;
+import de.ids_mannheim.korap.query.SpanSegmentQuery;
+import de.ids_mannheim.korap.util.QueryException;
+
+public class SpanRelationWrapper extends SpanQueryWrapper {
+
+    private SpanQueryWrapper relationQuery;
+    private SpanQueryWrapper subQuery1;
+    private SpanQueryWrapper subQuery2;
+
+    public SpanRelationWrapper (SpanQueryWrapper relationWrapper,
+            SpanQueryWrapper operand1, SpanQueryWrapper operand2) {
+
+        this.relationQuery = relationWrapper;
+        if (relationQuery != null) {
+            this.isNull = false;
+        }
+        else
+            return;
+
+        this.subQuery1 = operand1;
+        this.subQuery2 = operand2;
+    }
+
+    @Override
+    public SpanQuery toQuery() throws QueryException {
+
+        if (this.isNull()) {
+            return null;
+        }
+
+        SpanQuery sq = relationQuery.retrieveNode(this.retrieveNode).toQuery();
+        if (sq == null) return null;
+
+        ArrayList<Byte> classNumbers = new ArrayList<Byte>();
+        classNumbers.add((byte) 1);
+        classNumbers.add((byte) 2);
+
+        SpanQuery subq1, subq2;
+        if (subQuery1.isEmpty) {
+            if (!subQuery2.isEmpty) {
+                // match subquery2
+                subq2 = subQuery2.retrieveNode(this.retrieveNode).toQuery();
+                if (subq2 != null) {
+                    return new SpanFocusQuery(
+                            new SpanSegmentQuery(sq, subq2, true), classNumbers);
+                }
+            }
+        }
+        else if (subQuery2.isEmpty) {
+            if (!subQuery1.isEmpty) {
+                // match subquery1
+                subq1 = subQuery1.retrieveNode(this.retrieveNode).toQuery();
+                if (subq1 != null) {
+                    return new SpanFocusQuery(
+                            new SpanSegmentQuery(sq, subq1, true),
+                        classNumbers);
+                }
+            }               
+        }
+        else{
+            // match both
+            subq1 = subQuery1.retrieveNode(this.retrieveNode).toQuery();
+            if (subq1 != null) {
+                SpanFocusQuery fq = new SpanFocusQuery(new SpanSegmentQuery(sq, subq1,
+                        true), (byte) 2);
+                fq.setSorted(false);
+                sq = fq;
+            }
+            
+            subq2 = subQuery2.retrieveNode(this.retrieveNode).toQuery();
+            if (subq2 != null){
+                return new SpanFocusQuery(new SpanSegmentQuery(sq, subq2, true),
+                    classNumbers);
+            }
+        }
+        
+        return new SpanFocusQuery(sq, classNumbers);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanWithAttributeQueryWrapper.java b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanWithAttributeQueryWrapper.java
index ebbc219..b51557d 100644
--- a/src/main/java/de/ids_mannheim/korap/query/wrap/SpanWithAttributeQueryWrapper.java
+++ b/src/main/java/de/ids_mannheim/korap/query/wrap/SpanWithAttributeQueryWrapper.java
@@ -171,6 +171,8 @@
             SpanQueryWrapper attrQueryWrapper) throws QueryException {
         SpanQuery sq = attrQueryWrapper.toQuery();
         if (sq != null) {
+            if (sq instanceof SpanAttributeQuery)
+                return (SpanAttributeQuery) sq;
             if (sq instanceof SpanTermQuery) {
                 return new SpanAttributeQuery((SpanTermQuery) sq,
                         attrQueryWrapper.isNegative, true);
diff --git a/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java b/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java
new file mode 100644
index 0000000..9179301
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/query/TestSpanRelationQueryJSON.java
@@ -0,0 +1,85 @@
+package de.ids_mannheim.korap.query;
+
+import static de.ids_mannheim.korap.TestSimple.getJSONQuery;
+import static org.junit.Assert.assertEquals;
+
+import org.apache.lucene.search.spans.SpanQuery;
+import org.junit.Test;
+
+import de.ids_mannheim.korap.query.wrap.SpanQueryWrapper;
+import de.ids_mannheim.korap.util.QueryException;
+
+public class TestSpanRelationQueryJSON {
+
+    @Test
+    public void testMatchRelationSource() throws QueryException {
+        //
+        String filepath = getClass().getResource(
+                "/queries/relation/match-source.json").getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "focus([1,2]spanSegment(tokens:>:mate/d:HEAD, <tokens:c:s />))",
+                sq.toString());
+    }
+
+    @Test
+    public void testMatchRelationTarget() throws QueryException {
+        //
+        String filepath = getClass().getResource(
+                "/queries/relation/match-target.json").getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "focus([1,2]spanSegment(tokens:<:mate/d:HEAD, <tokens:c:vp />))",
+                sq.toString());
+    }
+
+    @Test
+    public void testMatchRelationSourceAndTarget() throws QueryException {
+        //
+        String filepath = getClass().getResource(
+                "/queries/relation/match-source-and-target.json").getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "focus([1,2]spanSegment(focus(2: spanSegment(tokens:>:mate/d:HEAD, <tokens:c:s />)), <tokens:c:vp />))",
+                sq.toString());
+    }
+
+    @Test
+    public void testMatchOperandWithProperty() throws QueryException {
+        //
+        String filepath = getClass().getResource(
+                "/queries/relation/operand-with-property.json").getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "focus([1,2]spanSegment(focus(2: spanSegment(tokens:>:mate/d:HEAD, "
+                        + "spanElementWithAttribute(<tokens:c:s />, spanAttribute(tokens:@root)))), <tokens:c:vp />))",
+                sq.toString());
+    }
+
+    @Test
+    public void testMatchOperandWithAttribute() throws QueryException {
+        //
+        String filepath = getClass().getResource(
+                "/queries/relation/operand-with-attribute.json").getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "focus([1,2]spanSegment(focus(2: spanSegment(tokens:>:mate/d:HEAD, "
+                        + "spanElementWithAttribute(<tokens:c:s />, spanAttribute(tokens:type:top)))), <tokens:c:vp />))",
+                sq.toString());
+    }
+
+    @Test
+    public void testMatchRelationOnly() throws QueryException {
+        //
+        String filepath = getClass().getResource(
+                "/queries/relation/relation-only.json").getFile();
+        SpanQueryWrapper sqwi = getJSONQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals("focus([1,2]tokens:<:mate/d:HEAD)", sq.toString());
+    }
+}
diff --git a/src/test/resources/queries/relation/match-source-and-target.json b/src/test/resources/queries/relation/match-source-and-target.json
new file mode 100644
index 0000000..860528f
--- /dev/null
+++ b/src/test/resources/queries/relation/match-source-and-target.json
@@ -0,0 +1,28 @@
+{
+    "query": {
+        "@type": "koral:group",
+        "operation": "operation:relation",
+        "operands": [
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "s"                                
+            },
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "vp"
+            }
+        ],
+        "relation": {
+            "@type": "koral:relation",
+            "wrap": {
+                "@type": "koral:term",
+                "foundry": "mate",
+                "layer": "d",
+                "key": "HEAD"
+            }
+        }
+    },
+    "meta": {}
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/match-source.json b/src/test/resources/queries/relation/match-source.json
new file mode 100644
index 0000000..3ca5686
--- /dev/null
+++ b/src/test/resources/queries/relation/match-source.json
@@ -0,0 +1,26 @@
+{
+    "query": {
+        "@type": "koral:group",
+        "operation": "operation:relation",
+        "operands": [
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "s"                                
+            },
+            {
+                "@type": "koral:token"             
+            }
+        ],
+        "relation": {
+            "@type": "koral:relation",
+            "wrap": {
+                "@type": "koral:term",
+                "foundry": "mate",
+                "layer": "d",
+                "key": "HEAD"
+            }
+        }
+    },
+    "meta": {}
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/match-target.json b/src/test/resources/queries/relation/match-target.json
new file mode 100644
index 0000000..7fe4ef0
--- /dev/null
+++ b/src/test/resources/queries/relation/match-target.json
@@ -0,0 +1,24 @@
+{
+    "query": {
+        "@type": "koral:group",
+        "operation": "operation:relation",
+        "operands": [
+        	{"@type": "koral:token"},
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "vp"                
+            }
+        ],
+        "relation": {
+            "@type": "koral:relation",
+            "wrap": {
+                "@type": "koral:term",
+                "foundry": "mate",
+                "layer": "d",
+                "key": "HEAD"
+            }
+        }
+    },
+    "meta": {}
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/operand-with-attribute.json b/src/test/resources/queries/relation/operand-with-attribute.json
new file mode 100644
index 0000000..570031c
--- /dev/null
+++ b/src/test/resources/queries/relation/operand-with-attribute.json
@@ -0,0 +1,35 @@
+{
+    "query": {
+        "@type": "koral:group",
+        "operation": "operation:relation",
+        "operands": [
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "s",
+                "match": "match:eq",
+                "attr": {
+		            "@type": "koral:term",
+		            "layer": "type",
+		            "key": "top",
+		            "match": "match:eq"
+		        }           
+            },
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "vp"
+            }
+        ],
+        "relation": {
+            "@type": "koral:relation",
+            "wrap": {
+                "@type": "koral:term",
+                "foundry": "mate",
+                "layer": "d",
+                "key": "HEAD"
+            }
+        }
+    },
+    "meta": {}
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/operand-with-property.json b/src/test/resources/queries/relation/operand-with-property.json
new file mode 100644
index 0000000..407c900
--- /dev/null
+++ b/src/test/resources/queries/relation/operand-with-property.json
@@ -0,0 +1,33 @@
+{
+    "query": {
+        "@type": "koral:group",
+        "operation": "operation:relation",
+        "operands": [
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "s",
+                "match": "match:eq",
+                "attr": {
+                	"@type": "koral:term",
+                	"root": true
+                }            
+            },
+            {
+                "@type": "koral:span",
+                "layer": "c",
+                "key": "vp"
+            }
+        ],
+        "relation": {
+            "@type": "koral:relation",
+            "wrap": {
+                "@type": "koral:term",
+                "foundry": "mate",
+                "layer": "d",
+                "key": "HEAD"
+            }
+        }
+    },
+    "meta": {}
+}
\ No newline at end of file
diff --git a/src/test/resources/queries/relation/relation-only.json b/src/test/resources/queries/relation/relation-only.json
new file mode 100644
index 0000000..8801184
--- /dev/null
+++ b/src/test/resources/queries/relation/relation-only.json
@@ -0,0 +1,20 @@
+{
+    "query": {
+        "@type": "koral:group",
+        "operation": "operation:relation",
+        "operands": [
+            {"@type": "koral:token"},
+            {"@type": "koral:token"}
+        ],
+        "relation": {
+            "@type": "koral:relation",
+            "wrap": {
+                "@type": "koral:term",
+                "foundry": "mate",
+                "layer": "d",
+                "key": "HEAD"
+            }
+        }
+    },
+    "meta": {}
+}
\ No newline at end of file