Added persistency layer for query references
Change-Id: I7d3ad7f285a27eec977aaf4f2b68fe85b0e1f228
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");
+ };
+};