Merge "Added persistency layer for query references"
diff --git a/full/Changes b/full/Changes
index 091921f..f4efc95 100644
--- a/full/Changes
+++ b/full/Changes
@@ -2,7 +2,9 @@
 26/10/2020
    - Updated dependency of nimbus-jose-jwt and oauth2-oidc-sdk (diewald)
 29/10/2020
-   - First introduction of a query reference rewrite (diewald)
+   - 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
index aab0b77..c4193ed 100644
--- a/full/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java
+++ b/full/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java
@@ -30,7 +30,7 @@
     private KustvaktConfiguration config;
 
     @Autowired
-    private QueryReferenceService refService;
+    private QueryReferenceService qService;
 
     @Override
     public KoralNode rewriteQuery (KoralNode node,
@@ -66,7 +66,7 @@
                     }
                 }
 
-                JsonNode qref = refService.searchQueryByName(
+                JsonNode qref = qService.searchQueryByName(
                     username,
                     queryRefName,
                     queryRefOwner);
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
index eb99f68..04d7b67 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/QueryReferenceService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/QueryReferenceService.java
@@ -1,34 +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 refName,
+                                       String qName,
                                        String createdBy) throws KustvaktException {
 
-        String refCode = createdBy + "/" + refName;
+        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));
+        }
 
-        if (refCode.equals("system/emptyToken")) {
-            return JsonUtils.readTree("{\"@type\":\"koral:token\"}");
+        // 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);            
         };
 
-        throw new KustvaktException(
-            StatusCodes.NO_RESOURCE_FOUND,
-            "Query reference " + refCode + " is not found.",
-            String.valueOf(refCode));
-    }
+        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/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java b/full/src/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java
index 5cae6bc..fd2b0ff 100644
--- a/full/src/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/rewrite/QueryRewriteTest.java
@@ -42,14 +42,18 @@
     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]{%23system/emptyToken} Baum")
+            .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());
+        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");        
+    };
+};