Major refactoring to support coherent notifications - some known issues in JSON responses included
diff --git a/src/main/java/de/ids_mannheim/korap/KorapCollection.java b/src/main/java/de/ids_mannheim/korap/KorapCollection.java
index dcb1461..eca212f 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapCollection.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapCollection.java
@@ -8,6 +8,7 @@
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.filter.BooleanFilter;
 import de.ids_mannheim.korap.filter.FilterOperation;
+import de.ids_mannheim.korap.response.Notifications;
 
 import org.apache.lucene.search.spans.SpanQuery;
 import org.apache.lucene.search.*;
@@ -24,19 +25,29 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * That's a pretty ugly API to
+ * create virtual collections.
+ * It works - so I got that going for
+ * me, which is nice.
+ *
+ * @author Nils Diewald
+ */
+
+
 // TODO: Make a cache for the bits!!! DELETE IT IN CASE OF AN EXTENSION OR A FILTER!
-// Todo: Maybe use randomaccessfilterstrategy
+// TODO: Maybe use randomaccessfilterstrategy
 // TODO: Maybe a constantScoreQuery can make things faster?
 
 // THIS MAY CHANGE for stuff like combining virtual collections
 // See http://mail-archives.apache.org/mod_mbox/lucene-java-user/
 //     200805.mbox/%3C17080852.post@talk.nabble.com%3E
 
-public class KorapCollection {
+public class KorapCollection extends Notifications {
     private KorapIndex index;
     private KorapDate created;
     private String id;
-    private String error;
+    //    private String error;
     private ArrayList<FilterOperation> filter;
     private int filterCount = 0;
     
@@ -61,7 +72,7 @@
 	this.filter = new ArrayList<FilterOperation>(5);
 
 	try {
-	    JsonNode json = mapper.readValue(jsonString, JsonNode.class);
+	    JsonNode json = mapper.readTree(jsonString);
 
 	    if (json.has("collection")) {
 		this.fromJSON(json.get("collection"));
@@ -70,16 +81,25 @@
 	    // Legacy collection serialization
 	    // This will be removed!
 	    else if (json.has("collections")) {
-		if (DEBUG)
-		    log.warn("Using DEPRECATED collection!");
-
+		this.addMessage(
+		    850,
+		    "Collections are deprecated in favour of a single collection"
+                );
 		for (JsonNode collection : json.get("collections")) {
 		    this.fromJSONLegacy(collection);
 		};
 	    };
 	}
-	catch (Exception e) {
-	    this.error = e.getMessage();
+	catch (QueryException qe) {
+	    this.addError(qe.getErrorCode(),qe.getMessage());
+	}
+	catch (IOException e) {
+	    this.addError(
+	        621,
+		"Unable to parse JSON",
+		"KorapCollection",
+		e.getLocalizedMessage()
+	    );
 	};
     };
 
@@ -92,10 +112,10 @@
     public void fromJSON (String jsonString) throws QueryException {
 	ObjectMapper mapper = new ObjectMapper();
 	try {
-	    this.fromJSON((JsonNode) mapper.readValue(jsonString, JsonNode.class));
+	    this.fromJSON((JsonNode) mapper.readTree(jsonString));
 	}
 	catch (Exception e) {
-	    this.error = e.getMessage();
+	    this.addError(621, "Unable to parse JSON", "KorapCollection");
 	};
     };
 
@@ -114,7 +134,7 @@
 	    this.fromJSONLegacy((JsonNode) mapper.readValue(jsonString, JsonNode.class));
 	}
 	catch (Exception e) {
-	    this.error = e.getMessage();
+	    this.addError(621, "Unable to parse JSON", "KorapCollection");
 	};
     };
 
@@ -124,10 +144,10 @@
      */
     public void fromJSONLegacy (JsonNode json) throws QueryException {
 	if (!json.has("@type"))
-	    throw new QueryException(612, "JSON-LD group has no @type attribute");
+	    throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 	if (!json.has("@value"))
-	    throw new QueryException(612, "Legacy filter need @value fields");
+	    throw new QueryException(851, "Legacy filter need @value fields");
 
 	String type = json.get("@type").asText();
 
@@ -162,18 +182,18 @@
 	    log.trace("Added filter: {}", filter.toString());
 
 	if (filter == null) {
-	    log.warn("No filter is given");
+	    this.addWarning(830, "Filter was empty");
 	    return this;
 	};
 
 	Filter f = (Filter) new QueryWrapperFilter(filter.toQuery());
 	if (f == null) {
-	    log.warn("Filter can't be wrapped");
+	    this.addWarning(831, "Filter is not wrappable");
 	    return this;
 	};
 	FilterOperation fo = new FilterOperation(f, false);
 	if (fo == null) {
-	    log.warn("Filter operation invalid");
+	    this.addWarning(832, "Filter operation is invalid");
 	    return this;
 	};
 	this.filter.add(fo);
@@ -329,14 +349,14 @@
 
     public long numberOf (String foundry, String type) throws IOException {
 	if (this.index == null)
-	    return (long) 0;
+	    return (long) -1;
 
 	return this.index.numberOf(this, foundry, type);
     };
 
     public long numberOf (String type) throws IOException {
 	if (this.index == null)
-	    return (long) 0;
+	    return (long) -1;
 
 	return this.index.numberOf(this, "tokens", type);
     };
@@ -410,8 +430,4 @@
 	sw.append("}");
 	return sw.getBuffer().toString();
     };
-    
-    public String getError () {
-	return this.error;
-    };
 };
diff --git a/src/main/java/de/ids_mannheim/korap/KorapDocument.java b/src/main/java/de/ids_mannheim/korap/KorapDocument.java
index e3dc9a0..2602b46 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapDocument.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapDocument.java
@@ -5,6 +5,7 @@
 import de.ids_mannheim.korap.util.KorapDate;
 import de.ids_mannheim.korap.document.KorapPrimaryData;
 import de.ids_mannheim.korap.index.FieldDocument;
+import de.ids_mannheim.korap.response.KorapResponse;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.annotation.*;
@@ -14,10 +15,10 @@
 /**
  * Abstract class representing a document in the KorAP index.
  *
- * @author ndiewald
+ * @author Nils Diewald
  */
 @JsonIgnoreProperties(ignoreUnknown = true)
-public abstract class KorapDocument {
+public abstract class KorapDocument extends KorapResponse {
     private KorapPrimaryData primaryData;
 
     @JsonIgnore
diff --git a/src/main/java/de/ids_mannheim/korap/KorapFilter.java b/src/main/java/de/ids_mannheim/korap/KorapFilter.java
index 9c271d9..32573f9 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapFilter.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapFilter.java
@@ -46,11 +46,9 @@
     protected BooleanFilter fromJSON (JsonNode json, String field) throws QueryException {
 	BooleanFilter bfilter = new BooleanFilter();
 
-	/*
-	  TODO: THIS UNFORTUNATELY BREAKS TESTS
+	// TODO: THIS UNFORTUNATELY BREAKS TESTS
 	if (!json.has("@type"))
-	    throw new QueryException(612, "JSON-LD group has no @type attribute");
-	*/
+	    throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 	String type = json.get("@type").asText();
 
diff --git a/src/main/java/de/ids_mannheim/korap/KorapIndex.java b/src/main/java/de/ids_mannheim/korap/KorapIndex.java
index 518bd36..a51eabd 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapIndex.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapIndex.java
@@ -99,7 +99,7 @@
     private int commitCounter = 0;
     private HashMap termContexts;
     private ObjectMapper mapper = new ObjectMapper();
-    private String version;
+    private String version, name;
 
     private byte[] pl = new byte[4];
     private static ByteBuffer bb       = ByteBuffer.allocate(4),
@@ -120,7 +120,8 @@
 	    try {
 		InputStream fr = new FileInputStream(f);
 		prop.load(fr);
-		this.version = prop.getProperty("lucene.index.version");
+		this.version = prop.getProperty("lucene.version");
+		this.name = prop.getProperty("lucene.name");
 	    }
 	    catch (FileNotFoundException e) {
 		log.warn(e.getLocalizedMessage());
@@ -165,6 +166,11 @@
 	return this.version;
     };
 
+    // Get system name
+    public String getName () {
+	return this.name;
+    };
+
 
     // Close connection to index
     public void close () throws IOException {
@@ -623,6 +629,9 @@
 	if (this.getVersion() != null)
 	    match.setVersion(this.getVersion());
 
+	if (this.getName() != null)
+	    match.setName(this.getName());
+
 	if (match.getStartPos() == -1)
 	    return match;
 
@@ -1255,7 +1264,11 @@
 	    kr.setTotalResults(cutoff ? -1 : i);
 	}
 	catch (IOException e) {
-	    kr.setError(600, e.getLocalizedMessage());
+	    kr.addError(
+	        600,
+		"Unable to read index",
+		e.getLocalizedMessage()
+	    );
 	    log.warn( e.getLocalizedMessage() );
 	};
 
@@ -1371,7 +1384,11 @@
 	    mc.setBenchmark(t1, System.nanoTime());
 	}
 	catch (IOException e) {
-	    mc.setError(600, e.getLocalizedMessage());
+	    mc.addError(
+	        600,
+	        "Unable to read index",
+	        e.getLocalizedMessage()
+            );
 	    log.warn(e.getLocalizedMessage());
 	};
 
diff --git a/src/main/java/de/ids_mannheim/korap/KorapMatch.java b/src/main/java/de/ids_mannheim/korap/KorapMatch.java
index b8afd1d..98811db 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapMatch.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapMatch.java
@@ -415,31 +415,6 @@
 	super.setID(id);
     };
 
-
-    /**
-     * Set version of the index
-     */
-    @JsonIgnore
-    public String getVersion () {
-	if (this.version == null)
-	    return null;
-	StringBuilder sb = new StringBuilder("lucene-backend-");
-	return sb.append(this.version).toString();
-    };
-
-
-    /**
-     * Set version number.
-     *
-     * @param version The version number of the index as
-     *                a string representation.
-     */
-    @JsonIgnore
-    public void setVersion (String version) {
-	this.version = version;
-    };
-
-
     /**
      * Get the positional start offset of the match.
      */
diff --git a/src/main/java/de/ids_mannheim/korap/KorapQuery.java b/src/main/java/de/ids_mannheim/korap/KorapQuery.java
index 70267c1..d7962e8 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapQuery.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapQuery.java
@@ -1,6 +1,7 @@
 package de.ids_mannheim.korap;
 
 import de.ids_mannheim.korap.query.wrap.*;
+import de.ids_mannheim.korap.response.Notifications;
 import de.ids_mannheim.korap.util.QueryException;
 
 import org.apache.lucene.search.spans.SpanQuery;
@@ -38,7 +39,7 @@
  * KorapQuery implements a simple API for wrapping
  * KorAP Lucene Index specific query classes.
  */
-public class KorapQuery {
+public class KorapQuery extends Notifications {
     private String field;
     private ObjectMapper json;
 
@@ -48,8 +49,6 @@
     // This advices the java compiler to ignore all loggings
     public static final boolean DEBUG = false;
 
-    private String warning;
-
     public static final byte
 	OVERLAP      = SpanWithinQuery.OVERLAP,
 	REAL_OVERLAP = SpanWithinQuery.REAL_OVERLAP,
@@ -79,10 +78,10 @@
 	public Boundary (JsonNode json, int defaultMin, int defaultMax) throws QueryException {
 
 	    if (!json.has("@type"))
-		throw new QueryException(612, "JSON-LD group has no @type attribute");
+		throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 	    if (!json.get("@type").asText().equals("korap:boundary"))
-		throw new QueryException(612, "Boundary definition is not valid");
+		throw new QueryException(702, "Boundary definition is invalid");
 
 	    // Set min boundary
 	    if (json.has("min"))
@@ -108,7 +107,8 @@
 	}
 	catch (IOException e) {
 	    String msg = e.getMessage();
-	    throw new QueryException(611, msg.split("\n")[0]);
+	    log.warn("Unable to parse JSON: " + msg.split("\n")[0]);
+	    throw new QueryException(621, "Unable to parse JSON");
 	};
 
 	if (!json.has("@type") && json.has("query"))
@@ -127,7 +127,7 @@
 	int number = 0;
 
 	if (!json.has("@type"))
-	    throw new QueryException(612, "JSON-LD group has no @type attribute");
+	    throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 	String type = json.get("@type").asText();
 
@@ -137,7 +137,7 @@
 	    SpanClassQueryWrapper classWrapper;
 
 	    if (!json.has("operation"))
-		throw new QueryException(612, "Group expects operation");
+		throw new QueryException(703, "Group expects operation");
 
 	    String operation = json.get("operation").asText();
 
@@ -145,13 +145,13 @@
 		log.trace("Found {} group", operation);
 
 	    if (!json.has("operands"))
-		throw new QueryException(612, "Operation needs operand list");
+		throw new QueryException(704, "Operation needs operand list");
 
 	    // Get all operands
 	    JsonNode operands = json.get("operands");
 
 	    if (!operands.isArray())
-		throw new QueryException(612, "Operation needs operand list");
+		throw new QueryException(704, "Operation needs operand list");
 
 	    if (DEBUG)
 		log.trace("Operands are {}", operands);
@@ -159,7 +159,6 @@
 	    switch (operation) {
 
 	    case "operation:or":
-
 		SpanAlterQueryWrapper ssaq = new SpanAlterQueryWrapper(this.field);
 		for (JsonNode operand : operands) {
 		    ssaq.or(this.fromJSON(operand));
@@ -169,7 +168,7 @@
 	    case "operation:position":
 
 		if (operands.size() != 2)
-		    throw new QueryException(612, "Position needs exactly two operands");
+		    throw new QueryException(705, "Number of operands is not acceptable");
 
 		// TODO: Check for operands
 		// TODO: LEGACY and not future proof
@@ -215,14 +214,14 @@
 			};
 		    };
 		default:
-		    throw new QueryException(613, "Frame type unknown");
+		    throw new QueryException(706, "Frame type is unknown");
 		};
 
 		// Check for exclusion modificator
 		Boolean exclude;
 		if (json.has("exclude") && json.get("exclude").asBoolean())
 		    throw new QueryException(
-			613,
+			760,
 		        "Exclusion is currently not supported in position operations"
 		    );
 
@@ -236,16 +235,22 @@
 	    case "operation:submatch":
 
 		if (operands.size() != 1)
-		    throw new QueryException(612, "Operation needs exactly two operands");
+		    throw new QueryException(705, "Number of operands is not acceptable");
 
 		if (json.has("classRef")) {
 		    if (json.has("classRefOp"))
-			throw new QueryException(613, "Class reference operators not supported yet");
+			throw new QueryException(
+			    761,
+			    "Class reference operators are currently not supported"
+                    );
 
 		    number = json.get("classRef").get(0).asInt();
 		}
 		else if (json.has("spanRef")) {
-		    throw new QueryException(613, "Span references not supported yet");
+		    throw new QueryException(
+			762,
+		        "Span references are currently not supported"
+                    );
 		};
 
 		return new SpanMatchModifyQueryWrapper(
@@ -254,10 +259,9 @@
 
 	    case "operation:sequence":
 
-		if (operands.size() == 1) {
-		    this.addWarning("Sequences with less than two operands are ignored");
+		// Sequence with only one operand
+		if (operands.size() == 1)
 		    return this.fromJSON(operands.get(0));
-		};
 
 		SpanSequenceQueryWrapper sseqqw = this.seq();
 
@@ -272,12 +276,13 @@
 		    // TODO
 		    if (json.has("exclude") && json.get("exclude").asBoolean())
 			throw new QueryException(
-			    613, "Excluding distance constraints are not supported yet"
+			    763,
+			    "Excluding distance constraints are currently not supported"
 			);
 
 		    if (!json.get("distances").isArray()) {
 			throw new QueryException(
-			    612,
+			    707,
 			    "Distance Constraints have " +
 			    "to be defined as arrays"
 			);
@@ -287,13 +292,13 @@
 		    JsonNode firstDistance = json.get("distances").get(0);
 
 		    if (!firstDistance.has("@type"))
-			throw new QueryException(612, "JSON-LD group has no @type attribute");
+			throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 		    JsonNode distances;
 		    if (firstDistance.get("@type").asText().equals("korap:group")) {
 			if (!firstDistance.has("operands") ||
 			    !firstDistance.get("operands").isArray())
-			    throw new QueryException(612, "Groups need operands");
+			    throw new QueryException(704, "Operation needs operand list");
 
 			distances = firstDistance.get("operands");
 		    }
@@ -309,7 +314,7 @@
 		    }
 
 		    else
-			throw new QueryException(612, "No valid distances defined");
+			throw new QueryException(708, "No valid distances defined");
 
 		    // Add all distance constraint to query
 		    for (JsonNode constraint : distances) {
@@ -350,7 +355,8 @@
 			    max = min;
 
 			if (DEBUG)
-			    log.trace("Add distance constraint of '{}': {}-{}", unit, min, max);
+			    log.trace("Add distance constraint of '{}': {}-{}",
+				      unit, min, max);
 
 			sseqqw.withConstraint(min, max, unit);
 		    };
@@ -380,18 +386,22 @@
 		};
 
 		if (json.has("classRefCheck"))
-		    this.addWarning("classRefCheck is not yet supported - " +
-				    "results may not be correct");
+		    this.addWarning(
+		        764,
+			"Class reference checks are currently not supported - results may not be correct"
+                    );
 
 		if (json.has("classRefOp"))
-		    this.addWarning("classRefOp is not yet supported - " +
-				    "results may not be correct");
+		    throw new QueryException(
+		        761,
+			"Class reference operators are currently not supported"
+                    );
 
 		if (number > 0) {
 		    if (operands.size() != 1)
 			throw new QueryException(
-			    612,
-			    "Class group expects exactly one operand in list"
+			    705,
+			    "Number of operands is not acceptable"
 			);
 
 		    if (DEBUG)
@@ -399,7 +409,7 @@
 
 		    if (number > MAX_CLASS_NUM) {
 			throw new QueryException(
-			    612, "Class numbers limited to " + MAX_CLASS_NUM
+			    709, "Valid class numbers exceeded"
                         );
 		    };
 
@@ -412,17 +422,16 @@
 		    return new SpanClassQueryWrapper(sqw, number);
 		};
 
-		throw new QueryException(612, "Class group expects class attribute");
+		throw new QueryException(710, "Class attribute missing");
 
 	    case "operation:repetition":
 
 		if (operands.size() != 1)
 		    throw new QueryException(
-		        612,
-			"Class group expects exactly one operand in list"
+		        705,
+			"Number of operands is not acceptable"
 		    );
 
-
 		int min = 0;
 		int max = 100;
 
@@ -468,52 +477,53 @@
 		return new SpanRepetitionQueryWrapper(sqw, min, max);
 
 	    case "operation:relation":
-		throw new QueryException(613, "Relations are not yet supported");
+		throw new QueryException(765, "Relations are currently not supported");
 	    };
 
-	    throw new QueryException(613, "Unknown group operation");
+	    throw new QueryException(711, "Unknown group operation");
 
 	case "korap:reference":
 	    if (json.has("operation") &&
 		!json.get("operation").asText().equals("operation:focus"))
-		throw new QueryException(613, "Reference operation " +
-					 json.get("operation").asText() +
-					 " not supported yet");
+		throw new QueryException(712, "Unknown reference operation");
 
 	    if (!json.has("operands"))
 		throw new QueryException(
-		    613, "Peripheral references are not supported yet"
+		    766, "Peripheral references are currently not supported"
                 );
 
 	    operands = json.get("operands");
 
 	    if (!operands.isArray())
-		throw new QueryException(612, "Operation needs operand list");
+		throw new QueryException(704, "Operation needs operand list");
 
 	    if (operands.size() == 0)
-		throw new QueryException(612, "Operation needs operand list");
+		throw new QueryException(704, "Operation needs operand list");
 
 	    if (operands.size() != 1)
-		throw new QueryException(612, "Operation needs exactly two operands");
+		throw new QueryException(705, "Number of operands is not acceptable");
 
 	    if (json.has("classRef")) {
 		if (json.has("classRefOp")) {
 		    throw new QueryException(
-		        613,
-			"Class reference operators not supported yet"
+		        761,
+			"Class reference operators are currently not supported"
 		    );
 		};
 
 		number = json.get("classRef").get(0).asInt();
 
+
 		if (number > MAX_CLASS_NUM)
 		    throw new QueryException(
-                        613,
-			"Class numbers limited to " + MAX_CLASS_NUM
+		        709, "Valid class numbers exceeded"
                     );
 	    }
 	    else if (json.has("spanRef")) {
-		throw new QueryException(613, "Span references not supported yet");
+		throw new QueryException(
+		    762,
+		    "Span references are currently not supported"
+	        );
 	    };
 
 	    if (DEBUG)
@@ -532,12 +542,9 @@
 	    return this._segFromJSON(json.get("wrap"));
 
 	case "korap:span":
-	    if (!json.has("key"))
-		throw new QueryException(612, "A span needs at least a key definition");
-
 	    return this._termFromJSON(json);
 	};
-	throw new QueryException(613, "Unknown serialized query type");
+	throw new QueryException(713, "Query type is not supported");
     };
 
 
@@ -545,7 +552,7 @@
     private SpanQueryWrapper _segFromJSON (JsonNode json) throws QueryException {
 
 	if (!json.has("@type"))
-	    throw new QueryException(612, "JSON-LD group has no @type attribute");
+	    throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 	String type = json.get("@type").asText();
 
@@ -571,12 +578,12 @@
 		return this._termFromJSON(json);
 	    };
 
-	    throw new QueryException(613, "Match relation unknown");
+	    throw new QueryException(741, "Match relation unknown");
 
 	case "korap:termGroup":
 
 	    if (!json.has("operands"))
-		throw new QueryException(612, "Term group expects operands");
+		throw new QueryException(742, "Term group needs operand list");
 
 	    // Get operands
 	    JsonNode operands = json.get("operands");
@@ -584,7 +591,7 @@
 	    SpanSegmentQueryWrapper ssegqw = this.seg();
 
 	    if (!json.has("relation"))
-		throw new QueryException(612, "Term group expects a relation");
+		throw new QueryException(743, "Term group expects a relation");
 
 	    switch (json.get("relation").asText()) {
 	    case "relation:and":
@@ -602,7 +609,7 @@
 		    }
 		    else {
 			throw new QueryException(
-			    613, "Object not supported in segment queries"
+			    744, "Operand not supported in term group"
                         );
 		    };
 		};
@@ -617,16 +624,16 @@
 		return ssaq;
 	    };
 	};
-	throw new QueryException(613, "Unknown token type");    
+	throw new QueryException(745, "Token type is not supported");    
     };
 
 
     private SpanQueryWrapper _termFromJSON (JsonNode json) throws QueryException {
 	if (!json.has("key") || json.get("key").asText().length() < 1)
-	    throw new QueryException(612, "Terms and spans have to provide key attributes");
+	    throw new QueryException(740, "Key definition is missing in term or span");
 	    
 	if (!json.has("@type"))
-	    throw new QueryException(612, "JSON-LD group has no @type attribute");
+	    throw new QueryException(701, "JSON-LD group has no @type attribute");
 
 	Boolean isTerm = json.get("@type").asText().equals("korap:term") ? true : false;
 	Boolean isCaseInsensitive = false;
@@ -658,6 +665,11 @@
 		break;
 
 	    case "orth":
+		// TODO: THIS IS A BUG! AND SHOULD BE NAMED "SURFACE"
+		layer = "s";
+		break;
+
+	    case "struct":
 		layer = "s";
 		break;
 
@@ -671,7 +683,10 @@
 		    layer = "i";
 		}
 		else {
-		    this.addWarning("Layer does not support case insensitivity");
+		    this.addWarning(
+			767,
+		        "Case insensitivity is currently not supported for this layer"
+		    );
 		};
 	    };
 
@@ -687,6 +702,9 @@
 	    value.append(isCaseInsensitive ? key.toLowerCase() : key);
 	};
 
+	if (json.has("value") && json.get("value").asText().length() > 0)
+	    value.append(':').append(json.get("value").asText());
+
 	// Regular expression or wildcard
 	if (isTerm && json.has("type")) {
 	    switch (json.get("type").asText()) {
@@ -694,22 +712,25 @@
 		return this.seg(this.re(value.toString(), isCaseInsensitive));
 	    case "type:wildcard":
 		return this.seq(this.wc(value.toString(), isCaseInsensitive));
+	    case "type:string":
+		break;
+	    default:
+		this.addWarning(746, "Term type is not supported - treated as a string");
 	    };
 	};
 
-	if (json.has("value") && json.get("value").asText().length() > 0)
-	    value.append(':').append(json.get("value").asText());
-
 	if (isTerm)
 	    return this.seg(value.toString());
 
 	if (json.has("attr"))
-	    this.addWarning("Attributes are not yet supported - " +
-			    "results may not be correct");
+	    this.addWarning(
+	        768,
+		"Attributes are currently not supported - results may not be correct");
 
 	return this.tag(value.toString());
     };
 
+    /*
     public boolean hasWarning () {
 	if (this.warning != null)
 	    return true;
@@ -732,7 +753,7 @@
     public void setWarning (String warning) {
 	this.warning = warning;
     };
-
+    */
 
     // SpanRegexQueryWrapper
     /**
diff --git a/src/main/java/de/ids_mannheim/korap/KorapResult.java b/src/main/java/de/ids_mannheim/korap/KorapResult.java
index ab4389a..9b00870 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapResult.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapResult.java
@@ -10,7 +10,7 @@
 
 import de.ids_mannheim.korap.index.PositionsToOffset;
 import de.ids_mannheim.korap.index.SearchContext;
-import de.ids_mannheim.korap.server.KorapResponse;
+import de.ids_mannheim.korap.response.KorapResponse;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -21,6 +21,7 @@
 /*
 TODO: Reuse the KorapSearch code for data serialization!
 */
+
 @JsonInclude(Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class KorapResult extends KorapResponse {
@@ -29,6 +30,9 @@
     @JsonIgnore
     public static final short ITEMS_PER_PAGE = 25;
 
+    private int totalResults;
+    private long totalTexts;
+
     private String query;
 
     private List<KorapMatch> matches;
@@ -43,7 +47,6 @@
     private String benchmarkSearchResults,
             benchmarkHitCounter;
     private String error = null;
-    private String warning = null;
 
     private JsonNode request;
 
@@ -70,15 +73,15 @@
         this.matches = new ArrayList<>(itemsPerPage);
         this.query = query;
         this.startIndex = startIndex;
-        this.itemsPerPage = (itemsPerPage > 50 || itemsPerPage < 1) ? ITEMS_PER_PAGE : itemsPerPage;
+        this.itemsPerPage = (itemsPerPage > 50 || itemsPerPage < 1) ?
+	    ITEMS_PER_PAGE : itemsPerPage;
         this.context = context;
-    }
+    };
 
 
-    public void add(KorapMatch km) {
+    public void add (KorapMatch km) {
         this.matches.add(km);
-    }
-
+    };
 
     public KorapMatch addMatch (PositionsToOffset pto,
 				int localDocID,
@@ -89,23 +92,6 @@
         // Temporary - should use the same interface like results
         // in the future:
         km.setContext(this.context);
-
-        // Add pos for context
-        // That's not really a good position for it,
-        // to be honest ...
-        // But maybe it will make the offset
-        // information in the match be obsolete!
-
-        // TODO:
-    /*
-    if (km.leftTokenContext) {
-	    pto.add(localDocID, startPos - this.leftContextOffset);
-	};
-	if (km.rightTokenContext) {
-	    pto.add(localDocID, endPos + this.rightContextOffset - 1);
-	};
-	*/
-
         this.add(km);
         return km;
     };
@@ -113,45 +99,15 @@
     @Deprecated
     public int totalResults() {
         return this.getTotalResults();
-    }
+    };
 
     public short getItemsPerPage() {
         return this.itemsPerPage;
-    }
-
+    };
 
     @Deprecated
     public short itemsPerPage() {
         return this.itemsPerPage;
-    }
-
-    /*
-
-    public String getError() {
-        return this.error;
-    }
-
-    public void setError(String msg) {
-        this.error = msg;
-    }
-
-    */
-
-    public String getWarning() {
-        return this.warning;
-    }
-
-    public void addWarning (String msg) {
-	if (msg == null)
-	    return;
-	if (this.warning == null)
-	    this.warning = msg;
-	else
-	    this.warning += "; " + warning;
-    };
-
-    public void setWarning (String warning) {
-	this.warning = warning;
     };
 
     public void setRequest(JsonNode request) {
@@ -162,6 +118,7 @@
         return this.request;
     };
 
+    /*
     @JsonIgnore
     public void setBenchmarkHitCounter(long t1, long t2) {
         this.benchmarkHitCounter =
@@ -177,6 +134,33 @@
         return this.benchmarkHitCounter;
     };
 
+    */
+
+    // Make this working in a KorapResult class
+    // that is independent from search and collection
+    public KorapResult setTotalTexts (long i) {
+        this.totalTexts = i;
+	return this;
+    };
+
+    public long getTotalTexts() {
+        return this.totalTexts;
+    };
+
+    public KorapResult setTotalResults (int i) {
+        this.totalResults = i;
+	return this;
+    };
+
+    public KorapResult incrTotalResults (int i) {
+        this.totalResults += i;
+	return this;
+    };
+
+    public int getTotalResults() {
+        return this.totalResults;
+    };
+
     @JsonIgnore
     public void setItemsPerResource (short value) {
 	this.itemsPerResource = value;
@@ -193,6 +177,8 @@
     };
 
     public void setTimeExceeded (boolean timeout) {
+	if (timeout)
+	    this.addWarning(682, "Search time exceeded");
 	this.timeExceeded = timeout;
     };
 
@@ -236,31 +222,41 @@
         return this.context;
     }
 
-
-    // Identical to KorapMatch!
-    public String toJSON () {
-        ObjectNode json = (ObjectNode) mapper.valueToTree(this);
+    public JsonNode toJSONnode () {
+	ObjectNode json = (ObjectNode) mapper.valueToTree(super.toJSONnode());
 
 	if (this.context != null)
 	    json.put("context", this.getContext().toJSON());
 
 	if (this.itemsPerResource > 0)
-	    json.put("itemsPerResource", this.itemsPerResource);
+	    json.put("itemsPerResource",
+		     this.itemsPerResource);
 
-        if (this.getVersion() != null)
-            json.put("version", this.getVersion());
+	json.put("itemsPerPage",
+		 this.itemsPerPage);
+
+	// TODO: If test
+	if (this.request != null)
+	    json.put("request", this.request);
+
+	// TODO: If test
+	if (this.request != null)
+	    json.put("request", this.request);
+	if (this.query != null)
+	    json.put("query", this.query);
+
+	json.put("startIndex", this.startIndex);
+
+	json.put("totalResults", this.getTotalResults());
+
+	if (this.timeExceeded)
+	    json.put("timeExceeded", this.timeExceeded);
 
 	// Add matches
-	json.putPOJO("matches", this.getMatches());
+	if (this.matches != null)
+	    json.putPOJO("matches", this.getMatches());
 
-        try {
-            return mapper.writeValueAsString(json);
-        }
-	catch (Exception e) {
-            log.warn(e.getLocalizedMessage());
-        };
-
-        return "{}";
+	return json;
     };
 
 
@@ -268,9 +264,6 @@
     public String toTokenListJSON () {
         ObjectNode json = (ObjectNode) mapper.valueToTree(this);
 
-        if (this.getVersion() != null)
-            json.put("version", this.getVersion());
-
 	ArrayNode array = json.putArray("matches");
 	
 	// Add matches as token lists
diff --git a/src/main/java/de/ids_mannheim/korap/KorapSearch.java b/src/main/java/de/ids_mannheim/korap/KorapSearch.java
index 2d342b2..d145856 100644
--- a/src/main/java/de/ids_mannheim/korap/KorapSearch.java
+++ b/src/main/java/de/ids_mannheim/korap/KorapSearch.java
@@ -10,6 +10,7 @@
 import de.ids_mannheim.korap.KorapResult;
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.index.SearchContext;
+import de.ids_mannheim.korap.response.Notifications;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.JsonNode;
@@ -26,7 +27,7 @@
  *
  * KorapSearch implements an object for all search relevant parameters.
  */
-public class KorapSearch {
+public class KorapSearch extends Notifications {
     private int startIndex = 0,
 	        limit = 0;
     private short count = 25,
@@ -36,7 +37,7 @@
     private SpanQuery query;
     private KorapCollection collection;
     private KorapIndex index;
-    private String error, warning;
+    //    private String error, warning;
 
     // Timeout search after milliseconds
     private long timeout = (long) 120_000;
@@ -50,7 +51,6 @@
 
     private long timeoutStart = Long.MIN_VALUE;
 
-
     {
 	context  = new SearchContext();
 
@@ -76,8 +76,10 @@
 
     public KorapSearch (String jsonString) {
 	ObjectMapper mapper = new ObjectMapper();
+
 	try {
-	    this.request = mapper.readValue(jsonString, JsonNode.class);
+	    // Todo - use correct method!
+	    this.request = mapper.readTree(jsonString);
 	    
 	    // "query" value
 	    if (this.request.has("query")) {
@@ -86,39 +88,49 @@
 		    SpanQueryWrapper qw = kq.fromJSON(this.request.get("query"));
 
 		    if (qw.isEmpty()) {
-			this.error = "This query matches everywhere";
+
+			// Unable to process result
+			this.addError(780, "This query matches everywhere");
 		    }
 		    else {
 			this.query = qw.toQuery();
 			if (qw.isOptional())
-			    this.addWarning("Optionality of query is ignored");
+			    this.addWarning(781, "Optionality of query is ignored");
 			if (qw.isNegative())
-			    this.addWarning("Exclusivity of query is ignored");
+			    this.addWarning(782, "Exclusivity of query is ignored");
 
-			// Set query deserialization warning
-			if (kq.hasWarning())
-			    this.addWarning(kq.getWarning());
 		    };
+		    // Copy notifications from query
+		    this.copyNotificationsFrom(kq);
+		    kq.clearNotifications();
 		}
 		catch (QueryException q) {
-		    this.error = q.getMessage();
+		    this.addError(q.getErrorCode(), q.getMessage());
 		};
 	    }
 	    else {
-		this.error = "No query defined";
+		this.addError(700, "No query given");
 	    };
 
-	    // Legacy code: Report warning coming from the request
-	    if (this.request.has("warning") && this.request.get("warning").asText().length() > 0)
-		this.addWarning(this.request.get("warning").asText());
+	    // <legacycode>
+	    if (this.request.has("warning") &&
+		this.request.get("warning").asText().length() > 0)
+		this.addWarning(
+		    799,
+		    this.request.get("warning").asText()
+		);
+	    // </legacycode>
+	    // <legacycode>
 	    if (this.request.has("warnings")) {
 		JsonNode warnings = this.request.get("warnings");
 		for (JsonNode node : warnings)
 		    if (node.asText().length() > 0)
-			this.addWarning(node.asText());
+			this.addWarning(799, node.asText());
 	    };
-	    // end of legacy code
+	    // </legacycode>
 
+	    // Copy notifications from request
+	    this.copyNotificationsFrom(this.request);
 	    
 	    // virtual collections
 	    if (this.request.has("collection") ||
@@ -126,7 +138,7 @@
 		this.request.has("collections"))
 		this.setCollection(new KorapCollection(jsonString));
 
-	    if (this.error == null) {
+	    if (!this.hasErrors()) {
 		if (this.request.has("meta")) {
 		    JsonNode meta = this.request.get("meta");
 
@@ -179,7 +191,7 @@
 
 	// Unable to parse JSON
 	catch (IOException e) {
-	    this.error = e.getMessage();
+	    this.addError(621, "Unable to parse JSON");
 	};
     };
 
@@ -189,7 +201,7 @@
 	    this.query = sqwi.toQuery();
 	}
 	catch (QueryException q) {
-	    this.error = q.getMessage();
+	    this.addError(q.getErrorCode(), q.getMessage());
 	};
     };
 
@@ -208,24 +220,6 @@
 	this.timeout = timeout;
     };
 
-    public String getError () {
-	return this.error;
-    };
-
-    public String getWarning () {
-	return this.warning;
-    };
-
-    public void addWarning (String msg) {
-	if (msg == null)
-	    return;
-
-	if (this.warning == null)
-	    this.warning = msg;
-	else
-	    this.warning += "; " + msg;
-    };
-
     public SpanQuery getQuery () {
 	return this.query;
     };
@@ -235,11 +229,12 @@
     };
 
     public KorapSearch setQuery (SpanQueryWrapper sqwi) {
+	// this.copyNotifications(sqwi);
 	try {
 	    this.query = sqwi.toQuery();
 	}
 	catch (QueryException q) {
-	    this.error = q.getMessage();
+	    this.addError(q.getErrorCode(), q.getMessage());
 	};
 	return this;
     };
@@ -354,8 +349,10 @@
 
     public KorapSearch setCollection (KorapCollection kc) {
 	this.collection = kc;
-	if (kc.getError() != null)
-	    this.error = kc.getError();
+
+	// Copy messages from the collection
+	this.copyNotificationsFrom(kc);
+	kc.clearNotifications();
 	return this;
     };
 
@@ -371,27 +368,27 @@
 	    KorapResult kr = new KorapResult();
 	    kr.setRequest(this.request);
 
-	    if (this.error != null)
-		kr.setError(this.error);
+	    if (this.hasErrors())
+		kr.copyNotificationsFrom(this);
 	    else
-		kr.setError(this.getClass() + " expects a query");
+		kr.addError(700, "No query given");
 	    return kr;
 	};
 
-	if (this.error != null) {
+	if (this.hasErrors()) {
 	    KorapResult kr = new KorapResult();
+
+	    // TODO: dev mode
 	    kr.setRequest(this.request);
-	    kr.setError(this.error);
-	    if (this.warning != null)
-		kr.addWarning(this.warning);
+	    kr.copyNotificationsFrom(this);
 	    return kr;
 	};
 
 	this.getCollection().setIndex(ki);
 	KorapResult kr = ki.search(this);
 	kr.setRequest(this.request);
-	if (this.warning != null)
-	    kr.addWarning(this.warning);
+	kr.copyNotificationsFrom(this);
+	this.clearNotifications();
 	return kr;
     };
 };
diff --git a/src/main/java/de/ids_mannheim/korap/index/MatchCollector.java b/src/main/java/de/ids_mannheim/korap/index/MatchCollector.java
index ca3f056..114adac 100644
--- a/src/main/java/de/ids_mannheim/korap/index/MatchCollector.java
+++ b/src/main/java/de/ids_mannheim/korap/index/MatchCollector.java
@@ -1,10 +1,12 @@
 package de.ids_mannheim.korap.index;
 import de.ids_mannheim.korap.KorapMatch;
-import de.ids_mannheim.korap.server.KorapResponse;
+import de.ids_mannheim.korap.response.KorapResponse;
 import java.util.*;
 
 public class MatchCollector extends KorapResponse {
     public int totalResultDocs = 0;
+    private int totalResults;
+    private long totalTexts;
 
     public void add (int uniqueDocID, int matchcount) {
 	this.totalResultDocs++;
@@ -25,6 +27,31 @@
 	return totalResultDocs;
     };
 
+    // Make this working in a KorapResult class
+    // that is independent from search and collection
+    public MatchCollector setTotalTexts (long i) {
+        this.totalTexts = i;
+	return this;
+    };
+
+    public long getTotalTexts() {
+        return this.totalTexts;
+    };
+
+    public MatchCollector setTotalResults (int i) {
+        this.totalResults = i;
+	return this;
+    };
+
+    public MatchCollector incrTotalResults (int i) {
+        this.totalResults += i;
+	return this;
+    };
+
+    public int getTotalResults() {
+        return this.totalResults;
+    };
+
     public void commit() {};
     public void close() {};
 
diff --git a/src/main/java/de/ids_mannheim/korap/response/KorapResponse.java b/src/main/java/de/ids_mannheim/korap/response/KorapResponse.java
new file mode 100644
index 0000000..6f43e32
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/response/KorapResponse.java
@@ -0,0 +1,147 @@
+package de.ids_mannheim.korap.response;
+
+import java.util.*;
+import java.io.*;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import de.ids_mannheim.korap.response.Notifications;
+
+public class KorapResponse extends Notifications {
+    ObjectMapper mapper = new ObjectMapper();
+
+    private String
+	version,
+	name,
+	node,
+	listener;
+    private String benchmark;
+
+    // add timeout!!!
+    // remove totalResults, totalTexts
+
+    public KorapResponse () {};
+
+    /**
+     * Get version of the index
+     */
+    @JsonIgnore
+    public String getVersion () {
+	if (this.version == null)
+	    return null;
+	return this.version;
+    };
+
+    /**
+     * Set version number.
+     *
+     * @param version The version number of the index as
+     *                a string representation.
+     */
+    @JsonIgnore
+    public void setVersion (String version) {
+	this.version = version;
+    };
+
+    /**
+     * Get name the index
+     */
+    @JsonIgnore
+    public String getName () {
+	if (this.name == null)
+	    return null;
+	return this.name;
+    };
+
+    /**
+     * Set name.
+     *
+     * @param name The name of the index as
+     *             a string representation.
+     */
+    @JsonIgnore
+    public void setName (String name) {
+	this.name = name;
+    };
+
+    @JsonIgnore
+    public String getNode () {
+	return this.node;
+    };
+
+    @JsonIgnore
+    public KorapResponse setNode (String name) {
+	this.node = name;
+	return this;
+    };
+
+    @JsonIgnore
+    public KorapResponse setBenchmark (long t1, long t2) {
+        this.benchmark =
+                (t2 - t1) < 100_000_000 ?
+	    // Store as miliseconds
+	    (((double) (t2 - t1) * 1e-6) + " ms") :
+	    // Store as seconds
+	    (((double) (t2 - t1) / 1000000000.0) + " s");
+	return this;
+    };
+
+    @JsonIgnore
+    public KorapResponse setBenchmark (String bm) {
+	this.benchmark = bm;
+	return this;
+    };
+
+    @JsonIgnore
+    public String getBenchmark () {
+        return this.benchmark;
+    };
+
+    @JsonIgnore
+    public KorapResponse setListener (String listener) {
+	this.listener = listener;
+	return this;
+    };
+
+    @JsonIgnore
+    public String getListener () {
+	return this.listener;
+    }
+
+    /**
+     * Serialize response to JSON node.
+     */
+    @Override
+    public JsonNode toJSONnode () {
+
+	// Get notifications json response
+	ObjectNode json = (ObjectNode) super.toJSONnode();
+
+	StringBuilder sb = new StringBuilder();
+        if (this.getName() != null) {
+	    sb.append(this.getName());
+
+	    if (this.getVersion() != null)
+		sb.append("-").append(this.getVersion());
+	}
+        else if (this.getVersion() != null) {
+	    sb.append(this.getVersion());
+	};
+
+	if (sb.length() > 0)
+	    json.put("version", sb.toString());
+
+        if (this.getNode() != null)
+            json.put("node", this.getNode());
+
+        if (this.getListener() != null)
+            json.put("listener", this.getListener());
+
+        if (this.getBenchmark() != null)
+            json.put("benchmark", this.getBenchmark());
+
+	return (JsonNode) json;
+    };
+};
diff --git a/src/main/java/de/ids_mannheim/korap/response/Message.java b/src/main/java/de/ids_mannheim/korap/response/Message.java
new file mode 100644
index 0000000..4f7cd29
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/response/Message.java
@@ -0,0 +1,98 @@
+package de.ids_mannheim.korap.response;
+
+import java.util.LinkedList;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.*;
+
+public class Message implements Cloneable {
+    // Mapper for JSON serialization
+    ObjectMapper mapper = new ObjectMapper();
+
+    private String msg;
+    private int code = 0;
+    private LinkedList<String> parameters;
+
+    public Message (int code, String msg) {
+	this.code = code;
+	this.msg  = msg;
+    };
+
+    public Message () {};
+
+    @JsonIgnore
+    public void setMessage (String msg) {
+	this.msg = msg;
+    };
+
+    @JsonIgnore
+    public String getMessage () {
+	return this.msg;
+    };
+
+    @JsonIgnore
+    public void setCode (int code) {
+	this.code = code;
+    };
+
+    @JsonIgnore
+    public int getCode () {
+	return this.code;
+    };
+
+    public void addParameter (String param) {
+	if (this.parameters == null)
+	    this.parameters = new LinkedList<String>();
+	this.parameters.add(param);
+    };
+
+    public Object clone () throws CloneNotSupportedException
+    {
+	Message clone = new Message();
+	if (this.msg != null)
+	    clone.msg = this.msg;
+
+	clone.code = this.code;
+
+	if (this.parameters != null) {
+	    for (String p : this.parameters) {
+		clone.addParameter(p);
+	    };
+	};
+
+	return clone;
+    };
+
+    
+    public JsonNode toJSONnode () {
+	ArrayNode message = mapper.createArrayNode();
+
+	if (this.code != 0)
+	    message.add(this.getCode());
+
+	message.add(this.getMessage());
+	if (parameters != null)
+	    for (String p : parameters)
+		message.add(p);
+	return (JsonNode) message;
+    };
+
+    /**
+     * Get JSON string
+     */
+    public String toJSON () {
+	String msg = "";
+	try {
+	    return mapper.writeValueAsString(this.toJSONnode());
+	}
+	catch (Exception e) {
+	    msg = ", \"" + e.getLocalizedMessage() + "\"";
+	};
+
+	return
+	    "[620, " +
+	    "\"Unable to generate JSON\"" + msg + "]";
+    };
+};
diff --git a/src/main/java/de/ids_mannheim/korap/response/Messages.java b/src/main/java/de/ids_mannheim/korap/response/Messages.java
new file mode 100644
index 0000000..1af8614
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/response/Messages.java
@@ -0,0 +1,152 @@
+package de.ids_mannheim.korap.response;
+
+import de.ids_mannheim.korap.util.QueryException;
+import de.ids_mannheim.korap.response.Message;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Nils Diewald
+ *
+ * An array of messages.
+ */
+public class Messages implements Cloneable {
+
+    // Mapper for JSON serialization
+    ObjectMapper mapper = new ObjectMapper();
+    private ArrayList<Message> messages;
+
+    public Messages () {
+	this.messages = new ArrayList<Message>(3);
+    };
+
+    /**
+     * Add message
+     */
+    public Message add (int code,
+			String message,
+			String ... terms) {
+	Message newMsg = new Message(code, message);
+	messages.add(newMsg);
+	if (terms != null)
+	    for (String t : terms)
+		newMsg.addParameter(t);
+	return newMsg;
+    };
+
+    /**
+     * Add message
+     */
+    public Message add (Message msg) {
+	messages.add(msg);
+	return msg;
+    };
+
+    /**
+     * Add message usng JsonNode
+     */
+    public Message add (JsonNode msg) throws QueryException {
+	if (!msg.isArray() || !msg.has(0))
+	    throw new QueryException(
+	        750, "Passed notifications are not well formed"
+	    );
+
+	// Valid message
+	Message newMsg = new Message();
+	short i = 1;
+	if (msg.get(0).isNumber()) {
+	    newMsg.setCode(msg.get(0).asInt());
+	    if (!msg.has(1))
+		throw new QueryException(
+	            750, "Passed notifications are not well formed"
+	        );
+	    newMsg.setMessage(msg.get(1).asText());
+	    i++;
+	}
+	else {
+	    newMsg.setMessage(msg.get(0).asText());
+	};
+
+	// Add parameters
+	while (msg.has(i))
+	    newMsg.addParameter(msg.get(i++).asText());
+
+	// Add messages to list
+	this.add(newMsg);
+	return newMsg;
+    };
+
+
+    /**
+     * Add messages
+     */
+    public void add (Messages msgs) {
+	for (Message msg : msgs.getMessages()) {
+	    this.add(msg);
+	};
+    };
+
+    /**
+     * Clear all messages
+     */
+    public void clear () {
+	messages.clear();
+    };
+
+    public int size () {
+	return this.messages.size();
+    };
+
+    @JsonIgnore
+    public Message get (int index) {
+	return this.messages.get(index);
+    };
+
+    @JsonIgnore
+    public List<Message> getMessages () {
+	return this.messages;
+    };
+
+    public Object clone () throws CloneNotSupportedException
+    {
+	Messages clone = new Messages();
+	for (Message m : this.messages) {
+	    clone.add((Message) m.clone());
+	};
+
+	return clone;
+    };
+
+    /**
+     * Get JSON node
+     */
+    public JsonNode toJSONnode () {
+	ArrayNode messageArray = mapper.createArrayNode();
+	for (Message msg : this.messages)
+	    messageArray.add(msg.toJSONnode());
+	return (JsonNode) messageArray;
+    };
+
+    /**
+     * Get JSON string
+     */
+    public String toJSON () {
+	String msg = "";
+	try {
+	    return mapper.writeValueAsString(this.toJSONnode());
+	}
+	catch (Exception e) {
+	    msg = ", \"" + e.getLocalizedMessage() + "\"";
+	};
+
+	return
+	    "[620, " +
+	    "\"Unable to generate JSON\"" + msg + "]";
+    };
+};
diff --git a/src/main/java/de/ids_mannheim/korap/response/Notifications.java b/src/main/java/de/ids_mannheim/korap/response/Notifications.java
new file mode 100644
index 0000000..361fb9a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/response/Notifications.java
@@ -0,0 +1,362 @@
+package de.ids_mannheim.korap.response;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.*;
+
+import java.util.*;
+import java.io.*;
+import de.ids_mannheim.korap.response.Message;
+import de.ids_mannheim.korap.response.Messages;
+import de.ids_mannheim.korap.util.QueryException;
+
+/**
+ * Unified notification class for KorAP related errors,
+ * warnings and messages.
+ *
+ * @author Nils Diewald
+ */
+
+@JsonInclude(Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Notifications {
+
+    // Create object mapper for JSON generation
+    ObjectMapper mapper = new ObjectMapper();
+
+    private Messages warnings,
+	             errors,
+	             messages;
+
+    /**
+     * Add a new warning.
+     *
+     * @param code Integer code representation of the warning
+     * @param msg String representation of the warning
+     */
+    public void addWarning (int code, String msg, String ... terms) {
+	if (this.warnings == null)
+	    this.warnings = new Messages();
+	this.warnings.add(code, msg, terms);
+    };
+
+    public void addWarning (JsonNode msg) {
+	if (this.warnings == null)
+	    this.warnings = new Messages();
+
+	try {
+	    this.warnings.add(msg);
+	}
+	catch (QueryException qe) {
+	    this.warnings.add(qe.getErrorCode(), qe.getMessage());
+	};
+    };
+
+    /**
+     * Add new warnings.
+     *
+     * @param msgs Array representation of the warning
+     */
+    public void addWarnings (Messages msgs) {
+	if (this.warnings == null)
+	    this.warnings = msgs;
+	else
+	    this.warnings.add(msgs);
+    };
+
+
+    /**
+     * Get all warnings.
+     */
+    public Messages getWarnings () {
+	return this.warnings;
+    };
+
+
+    public Message getWarning (int index) {
+	if (this.warnings != null)
+	    return this.warnings.get(index);
+	return (Message) null;
+    };
+
+
+    /**
+     * Check for warnings.
+     */
+    public boolean hasWarnings () {
+	if (this.warnings == null || this.warnings.size() == 0)
+	    return false;
+	return true;
+    };
+
+    /**
+     * Add a new error.
+     *
+     * @param code Integer code representation of the error
+     * @param msg String representation of the error
+     */
+    public void addError (int code, String msg, String ... terms) {
+	if (this.errors == null)
+	    this.errors = new Messages();
+	this.errors.add(code, msg, terms);
+    };
+
+    public void addError (JsonNode msg) {
+	if (this.errors == null)
+	    this.errors = new Messages();
+	try {
+	    this.errors.add(msg);
+	}
+	catch (QueryException qe) {
+	    this.errors.add(qe.getErrorCode(), qe.getMessage());
+	};
+    };
+
+
+    /**
+     * Add new warnings.
+     *
+     * @param msgs Array representation of the warning
+     */
+    public void addErrors (Messages msgs) {
+	if (this.errors == null)
+	    this.errors = msgs;
+	else
+	    this.errors.add(msgs);
+    };
+
+
+    /**
+     * Get all errors.
+     */
+    public Messages getErrors () {
+	return this.errors;
+    };
+
+    public Message getError (int index) {
+	if (this.errors != null)
+	    return this.errors.get(index);
+	return (Message) null;
+    };
+
+    /**
+     * Check for errors.
+     */
+    public boolean hasErrors () {
+	if (this.errors == null || this.errors.size() == 0)
+	    return false;
+	return true;
+    };
+
+
+    /**
+     * Add a new message.
+     *
+     * @param code Integer code representation of the message
+     * @param msg String representation of the message
+     */
+    public void addMessage (int code, String msg, String ... terms) {
+	if (this.messages == null)
+	    this.messages = new Messages();
+	this.messages.add(code, msg, terms);
+    };
+
+    public void addMessage (JsonNode msg) {
+	if (this.messages == null)
+	    this.messages = new Messages();
+	try {
+	    this.messages.add(msg);
+	}
+	catch (QueryException qe) {
+	    this.messages.add(qe.getErrorCode(), qe.getMessage());
+	};
+    };
+
+    /**
+     * Add new warnings.
+     *
+     * @param msgs Array representation of the warning
+     */
+    public void addMessages (Messages msgs) {
+	if (this.messages == null)
+	    this.messages = msgs;
+	else
+	    this.messages.add(msgs);
+    };
+
+
+    /**
+     * Get all messages.
+     */
+    public Messages getMessages () {
+	return this.messages;
+    };
+
+    public Message getMessage (int index) {
+	if (this.messages != null)
+	    return this.messages.get(index);
+	return (Message) null;
+    };
+
+
+    /**
+     * Check for messages.
+     */
+    public boolean hasMessages () {
+	if (this.messages == null || this.messages.size() == 0)
+	    return false;
+	return true;
+    };
+
+
+    /**
+     * Copy notifications
+     */
+    public void copyNotificationsFrom (Notifications notes) {
+	try {
+	    if (notes.hasErrors())
+		this.addErrors((Messages) notes.getErrors().clone());
+	    if (notes.hasWarnings())
+		this.addWarnings((Messages) notes.getWarnings().clone());
+	    if (notes.hasMessages())
+		this.addMessages((Messages) notes.getMessages().clone());
+	}
+	catch (CloneNotSupportedException cnse) {
+	};
+	return;
+    };
+
+
+    /**
+     * Copy notifications from JsonNode
+     */
+    public void copyNotificationsFrom (JsonNode request) {
+
+	// Add warnings from JSON
+	if (request.has("warnings") &&
+	    request.get("warnings").isArray()) {
+	    JsonNode msgs = request.get("warnings");
+	    for (JsonNode msg : msgs)
+		this.addWarning(msg);
+	};
+
+	// Add messages from JSON
+	if (request.has("messages") &&
+	    request.get("messages").isArray()) {
+	    JsonNode msgs = request.get("messages");
+	    if (msgs.isArray())
+		for (JsonNode msg : msgs)
+		    this.addMessage(msg);
+	};
+
+	// Add errors from JSON
+	if (request.has("errors") &&
+	    request.get("errors").isArray()) {
+	    JsonNode msgs = request.get("errors");
+	    if (msgs.isArray())
+		for (JsonNode msg : msgs)
+		    this.addError(msg);
+	};
+    };
+
+
+    /**
+     * Serialize response to JSON node.
+     */
+    public JsonNode toJSONnode () {
+	ObjectNode json =  mapper.createObjectNode();
+
+	// Add messages
+	if (this.hasWarnings())
+	    json.put("warnings", this.getWarnings().toJSONnode());
+	if (this.hasErrors())
+	    json.put("errors", this.getErrors().toJSONnode());
+	if (this.hasMessages())
+	    json.put("messages", this.getMessages().toJSONnode());
+
+	return (JsonNode) json;
+    };
+
+    /**
+     * Serialize response to JSON string.
+     */
+    public String toJSON () {
+	String msg = "";
+	try {
+	    JsonNode node = this.toJSONnode();
+	    if (node == null)
+		return "{}";
+	    return mapper.writeValueAsString(node);
+	}
+	catch (Exception e) {
+	    msg = ", \"" + e.getLocalizedMessage() + "\"";
+	};
+
+	return
+	    "{\"errors\" : [" +
+	    "[620, " +
+	    "\"Unable to generate JSON\"" + msg + "]" +
+	    "]}";
+    };
+
+    /**
+     * Clear all notifications.
+     */
+    public void clearNotifications () {
+	if (this.warnings != null)
+	    this.warnings.clear();
+	if (this.messages != null)
+	    this.messages.clear();
+	if (this.errors != null)
+	    this.errors.clear();
+    };
+
+
+    // Remove:
+    @Deprecated
+    public void addError (String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void addWarning (String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void addMessage (String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void setError (String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void setWarning (String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void setMessage (String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void setError (int code, String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void setWarning (int code, String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+
+    @Deprecated
+    public void setMessage (int code, String msg) {
+	System.err.println("DEPRECATED " + msg);
+    };
+};
diff --git a/src/main/java/de/ids_mannheim/korap/server/KorapResponse.java b/src/main/java/de/ids_mannheim/korap/server/KorapResponse.java
deleted file mode 100644
index bcdd258..0000000
--- a/src/main/java/de/ids_mannheim/korap/server/KorapResponse.java
+++ /dev/null
@@ -1,156 +0,0 @@
-package de.ids_mannheim.korap.server;
-
-import java.util.*;
-import java.io.*;
-
-import com.fasterxml.jackson.annotation.*;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-
-@JsonInclude(Include.NON_NULL)
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class KorapResponse {
-    ObjectMapper mapper = new ObjectMapper();
-
-    private String errstr, msg, version, node, listener;
-    private int err;
-    private int totalResults;
-    private long totalTexts;
-    private String benchmark;
-
-    public KorapResponse (String node, String version) {
-	this.setNode(node);
-	this.setVersion(version);
-    };
-
-    public KorapResponse () {};
-
-    @JsonIgnore
-    public KorapResponse setError (int code, String msg) {
-	this.err = code;
-	this.errstr = msg;
-	return this;
-    };
-
-    @JsonIgnore
-    public KorapResponse setError (String msg) {
-	this.err = 699;
-	this.errstr = msg;
-	return this;
-    };
-
-    public KorapResponse setErrstr (String msg) {
-	this.errstr = msg;
-	return this;
-    };
-
-    public String getErrstr () {
-	return this.errstr;
-    };
-
-    public KorapResponse setErr (int num) {
-	this.err = num;
-	return this;
-    };
-
-    public int getErr () {
-	return this.err;
-    };
-
-    public KorapResponse setMsg (String msg) {
-	this.msg = msg;
-	return this;
-    };
-
-    public String getMsg () {
-	return this.msg;
-    };
-
-    public String getVersion () {
-	return this.version;
-    };
-
-    public KorapResponse setVersion (String version) {
-	this.version = version;
-	return this;
-    };
-
-    public String getNode () {
-	return this.node;
-    };
-
-    public KorapResponse setNode (String name) {
-	this.node = name;
-	return this;
-    };
-
-    public KorapResponse setTotalTexts (long i) {
-        this.totalTexts = i;
-	return this;
-    };
-
-    public long getTotalTexts() {
-        return this.totalTexts;
-    };
-
-    public KorapResponse setTotalResults (int i) {
-        this.totalResults = i;
-	return this;
-    };
-
-    public KorapResponse incrTotalResults (int i) {
-        this.totalResults += i;
-	return this;
-    };
-
-
-    public int getTotalResults() {
-        return this.totalResults;
-    };
-
-    @JsonIgnore
-    public KorapResponse setBenchmark (long t1, long t2) {
-        this.benchmark =
-                (t2 - t1) < 100_000_000 ? (((double) (t2 - t1) * 1e-6) + " ms") :
-                        (((double) (t2 - t1) / 1000000000.0) + " s");
-	return this;
-    };
-
-    public KorapResponse setBenchmark (String bm) {
-	this.benchmark = bm;
-	return this;
-    };
-
-    public String getBenchmark () {
-        return this.benchmark;
-    };
-
-    public KorapResponse setListener (String listener) {
-	this.listener = listener;
-	return this;
-    };
-
-    public String getListener () {
-	return this.listener;
-    }
-
-    // Serialize
-    public String toJSON () {
-	ObjectNode json =  (ObjectNode) mapper.valueToTree(this);
-	if (json.size() == 0)
-	    return "{}";
-
-	try {
-	    return mapper.writeValueAsString(json);
-	}
-	catch (Exception e) {
-	    this.errstr = e.getLocalizedMessage();
-	};
-
-	return "{\"errstr\" : \"" + this.errstr + "\"}";
-    };
-};
diff --git a/src/main/java/de/ids_mannheim/korap/server/Resource.java b/src/main/java/de/ids_mannheim/korap/server/Resource.java
index 1630bbc..6a663ae 100644
--- a/src/main/java/de/ids_mannheim/korap/server/Resource.java
+++ b/src/main/java/de/ids_mannheim/korap/server/Resource.java
@@ -26,7 +26,7 @@
 import de.ids_mannheim.korap.KorapCollection;
 import de.ids_mannheim.korap.KorapMatch;
 import de.ids_mannheim.korap.KorapResult;
-import de.ids_mannheim.korap.server.KorapResponse;
+import de.ids_mannheim.korap.response.KorapResponse;
 import de.ids_mannheim.korap.index.FieldDocument;
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.index.MatchCollector;
@@ -87,13 +87,21 @@
     @Produces(MediaType.APPLICATION_JSON)
     public String info () {
 	KorapIndex index = KorapNode.getIndex();
-	KorapResponse kresp = new KorapResponse(KorapNode.getName(), index.getVersion());
+	KorapResponse kresp = new KorapResponse();
+	kresp.setNode(KorapNode.getName());
+	kresp.setName(index.getName());
+	kresp.setVersion(index.getVersion());
+
 	kresp.setListener(KorapNode.getListener());
 	long texts = -1;
-
-	return kresp.setTotalTexts(index.numberOf("documents"))
-	    .setMsg("Up and running!")
-	    .toJSON();
+	/*
+	kresp.addMessage(
+	    "Number of documents in the index",
+	    String.parseLong(index.numberOf("documents"))
+	);
+	*/
+	kresp.addMessage(680, "Server is up and running!");
+	return kresp.toJSON();
     };
     
 
@@ -123,10 +131,17 @@
 
 	// Get index
 	KorapIndex index = KorapNode.getIndex();
-	KorapResponse kresp = new KorapResponse(KorapNode.getName(), index.getVersion());
 
-	if (index == null)
-	    return kresp.setError(601, "Unable to find index").toJSON();
+	KorapResponse kresp = new KorapResponse();
+	kresp.setNode(KorapNode.getName());
+
+	if (index == null) {
+	    kresp.addError(601, "Unable to find index");
+	    return kresp.toJSON();
+	};
+
+	kresp.setVersion(index.getVersion());
+	kresp.setName(index.getName());
 
 	String ID = "Unknown";
 	try {
@@ -134,15 +149,15 @@
 	    ID = fd.getID();
 	}
 	// Set HTTP to ???
+	// TODO: This may be a field error!
 	catch (IOException e) {
-	    
-	    return kresp.setErrstr(e.getLocalizedMessage()).toJSON();
+	    kresp.addError(602, "Unable to add document to index");
+	    return kresp.toJSON();
 	};
 
 	// Set HTTP to 200
-	return kresp.
-	    setMsg("Text \"" + ID + "\" was added successfully")
-	    .toJSON();
+	kresp.addMessage(681, "Document was added successfully", ID);
+	return kresp.toJSON();
     };
 
 
@@ -157,13 +172,16 @@
 
 	// Get index
 	KorapIndex index = KorapNode.getIndex();
-	KorapResponse kresp = new KorapResponse(KorapNode.getName(), index.getVersion());
+	KorapResponse kresp = new KorapResponse();
+	kresp.setNode(KorapNode.getName());
 
-	if (index == null)
-	    return kresp.setError(601, "Unable to find index").toJSON();
+	if (index == null) {
+	    kresp.addError(601, "Unable to find index");
+	    return kresp.toJSON();
+	};
 
-	if (version == null)
-	    version = index.getVersion();
+	kresp.setVersion(index.getVersion());
+	kresp.setName(index.getName());
 
 	// There are documents to commit
 	try {
@@ -171,7 +189,8 @@
 	}
 	catch (IOException e) {
 	    // Set HTTP to ???
-	    return kresp.setErrstr(e.getMessage()).toJSON();
+	    kresp.addError(603, "Unable to commit staged data to index");
+	    return kresp.toJSON();
 	};
 
 	// Set HTTP to ???
@@ -219,14 +238,18 @@
 	    };
 	    KorapResult kr = new KorapResult();
 	    kr.setNode(KorapNode.getName());
-	    kr.setError(610, "No UUIDs given");
+	    kr.addError(610, "Missing request parameters", "No unique IDs were given");
 	    return kr.toJSON();
 	};
 
-	return new KorapResponse(
-	    KorapNode.getName(),
-	    index.getVersion()
-        ).setError(601, "Unable to find index").toJSON();
+	KorapResponse kresp = new KorapResponse();
+	kresp.setNode(KorapNode.getName());
+	kresp.setName(index.getName());
+	kresp.setVersion(index.getVersion());
+
+	kresp.addError(601, "Unable to find index");
+	
+	return kresp.toJSON();
     };
 
 
@@ -247,11 +270,12 @@
 	KorapIndex index = KorapNode.getIndex();
 
 	// No index found
-	if (index == null)
-  	    return new KorapResponse(
-	        KorapNode.getName(),
-	        index.getVersion()
-            ).setError(601, "Unable to find index").toJSON();
+	if (index == null) {
+	    KorapResponse kresp = new KorapResponse();
+	    kresp.setNode(KorapNode.getName());
+	    kresp.addError(601, "Unable to find index");
+  	    return kresp.toJSON();
+	};
 
 	// Get the database
 	try {
@@ -271,10 +295,13 @@
 	    log.error(e.getLocalizedMessage());
 	};
 
-	return new KorapResponse(
-	    KorapNode.getName(),
-	    index.getVersion()
-        ).setError("Unable to connect to database").toJSON();
+	KorapResponse kresp = new KorapResponse();
+	kresp.setNode(KorapNode.getName());
+	kresp.setName(index.getName());
+	kresp.setVersion(index.getVersion());
+
+	kresp.addError(604, "Unable to connect to database");
+	return kresp.toJSON();
     };
 
 
@@ -307,10 +334,13 @@
 	    return kr.toJSON();
 	};
 
-	return new KorapResponse(
-	    KorapNode.getName(),
-	    index.getVersion()
-        ).setError(601, "Unable to find index").toJSON();
+	KorapResponse kresp = new KorapResponse();
+	kresp.setNode(KorapNode.getName());
+	kresp.setName(index.getName());
+	kresp.setVersion(index.getVersion());
+
+	kresp.addError(601, "Unable to find index");
+	return kresp.toJSON();
     };
 
     @GET
@@ -373,14 +403,14 @@
 	    catch (QueryException qe) {
 		// Todo: Make KorapMatch rely on KorapResponse!
                 KorapMatch km = new KorapMatch();
-                km.setError(qe.getMessage());
+                km.addError(qe.getErrorCode(), qe.getMessage());
                 return km.toJSON();
             }
 	};
 
 	// Response with error message
         KorapMatch km = new KorapMatch();
-        km.setError("Index not found");
+        km.addError(601, "Unable to find index");
         return km.toJSON();
     };
 
diff --git a/src/main/java/de/ids_mannheim/korap/util/KorapString.java b/src/main/java/de/ids_mannheim/korap/util/KorapString.java
index 1a653de..2922425 100644
--- a/src/main/java/de/ids_mannheim/korap/util/KorapString.java
+++ b/src/main/java/de/ids_mannheim/korap/util/KorapString.java
@@ -13,11 +13,19 @@
  */
 public class KorapString {
 
-    public static String StringfromFile(String path, Charset encoding) throws IOException {
+    /**
+     * Get String from file
+     */
+    public static String StringfromFile (String path, Charset encoding)
+	throws IOException {
 	byte[] encoded = Files.readAllBytes(Paths.get(path));
 	return new String(encoded, encoding);
     };
 
+
+    /**
+     * Get String from file
+     */
     public static String StringfromFile (String path) throws IOException {
 	return StringfromFile(path, StandardCharsets.UTF_8);
     };
diff --git a/src/main/java/de/ids_mannheim/korap/util/QueryException.java b/src/main/java/de/ids_mannheim/korap/util/QueryException.java
index c4ed26b..dc9d002 100644
--- a/src/main/java/de/ids_mannheim/korap/util/QueryException.java
+++ b/src/main/java/de/ids_mannheim/korap/util/QueryException.java
@@ -18,8 +18,7 @@
 
   public QueryException(Throwable cause) {
       super(cause);
-  };
-  
+  };  
   
   public QueryException(int code, String message) {	  
 	  super(message);
diff --git a/src/main/resources/index.properties b/src/main/resources/index.properties
index f9ecaaf..1c87567 100644
--- a/src/main/resources/index.properties
+++ b/src/main/resources/index.properties
@@ -1,4 +1,5 @@
-lucene.index.version = ${project.version}
+lucene.version = ${project.version}
+lucene.name = ${project.name}
 lucene.index.commit.count = 134217000
 lucene.index.commit.log = log/korap.commit.log
 lucene.indexDir = /home/ndiewald/Repositories/korap/KorAP-modules/KorAP-lucene-index/sandbox/index-tanja