Merge "Fix up com.fasterxml.jackson.jaxrs from [2.9.8,] to 2.11.3"
diff --git a/.gitignore b/.gitignore
index 4e95513..95d91a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,7 @@
 *.iml
 dependency-reduced-pom.xml
 admin_token
-/sandbox/
+/sandbox
 /bin/
 /db.sqlite
 /lite/liteDB.sqlite
diff --git a/core/Changes b/core/Changes
index 8700115..611af1f 100644
--- a/core/Changes
+++ b/core/Changes
@@ -5,6 +5,8 @@
    - Removed salt from config and updated config files.
 05/02/2020
    - Added welcome page.
+01/04/2020
+   - Support expand query parameter for match retrieval (diewald)
 11/05/2020
    - Added tool to create VC from list (diewald)
 29/05/2020
diff --git a/core/pom.xml b/core/pom.xml
index 958595a..f617a48 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -241,7 +241,7 @@
 		<dependency>
 			<groupId>de.ids_mannheim.korap</groupId>
 			<artifactId>Koral</artifactId>
-			<version>[0.36,)</version>
+			<version>[0.37,)</version>
 			<exclusions>
 				<exclusion>
 					<groupId>org.eclipse.jetty</groupId>
@@ -299,7 +299,7 @@
 		<dependency>
 			<groupId>de.ids_mannheim.korap</groupId>
 			<artifactId>Krill</artifactId>
-			<version>[0.59.2,)</version>
+			<version>[0.59.3,)</version>
 			<exclusions>
 				<exclusion>
 					<groupId>org.glassfish.jersey.containers</groupId>
diff --git a/core/src/main/java/de/ids_mannheim/korap/service/SearchService.java b/core/src/main/java/de/ids_mannheim/korap/service/SearchService.java
index 2d414f9..18a349c 100644
--- a/core/src/main/java/de/ids_mannheim/korap/service/SearchService.java
+++ b/core/src/main/java/de/ids_mannheim/korap/service/SearchService.java
@@ -360,13 +360,14 @@
     public String retrieveMatchInfo (String corpusId, String docId,
             String textId, String matchId, Set<String> foundries,
             String username, HttpHeaders headers, Set<String> layers,
-            boolean spans, boolean highlights) throws KustvaktException {
+            boolean spans, boolean sentenceExpansion,
+            boolean highlights) throws KustvaktException {
         String matchid =
                 searchKrill.getMatchId(corpusId, docId, textId, matchId);
 
         User user = createUser(username, headers);
         Pattern p = determineAvailabilityPattern(user);
-        
+
         boolean match_only = foundries == null || foundries.isEmpty();
         String results;
 //        try {
@@ -387,7 +388,7 @@
                 }
 
                 results = searchKrill.getMatch(matchid, foundryList, layerList,
-                        spans, highlights, true, p);
+                        spans, highlights, sentenceExpansion, p);
             }
             else {
                 results = searchKrill.getMatch(matchid, p);
diff --git a/core/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java b/core/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java
index 8096c77..27665a2 100644
--- a/core/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java
+++ b/core/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java
@@ -33,10 +33,6 @@
 
     private static final boolean DEBUG = false;
 
-    // Temporary - shouldn't be here.
-    String indexDir = "/data/prep_corpus/index/";
-    String i = "/Users/hanl/Projects/prep_corpus";
-    String klinux10 = "/vol/work/hanl/indices";
     public static KrillIndex index;
 
     /**
diff --git a/core/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java b/core/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java
index 128a0de..e3967b4 100644
--- a/core/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java
+++ b/core/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java
@@ -238,7 +238,7 @@
             @QueryParam("hls") Boolean highlights) throws KustvaktException {
 
         return retrieveMatchInfo(ctx, headers, locale, corpusId, docId, textId,
-                matchId, foundries, layers, spans, highlights);
+                                 matchId, foundries, layers, spans, "sentence", highlights);
     }
     
     @GET
@@ -253,9 +253,15 @@
             @QueryParam("foundry") Set<String> foundries,
             @QueryParam("layer") Set<String> layers,
             @QueryParam("spans") Boolean spans, 
+            @QueryParam("expand") String expansion, 
             // Highlights may also be a list of valid highlight classes
             @QueryParam("hls") Boolean highlights) throws KustvaktException {
 
+        Boolean expandToSentence = true;
+        if (expansion != null && (expansion.equals("false") || expansion.equals("null"))) {
+            expandToSentence = false;
+        }
+
         TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
         scopeService.verifyScope(tokenContext, OAuth2Scope.MATCH_INFO);
         spans = spans != null ? spans : false;
@@ -265,7 +271,7 @@
         try{
             String results = searchService.retrieveMatchInfo(corpusId, docId,
                     textId, matchId, foundries, tokenContext.getUsername(),
-                    headers, layers, spans, highlights);
+                    headers, layers, spans, expandToSentence, highlights);
             return Response.ok(results).build();
         }
         catch (KustvaktException e) {
diff --git a/full/Changes b/full/Changes
index 49be3dd..f4efc95 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,6 +1,10 @@
 # version 0.63
 26/10/2020
    - Updated dependency of nimbus-jose-jwt and oauth2-oidc-sdk (diewald)
+29/10/2020
+   - Introduction of a query reference rewrite mechanism (diewald)
+30/10/2020
+   - Added database methods for storing query references (diewald) 
 
 # version 0.62.4
 24/01/2020
diff --git a/full/src/main/java/de/ids_mannheim/korap/constant/VirtualCorpusType.java b/full/src/main/java/de/ids_mannheim/korap/constant/VirtualCorpusType.java
index 3d6b4fa..0aea814 100644
--- a/full/src/main/java/de/ids_mannheim/korap/constant/VirtualCorpusType.java
+++ b/full/src/main/java/de/ids_mannheim/korap/constant/VirtualCorpusType.java
@@ -7,6 +7,11 @@
  * @author margaretha
  *
  */
+/*
+ * TODO (nd):
+ *   This should probably be renamed to something like RessourceType,
+ *   as QueryReferences will use the same types.
+ */
 public enum VirtualCorpusType {
     /**
      * available for all
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/QueryReferenceDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/QueryReferenceDao.java
new file mode 100644
index 0000000..2d849b6
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/QueryReferenceDao.java
@@ -0,0 +1,126 @@
+package de.ids_mannheim.korap.dao;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.NonUniqueResultException;
+import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Join;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+
+import de.ids_mannheim.korap.constant.VirtualCorpusType;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+import de.ids_mannheim.korap.entity.QueryReference;
+import de.ids_mannheim.korap.entity.QueryReference_;
+
+/**
+ * QueryReferenceDao manages database queries and transactions
+ * regarding query fragments, e.g. retrieving and storing queries
+ * for embedding in more complex queries.
+ * 
+ * Based on VirtualCorpusDao.
+ *
+ * @author diewald
+ *
+ */
+
+@Transactional
+@Repository
+public class QueryReferenceDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    /**
+     * Create query reference and return ID.
+     * Does not support any access management yet
+     */
+    public int createQuery(
+        String qName,
+        VirtualCorpusType type,
+        // CorpusAccess requiredAccess,
+        String koralQuery,
+        String definition,
+        String description,
+        String status,
+        String createdBy
+        ) throws KustvaktException {
+        
+        QueryReference q = new QueryReference();
+        q.setName(qName);
+        q.setType(type);
+        q.setKoralQuery(koralQuery);
+        q.setDefinition(definition);
+        q.setDescription(description);
+        q.setStatus(status);
+        q.setCreatedBy(createdBy);
+
+        // Maybe unused
+        q.setRequiredAccess("");
+
+        entityManager.persist(q);
+        return q.getId();
+    };
+
+
+    /**
+     * Retrieve a single query reference based on its name.
+     */
+    public QueryReference retrieveQueryByName (String qName, String createdBy)
+        throws KustvaktException {
+        ParameterChecker.checkStringValue(createdBy, "createdBy");
+        ParameterChecker.checkStringValue(qName, "q");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryReference> query =
+            builder.createQuery(QueryReference.class);
+
+        Root<QueryReference> qref = query.from(QueryReference.class);
+
+        Predicate condition = builder.and(
+            builder.equal(qref.get(QueryReference_.createdBy),
+                          createdBy),
+            builder.equal(qref.get(QueryReference_.name), qName));
+
+        query.select(qref);
+        query.where(condition);
+
+        Query q = entityManager.createQuery(query);
+        QueryReference qr = null;
+        try {
+            qr = (QueryReference) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+        catch (NonUniqueResultException e) {
+            String refCode = createdBy + "/" + qName;
+            throw new KustvaktException(StatusCodes.NON_UNIQUE_RESULT_FOUND,
+                    "Non unique result found for query: retrieve virtual corpus by name "
+                            + refCode,
+                    String.valueOf(refCode), e);
+        }
+        return qr;
+    };
+
+    /**
+     * Remove a query reference from the database.
+     */
+    public void deleteQueryReference (QueryReference q)
+            throws KustvaktException {
+        if (!entityManager.contains(q)) {
+            q = entityManager.merge(q);
+        }
+        entityManager.remove(q);
+    }
+};
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/QueryReference.java b/full/src/main/java/de/ids_mannheim/korap/entity/QueryReference.java
new file mode 100644
index 0000000..2a6ecff
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/QueryReference.java
@@ -0,0 +1,100 @@
+package de.ids_mannheim.korap.entity;
+
+import java.util.List;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+import de.ids_mannheim.korap.constant.VirtualCorpusType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes the query reference table.
+ *
+ * It is yet not as complete as the Virtual Corpus implementation,
+ * as it has no mechanism for sharing any query references.
+ * 
+ * @author diewald
+ *
+ * @see VirtualCorpus
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "query_reference")
+public class QueryReference implements Comparable<QueryReference> {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String name;
+    @Enumerated(EnumType.STRING)
+    private VirtualCorpusType type; // TODO (nd): This should be named RessourceType
+    private String status;
+    private String description;
+    // @Enumerated(EnumType.STRING)
+    @Column(name = "required_access")
+    private String requiredAccess;
+    //private CorpusAccess requiredAccess;
+    @Column(name = "query")
+    private String koralQuery;
+    private String definition;
+    @Column(name = "created_by")
+    private String createdBy;
+
+    /*
+    @OneToMany(mappedBy = "queryReference", fetch = FetchType.LAZY,
+            cascade = CascadeType.REMOVE)
+    private List<VirtualCorpusAccess> virtualCorpusAccess;
+    */
+
+    @Override
+    public String toString () {
+        return "id=" + id +
+            ", name= " + name +
+            ", type= " + type +
+            ", status= " + status +
+            ", description=" + description +
+            // ", requiredAccess=" + requiredAccess +
+            ", query= " + koralQuery +
+            ", definition= " + definition +
+            ", createdBy= " + createdBy;
+    }
+
+    @Override
+    public int hashCode () {
+        int prime = 37;
+        int result = 1;
+        result = prime * result + id;
+        result = prime * result + name.hashCode();
+        result = prime * result + createdBy.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals (Object obj) {
+        QueryReference q = (QueryReference) obj;
+        return (this.id == q.getId()) ? true : false;
+    }
+
+    @Override
+    public int compareTo (QueryReference o) {
+        if (this.getId() > o.getId()) {
+            return 1;
+        }
+        else if (this.getId() < o.getId()) {
+            return -1;
+        }
+        return 0;
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java b/full/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java
new file mode 100644
index 0000000..c4193ed
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java
@@ -0,0 +1,110 @@
+package de.ids_mannheim.korap.rewrite;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.rewrite.KoralNode.RewriteIdentifier;
+import de.ids_mannheim.korap.service.QueryReferenceService;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.util.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * Rewrites query reference with the corresponding koral
+ * query describing the actual query fragment.
+ * 
+ * Based on VirtualCorpusRewrite. 
+ *
+ * @author diewald, margaretha
+ *
+ */
+@Component
+public class QueryReferenceRewrite implements RewriteTask.RewriteQuery {
+
+    @Autowired
+    private KustvaktConfiguration config;
+
+    @Autowired
+    private QueryReferenceService qService;
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node,
+                                   KustvaktConfiguration config,
+                                   User user) throws KustvaktException {
+        if (node.has("query")) {
+            node = node.at("/query");
+            findQueryRef(user.getUsername(), node);
+        }
+        return node;
+    }
+
+    private void findQueryRef (String username, KoralNode koralNode)
+        throws KustvaktException {
+        if (koralNode.has("@type")
+            && koralNode.get("@type").equals("koral:queryRef")) {
+            if (!koralNode.has("ref")) {
+                throw new KustvaktException(
+                    StatusCodes.MISSING_QUERY_REFERENCE,
+                    "ref is not found"
+                    );
+            }
+            else {
+                String queryRefName = koralNode.get("ref");
+                String queryRefOwner = "system";
+                boolean ownerExist = false;
+                if (queryRefName.contains("/")) {
+                    String[] names = queryRefName.split("/");
+                    if (names.length == 2) {
+                        queryRefOwner = names[0];
+                        queryRefName = names[1];
+                        ownerExist = true;
+                    }
+                }
+
+                JsonNode qref = qService.searchQueryByName(
+                    username,
+                    queryRefName,
+                    queryRefOwner);
+
+                rewriteQuery(qref,koralNode);
+            }
+        }
+        
+        else if (koralNode.has("operands")) {
+            KoralNode operands = koralNode.at("/operands");
+        
+            for (int i = 0; i < operands.size(); i++) {
+                KoralNode operand = operands.get(i);
+                this.findQueryRef(username, operand);
+                operand.buildRewrites();
+            }
+        }
+    }
+
+
+    private void removeOwner (String koralQuery,
+                              String queryRefOwner,
+                              KoralNode koralNode) throws KustvaktException {
+        JsonNode jsonNode = koralNode.rawNode();
+        String ref = jsonNode.at("/ref").asText();
+        koralNode.remove("ref", new RewriteIdentifier("ref", ref));
+
+        ref = ref.substring(queryRefOwner.length() + 1, ref.length());
+        koralNode.set("ref", ref, new RewriteIdentifier("ref", ref));
+    }
+
+    private void rewriteQuery (JsonNode qref, KoralNode koralNode)
+        throws KustvaktException {
+        JsonNode jsonNode = koralNode.rawNode();
+        koralNode.remove("@type",
+                new RewriteIdentifier("@type", jsonNode.at("/@type").asText()));
+        koralNode.remove("ref",
+                new RewriteIdentifier("ref", jsonNode.at("/ref").asText()));
+        koralNode.setAll((ObjectNode) qref);
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/QueryReferenceService.java b/full/src/main/java/de/ids_mannheim/korap/service/QueryReferenceService.java
new file mode 100644
index 0000000..04d7b67
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/service/QueryReferenceService.java
@@ -0,0 +1,183 @@
+package de.ids_mannheim.korap.service;
+
+import java.sql.SQLException;
+
+import java.util.regex.Pattern;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.dao.QueryReferenceDao;
+
+import de.ids_mannheim.korap.constant.VirtualCorpusType;
+
+import de.ids_mannheim.korap.entity.QueryReference;
+
+
+/**
+ * This service is similar to VirtualCorpusService,
+ * while not as complete.
+ * For the moment, e.g., there is no mechanism supported to
+ * share a query reference with other users or groups.
+ * Only private queries are supported.
+ *
+ * @author diewald
+ */
+@Service
+public class QueryReferenceService {
+
+    public static Pattern qNamePattern = Pattern.compile("[-\\w.]+");
+
+    @Autowired
+    private AdminDao adminDao;
+
+    @Autowired
+    private QueryReferenceDao qDao;
+
+
+    /**
+     * Serch for a query by its name.
+     */
+    public JsonNode searchQueryByName (String username,
+                                       String qName,
+                                       String createdBy) throws KustvaktException {
+
+        QueryReference qr = qDao.retrieveQueryByName(qName, createdBy);
+        if (qr == null) {
+            String refCode = createdBy + "/" + qName;
+            throw new KustvaktException(
+                StatusCodes.NO_RESOURCE_FOUND,
+                "Query reference " + refCode + " is not found.",
+                String.valueOf(refCode));
+        }
+
+        // TODO:
+        //   checkVCAcess(q, username);
+        return JsonUtils.readTree(qr.getKoralQuery());
+    };
+
+
+    /**
+     * Store a query in the database.
+     */
+    public void storeQuery (String qJson,
+                            String qName,
+                            String createdBy)
+        throws KustvaktException {
+
+        // TODO:
+        //   This doesn't support a whole bunch of applicable
+        //   information from VCs, like 'definition', 'description',
+        //   'status' etc.
+        
+        storeQuery(
+            qJson,
+            qName,
+            "",
+            createdBy
+            );
+    }
+
+
+    /**
+     * Store a query in the database.
+     */
+    public void storeQuery (String qJson,
+                            String qName,
+                            String desc,
+                            String username)
+        throws KustvaktException {
+        ParameterChecker.checkStringValue(qJson, "q");
+        ParameterChecker.checkNameValue(qName, "qName");
+
+        if (!qNamePattern.matcher(qName).matches()) {
+            throw new KustvaktException(
+                StatusCodes.INVALID_ARGUMENT,
+                "Query name must only contain letters, numbers, "
+                + "underscores, hypens and spaces",
+                qName);
+        }
+
+        if (username.equals("system") && !adminDao.isAdmin(username)) {
+            throw new KustvaktException(
+                StatusCodes.AUTHORIZATION_FAILED,
+                "Unauthorized operation for user: " + username, username);            
+        };
+
+        int qId = 0;
+        try {
+            qId = qDao.createQuery(
+                qName,
+                VirtualCorpusType.PRIVATE,
+                qJson,
+                "", // TODO: definition,
+                desc,
+                "", // TODO: status,
+                username);
+
+        }
+        catch (Exception e) {
+            Throwable cause = e;
+            Throwable lastCause = null;
+            while ((cause = cause.getCause()) != null
+                   && !cause.equals(lastCause)) {
+                if (cause instanceof SQLException) {
+                    break;
+                }
+                lastCause = cause;
+            }
+            throw new KustvaktException(
+                StatusCodes.DB_INSERT_FAILED,
+                cause.getMessage()
+                );
+        };
+
+        // TODO:
+        //   This doesn't publish the query, if it is meant to be published
+        //   based on its type.
+    };
+
+
+    /**
+     * Only admin and the owner of the virtual corpus are allowed to
+     * delete a virtual corpus.
+     */
+    public void deleteQueryByName (
+        String username,
+        String qName,
+        String createdBy
+        ) throws KustvaktException {
+
+        QueryReference q = qDao.retrieveQueryByName(qName, createdBy);
+
+        if (q == null) {
+            String refCode = createdBy + "/" + qName;
+            throw new KustvaktException(
+                StatusCodes.NO_RESOURCE_FOUND,
+                "Query reference " + refCode + " is not found.",
+                String.valueOf(refCode));
+        }
+
+        // Check if the user created the qr or is admin
+        else if (q.getCreatedBy().equals(username)
+                 || adminDao.isAdmin(username)) {
+            // TODO:
+            //   Here checks for publication status is missing
+            qDao.deleteQueryReference(q);
+        }
+
+        else {
+            throw new KustvaktException(
+                StatusCodes.AUTHORIZATION_FAILED,
+                "Unauthorized operation for user: " + username, username);
+        };
+    };
+};
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java b/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
index 4532991..1163573 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
@@ -331,7 +331,7 @@
 
         if (!vcNamePattern.matcher(vcName).matches()) {
             throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
-                    "Virtual corpus name must only contains letters, numbers, "
+                    "Virtual corpus name must only contain letters, numbers, "
                             + "underscores, hypens and spaces",
                     vcName);
         }
diff --git a/full/src/main/resources/db/mysql/V1.7__query_references.sql b/full/src/main/resources/db/mysql/V1.7__query_references.sql
new file mode 100644
index 0000000..6d636da
--- /dev/null
+++ b/full/src/main/resources/db/mysql/V1.7__query_references.sql
@@ -0,0 +1,14 @@
+CREATE TABLE IF NOT EXISTS query_refernce (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  name VARCHAR(255) NOT NULL,
+  type VARCHAR(100) NOT NULL,
+  required_access VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  description VARCHAR(255) DEFAULT NULL,
+  status VARCHAR(100) DEFAULT NULL,
+  query TEXT NOT NULL,
+  definition VARCHAR(255) DEFAULT NULL,
+  UNIQUE INDEX unique_index (name,created_by),
+  INDEX owner_index (created_by),
+  INDEX type_index (type)
+);
diff --git a/full/src/main/resources/db/sqlite/V1.7__query_references.sql b/full/src/main/resources/db/sqlite/V1.7__query_references.sql
new file mode 100644
index 0000000..0ebdbc1
--- /dev/null
+++ b/full/src/main/resources/db/sqlite/V1.7__query_references.sql
@@ -0,0 +1,16 @@
+CREATE TABLE IF NOT EXISTS query_reference (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  name VARCHAR(255) NOT NULL,
+  type VARCHAR(100) NOT NULL,
+  required_access VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  description VARCHAR(255) DEFAULT NULL,
+  status VARCHAR(100) DEFAULT NULL,
+  query TEXT NOT NULL,
+  definition VARCHAR(255) DEFAULT NULL
+);
+
+CREATE INDEX query_reference_owner_index ON query_reference(created_by);
+CREATE INDEX query_reference_type_index ON query_reference(type);
+CREATE UNIQUE INDEX query_reference_unique_name 
+	ON query_reference(name,created_by);
diff --git a/full/src/main/resources/db/test/V3.7__insert_query_references.sql b/full/src/main/resources/db/test/V3.7__insert_query_references.sql
new file mode 100644
index 0000000..e13b13b
--- /dev/null
+++ b/full/src/main/resources/db/test/V3.7__insert_query_references.sql
@@ -0,0 +1,4 @@
+-- query references
+INSERT INTO query_reference(name, type, required_access, created_by, description, status, query) 
+	VALUES ("dory-q", "PRIVATE", "FREE", "dory", "test query", "experimental",
+	'{ "@type": "koral:token" }');
diff --git a/full/src/main/resources/default-config.xml b/full/src/main/resources/default-config.xml
index bd25718..c1da94b 100644
--- a/full/src/main/resources/default-config.xml
+++ b/full/src/main/resources/default-config.xml
@@ -220,12 +220,14 @@
 	<bean id="foundryRewrite" class="de.ids_mannheim.korap.rewrite.FoundryRewrite"/>
 	<bean id="collectionRewrite" class="de.ids_mannheim.korap.rewrite.CollectionRewrite"/>
 	<bean id="virtualCorpusRewrite" class="de.ids_mannheim.korap.rewrite.VirtualCorpusRewrite"/>
+ 	<bean id="queryReferenceRewrite" class="de.ids_mannheim.korap.rewrite.QueryReferenceRewrite"/>
 	
 	<util:list id="rewriteTasks"
 		value-type="de.ids_mannheim.korap.rewrite.RewriteTask">
 		<ref bean="foundryRewrite" />
 		<ref bean="collectionRewrite" />
 		<ref bean="virtualCorpusRewrite" />
+    <ref bean="queryReferenceRewrite" />
 	</util:list>
 	
 	<bean id="rewriteHandler" class="de.ids_mannheim.korap.rewrite.RewriteHandler">
@@ -389,4 +391,4 @@
 			</props>
 		</constructor-arg>
 	</bean>
-</beans>
\ No newline at end of file
+</beans>
diff --git a/full/src/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java b/full/src/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java
new file mode 100644
index 0000000..fd2b0ff
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.rewrite;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+import com.sun.jersey.api.client.ClientResponse;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.SpringJerseyTest;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.util.QueryException;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * @author diewald
+ *
+ */
+public class QueryRewriteTest extends SpringJerseyTest {
+
+    @Test
+    public void testRewriteRefNotFound ()
+            throws KustvaktException, Exception {
+
+        ClientResponse response = resource().path(API_VERSION).path("search")
+            .queryParam("q", "[orth=der]{%23examplequery} Baum")
+            .queryParam("ql", "poliqarp")
+            .get(ClientResponse.class);
+
+        String ent = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(ent);
+        assertEquals(node.at("/errors/0/1").asText(), "Query reference system/examplequery is not found.");
+    }
+
+    @Test
+    public void testRewriteRefRewrite ()
+            throws KustvaktException, Exception {
+
+        // Added in the database migration sql for tests
+        ClientResponse response = resource().path(API_VERSION).path("search")
+            .queryParam("q", "[orth=der]{%23dory/dory-q} Baum")
+            .queryParam("ql", "poliqarp")
+            .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                    .createBasicAuthorizationHeaderValue("dory", "pass"))
+            .get(ClientResponse.class);
+
+        String ent = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(ent);
+        assertEquals("koral:token", node.at("/query/operands/1/@type").asText());
+        assertEquals("@type(koral:queryRef)",
+                     node.at("/query/operands/1/rewrites/0/scope").asText());
+    }
+}
diff --git a/full/src/test/java/de/ids_mannheim/korap/service/QueryReferenceServiceTest.java b/full/src/test/java/de/ids_mannheim/korap/service/QueryReferenceServiceTest.java
new file mode 100644
index 0000000..93d1133
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/service/QueryReferenceServiceTest.java
@@ -0,0 +1,53 @@
+package de.ids_mannheim.korap.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.constant.VirtualCorpusType;
+import de.ids_mannheim.korap.dto.VirtualCorpusAccessDto;
+import de.ids_mannheim.korap.dto.VirtualCorpusDto;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.VirtualCorpus;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.web.input.VirtualCorpusJson;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration("classpath:test-config.xml")
+public class QueryReferenceServiceTest {
+
+    @Autowired
+    private QueryReferenceService qService;
+
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void createQuery () throws KustvaktException {
+        qService.storeQuery("{\"@type\":\"koral:token\"}", "new-query", "me" );
+        JsonNode json = qService.searchQueryByName("me", "new-query", "me");
+        assertEquals("koral:token", json.at("/@type").asText());
+        qService.deleteQueryByName("me", "new-query", "me");
+    };
+
+    @Test
+    public void testCreateNonUniqueQuery () throws KustvaktException {
+        qService.storeQuery("{\"@type\":\"koral:token\"}", "new-query", "me" );
+        thrown.expect(KustvaktException.class);
+        qService.storeQuery("{\"@type\":\"koral:token\"}", "new-query", "me" );
+        qService.deleteQueryByName("me", "new-query", "me");        
+    };
+};
diff --git a/full/src/test/resources/test-config.xml b/full/src/test/resources/test-config.xml
index 82b314e..b9f5e07 100644
--- a/full/src/test/resources/test-config.xml
+++ b/full/src/test/resources/test-config.xml
@@ -203,6 +203,7 @@
 	<bean id="collectionCleanRewrite" class="de.ids_mannheim.korap.rewrite.CollectionCleanRewrite"/>
 	<bean id="virtualCorpusRewrite" class="de.ids_mannheim.korap.rewrite.VirtualCorpusRewrite"/>
 	<bean id="collectionConstraint" class="de.ids_mannheim.korap.rewrite.CollectionConstraint"/>
+ 	<bean id="queryReferenceRewrite" class="de.ids_mannheim.korap.rewrite.QueryReferenceRewrite"/>
 	
 	<util:list id="rewriteTasks"
 		value-type="de.ids_mannheim.korap.rewrite.RewriteTask">
@@ -211,6 +212,7 @@
 		<ref bean="foundryRewrite" />
 		<ref bean="collectionRewrite" />
 		<ref bean="virtualCorpusRewrite" />
+		<ref bean="queryReferenceRewrite" />
 	</util:list>
 	
 	<bean id="rewriteHandler" class="de.ids_mannheim.korap.rewrite.RewriteHandler">
@@ -375,4 +377,4 @@
 			</props>
 		</constructor-arg>
 	</bean>
-</beans>
\ No newline at end of file
+</beans>
diff --git a/lite/pom.xml b/lite/pom.xml
index d80b3e4..b40e638 100644
--- a/lite/pom.xml
+++ b/lite/pom.xml
@@ -129,6 +129,18 @@
 					</execution>
 				</executions>
 			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>cobertura-maven-plugin</artifactId>
+				<version>2.7</version>
+				<configuration>
+					<formats>
+						<format>html</format>
+						<format>xml</format>
+					</formats>
+					<check/>
+				</configuration>
+			</plugin>
 		</plugins>
 	</build>
 
@@ -160,5 +172,18 @@
 			<version>1.18.16</version>
 			<scope>provided</scope>
 		</dependency>
+		<!-- https://mvnrepository.com/artifact/org.codehaus.mojo/cobertura-maven-plugin -->
+		<dependency>
+			<groupId>org.codehaus.mojo</groupId>
+			<artifactId>cobertura-maven-plugin</artifactId>
+			<version>2.7</version>
+			<scope>test</scope>
+		</dependency>
+		<!-- https://mvnrepository.com/artifact/backport-util-concurrent/backport-util-concurrent -->
+		<dependency>
+			<groupId>backport-util-concurrent</groupId>
+			<artifactId>backport-util-concurrent</artifactId>
+			<version>3.1</version>
+		</dependency>
 	</dependencies>
 </project>
diff --git a/lite/src/test/java/de/ids_mannheim/korap/web/service/LiteSearchControllerTest.java b/lite/src/test/java/de/ids_mannheim/korap/web/service/LiteSearchControllerTest.java
index 4c2dd28..479ea57 100644
--- a/lite/src/test/java/de/ids_mannheim/korap/web/service/LiteSearchControllerTest.java
+++ b/lite/src/test/java/de/ids_mannheim/korap/web/service/LiteSearchControllerTest.java
@@ -223,6 +223,27 @@
     };
 
     @Test
+    public void testMatchInfoWithoutExtension () throws KustvaktException {
+        ClientResponse response = resource().path(API_VERSION)
+                .path("corpus/GOE/AGA/01784/p36-46(5)37-45(2)38-42")
+                .queryParam("foundry", "-").queryParam("spans", "false")
+                .queryParam("expand","false")
+                .get(ClientResponse.class);
+        assertEquals(ClientResponse.Status.OK.getStatusCode(),
+                response.getStatus());
+        String ent = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(ent);
+        assertNotNull(node);
+        assertEquals("GOE/AGA/01784", node.at("/textSigle").asText());
+        assertEquals("match-GOE/AGA/01784-p36-46(5)37-45(2)38-42",
+                node.at("/matchID").asText());
+        assertEquals("<span class=\"context-left\"><span class=\"more\"></span></span><span class=\"match\"><mark>gefüttert; der Ort ist sehr zerschossen; dann über die Schiffbrücke</mark></span><span class=\"context-right\"><span class=\"more\"></span></span>",
+                node.at("/snippet").asText());
+        assertEquals("Belagerung von Mainz", node.at("/title").asText());
+    };
+
+    
+    @Test
     public void testMatchInfoGetWithHighlights () throws KustvaktException {
         ClientResponse response = resource().path(API_VERSION)
                 .path("corpus/GOE/AGA/01784/p36-46(5)37-45(2)38-42/matchInfo")