Merge "Bump jackson-jaxrs-json-provider from 2.11.3 to 2.12.1 in /core"
diff --git a/full/Changes b/full/Changes
index 45ff8ec..8d3d4a2 100644
--- a/full/Changes
+++ b/full/Changes
@@ -3,7 +3,8 @@
  - Updated libraries (margaretha)
  - Renamed virtual corpus to query (margaretha)
 2021-02-26
- - Added query access roles and fixed vc access roles.
+ - Added query access roles and fixed vc access roles (margaretha)
+ - Added delete query webservice and tests (margaretha) 
  
 # version 0.63
 26/10/2020
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java b/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
index faa150c..41faa2a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
@@ -98,7 +98,8 @@
                         if (DEBUG) {
                             jlog.debug("Delete existing vc: " + filename);
                         }
-                        vcService.deleteVC("system", vc.getId());
+                        vcService.deleteQueryByName("system", vc.getName(),
+                                vc.getCreatedBy(), QueryType.VIRTUAL_CORPUS);
                     }
                 }
                 catch (KustvaktException e) {
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
index 27d3b9f..9fa399c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
@@ -40,7 +40,7 @@
 
 /**
  * QueryDao manages database queries and transactions
- * regarding KorAP queries, e.g. retrieving and storing queries.
+ * regarding virtual corpus and KorAP queries.
  * 
  * @author margaretha
  *
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/QueryService.java b/full/src/main/java/de/ids_mannheim/korap/service/QueryService.java
index 2289a47..3d60f21 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/QueryService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/QueryService.java
@@ -42,14 +42,22 @@
 import de.ids_mannheim.korap.utils.KoralCollectionQueryBuilder;
 import de.ids_mannheim.korap.utils.ParameterChecker;
 import de.ids_mannheim.korap.web.SearchKrill;
+import de.ids_mannheim.korap.web.controller.QueryReferenceController;
 import de.ids_mannheim.korap.web.controller.VirtualCorpusController;
 import de.ids_mannheim.korap.web.input.QueryJson;
 
 /**
- * VirtualCorpusService handles the logic behind
- * {@link VirtualCorpusController}.
- * It communicates with {@link QueryDao} and returns
- * {@link QueryDto} to {@link VirtualCorpusController}.
+ * QueryService handles the logic behind
+ * {@link VirtualCorpusController} and
+ * {@link QueryReferenceController}. Virtual corpora and
+ * stored-queries are both treated as queries of different types.
+ * Thus, they are handled logically similarly.
+ * 
+ * QueryService communicates with {@link QueryDao}, handles
+ * {@link QueryDO} and
+ * returns
+ * {@link QueryDto} to {@link VirtualCorpusController} and
+ * {@link QueryReferenceController}.
  * 
  * @author margaretha
  *
@@ -158,59 +166,15 @@
         return dtos;
     }
 
-    /**
-     * Only admin and the owner of the query are allowed to
-     * delete a query.
-     * 
-     * @param username
-     *            username
-     * @param queryId
-     *            query id
-     * @throws KustvaktException
-     */
-    @Deprecated
-    public void deleteVC (String username, int vcId) throws KustvaktException {
-
-        QueryDO vc = queryDao.retrieveQueryById(vcId);
-
-        if (vc.getCreatedBy().equals(username) || adminDao.isAdmin(username)) {
-
-            if (vc.getType().equals(ResourceType.PUBLISHED)) {
-                QueryAccess access =
-                        accessDao.retrieveHiddenAccess(vcId);
-                accessDao.deleteAccess(access, "system");
-                userGroupService.deleteAutoHiddenGroup(
-                        access.getUserGroup().getId(), "system");
-            }
-            queryDao.deleteQuery(vc);
-        }
-        else {
-            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + username, username);
-        }
-    }
-
-    /**
-     * Only admin and the owner of the virtual corpus are allowed to
-     * delete a virtual corpus.
-     * 
-     * @param username
-     *            username
-     * @param queryName
-     *            virtual corpus name
-     * @param createdBy
-     *            virtual corpus creator
-     * @throws KustvaktException
-     */
     public void deleteQueryByName (String username, String queryName,
-            String createdBy) throws KustvaktException {
+            String createdBy, QueryType type) throws KustvaktException {
 
         QueryDO query = queryDao.retrieveQueryByName(queryName, createdBy);
 
         if (query == null) {
             String code = createdBy + "/" + queryName;
             throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
-                    "Virtual corpus " + code + " is not found.",
+                    "Query " + code + " is not found.",
                     String.valueOf(code));
         }
         else if (query.getCreatedBy().equals(username)
@@ -223,7 +187,8 @@
                 userGroupService.deleteAutoHiddenGroup(
                         access.getUserGroup().getId(), "system");
             }
-            if (KrillCollection.cache.get(query.getName()) != null) {
+            if (type.equals(QueryType.VIRTUAL_CORPUS)
+                    && KrillCollection.cache.get(query.getName()) != null) {
                 KrillCollection.cache.remove(query.getName());
             }
             queryDao.deleteQuery(query);
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java
index 2ebd938..4e90699 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java
@@ -3,6 +3,7 @@
 import java.util.List;
 
 import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
@@ -60,7 +61,8 @@
     /**
      * Creates a query reference according to the given Json.
      * The query reference creator must be the same as the
-     * authenticated username.
+     * authenticated username, except for admins. Admins may create
+     * and update system queries and queries for/of any users.
      * 
      * TODO: In the future, this may also update a query.
      *
@@ -69,9 +71,12 @@
      *            the username of the vc creator, must be the same
      *            as the authenticated username
      * @param qName
-     *           the vc name
-     * @param query a json object describing the query and its properties
-     * @return
+     *            the vc name
+     * @param query
+     *            a json object describing the query and its
+     *            properties
+     * @return HTTP Status 201 Created when creating a new query, or 204
+     *         No Content when updating an existing query.
      * @throws KustvaktException
      */
     @PUT
@@ -130,35 +135,35 @@
     }
 
     /**
-     * Only the VC owner and system admins can delete VC. VCA admins
-     * can delete VC-accesses e.g. of project VC, but not the VC
-     * themselves.
+     * Only the query owner and system admins can delete queries.
+     * Query access admins can delete query-accesses e.g. of project
+     * queries, but not the queries themselves.
      * 
      * @param securityContext
      * @param createdBy
-     *            vc creator
-     * @param vcName
-     *            vc name
+     *            query creator
+     * @param qName
+     *            query name
      * @return HTTP status 200, if successful
      */
-    /*
+    
     @DELETE
-    @Path("~{createdBy}/{vcName}")
+    @Path("~{createdBy}/{qName}")
     public Response deleteVCByName (@Context SecurityContext securityContext,
             @PathParam("createdBy") String createdBy,
-            @PathParam("vcName") String vcName) {
+            @PathParam("qName") String qName) {
         TokenContext context =
                 (TokenContext) securityContext.getUserPrincipal();
         try {
             scopeService.verifyScope(context, OAuth2Scope.DELETE_VC);
-            service.deleteVCByName(context.getUsername(), vcName, createdBy);
+            service.deleteQueryByName(context.getUsername(), qName, createdBy,
+                    QueryType.QUERY);
         }
         catch (KustvaktException e) {
         throw kustvaktResponseHandler.throwit(e);
         }
         return Response.ok().build();
     };
-    */
 
     
     /**
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
index 8f74282..eecb1b3 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
@@ -71,21 +71,27 @@
     private OAuth2ScopeService scopeService;
 
     /**
-     * Updates a vc according to the given VirtualCorpusJson, if the
-     * VC exists, otherwise creates a new VC with the given VC creator
-     * and VC name specified as the path parameters. The vc creator
-     * must be the same as the authenticated username.
+     * Creates a new VC with the given VC creator and VC name
+     * specified as the path parameters. If a VC with the same name
+     * and creator exists, the VC will be updated instead.
      * 
      * VC name cannot be updated.
      * 
+     * The VC creator must be the same as the authenticated username,
+     * except for admins. Admins can create or update system VC as
+     * well as VC for any users.
+     * 
+     * 
      * @param securityContext
      * @param vcCreator
      *            the username of the vc creator, must be the same
-     *            as the authenticated username
+     *            as the authenticated username, except admins
      * @param vcName
-     *           the vc name
-     * @param vc a json object describing the VC
-     * @return
+     *            the vc name
+     * @param vc
+     *            a json object describing the VC
+     * @return HTTP Status 201 Created when creating a new VC, or 204
+     *         No Content when updating an existing VC.
      * @throws KustvaktException
      */
     @PUT
@@ -259,7 +265,8 @@
                 (TokenContext) securityContext.getUserPrincipal();
         try {
             scopeService.verifyScope(context, OAuth2Scope.DELETE_VC);
-            service.deleteQueryByName(context.getUsername(), vcName, createdBy);
+            service.deleteQueryByName(context.getUsername(), vcName, createdBy,
+                    QueryType.VIRTUAL_CORPUS);
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
diff --git a/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java b/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
index 529efbd..1185bae 100644
--- a/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
@@ -1,25 +1,25 @@
 package de.ids_mannheim.korap.service;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import java.util.List;
 
-import org.junit.Rule;
+import org.junit.Assert;
 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 de.ids_mannheim.korap.constant.QueryType;
-import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.dto.QueryAccessDto;
 import de.ids_mannheim.korap.dto.QueryDto;
-import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.web.input.QueryJson;
 
@@ -32,12 +32,9 @@
     @Autowired
     private UserGroupService groupService;
 
-    @Rule
-    public ExpectedException thrown = ExpectedException.none();
-
     @Test
     public void testCreateNonUniqueVC () throws KustvaktException {
-        thrown.expect(KustvaktException.class);
+        
         // EM: message differs depending on the database used
         // for testing. The message below is from sqlite.
         // thrown.expectMessage("A UNIQUE constraint failed "
@@ -48,7 +45,9 @@
         vc.setCorpusQuery("corpusSigle=GOE");
         vc.setType(ResourceType.PRIVATE);
         vc.setQueryType(QueryType.VIRTUAL_CORPUS);
-        vcService.storeQuery(vc, "dory-vc", "dory");
+        
+        Assert.assertThrows(KustvaktException.class,
+                () -> vcService.storeQuery(vc, "dory-vc", "dory"));
     }
 
     @Test
@@ -77,16 +76,18 @@
         assertEquals(UserGroupStatus.HIDDEN, group.getStatus());
 
         //delete vc
-        vcService.deleteQueryByName(username, vcName, username);
+        vcService.deleteQueryByName(username, vcName, username,
+                QueryType.VIRTUAL_CORPUS);
         
         // check hidden access
         accesses = vcService.listQueryAccessByUsername("admin");
         assertEquals(size-1, accesses.size());
         
         // check hidden group
-        thrown.expect(KustvaktException.class);
-        group = groupService.retrieveUserGroupById(groupId);
-        thrown.expectMessage("Group with id "+groupId+" is not found");
+        KustvaktException e = assertThrows(KustvaktException.class,
+                () -> groupService.retrieveUserGroupById(groupId));
+        assertEquals("Group with id " + groupId + " is not found",
+                e.getMessage());
     }
 
     @Test
@@ -138,9 +139,10 @@
         accesses = vcService.listQueryAccessByUsername("admin");
         assertEquals(size - 1, accesses.size());
 
-        thrown.expect(KustvaktException.class);
-        group = groupService.retrieveUserGroupById(groupId);
-        thrown.expectMessage("Group with id 5 is not found");
+        KustvaktException e = assertThrows(KustvaktException.class,
+                () -> groupService.retrieveUserGroupById(groupId));
+        
+        assertEquals("Group with id 5 is not found", e.getMessage());
     }
 
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/QueryReferenceControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/QueryReferenceControllerTest.java
index de5b2da..c2279fe 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/QueryReferenceControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/QueryReferenceControllerTest.java
@@ -17,17 +17,21 @@
 import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.constant.ResourceType;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.user.User.CorpusAccess;
 import de.ids_mannheim.korap.utils.JsonUtils;
 
 public class QueryReferenceControllerTest extends SpringJerseyTest {
 
     private String testUser = "qRefControllerTest";
+    private String adminUser = "admin";
 
     @Test
     public void testCreatePrivateQuery () throws KustvaktException {
-        String json = "{\"type\": \"PRIVATE\"" + ",\"queryType\": \"QUERY\""
-                + ",\"queryLanguage\": \"poliqarp\"" + ",\"query\": \"der\"}";
+        String json = "{\"type\": \"PRIVATE\"" 
+                + ",\"queryType\": \"QUERY\""
+                + ",\"queryLanguage\": \"poliqarp\"" 
+                + ",\"query\": \"der\"}";
 
         String qName = "new_query";
         ClientResponse response = resource().path(API_VERSION).path("query")
@@ -48,6 +52,109 @@
         assertEquals("der", node.at("/query").asText());
         assertEquals("poliqarp", node.at("/queryLanguage").asText());
         assertEquals(CorpusAccess.PUB.name(), node.at("/requiredAccess").asText());
+        
+        testDeleteQueryByName(qName, testUser);
+    }
+
+    @Test
+    public void testCreateUserQueryByAdmin () throws KustvaktException {
+        String json = "{\"type\": \"PRIVATE\""
+                + ",\"queryType\": \"QUERY\""
+                + ",\"queryLanguage\": \"poliqarp\"" 
+                + ",\"query\": \"Sommer\"}";
+
+        String qName = "marlin-query";
+        ClientResponse response = resource().path(API_VERSION).path("query")
+                .path("~marlin").path(qName)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(adminUser, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).put(ClientResponse.class);
+
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        testDeleteQueryByName(qName, "admin");
+    }
+    
+    @Test
+    public void testCreateSystemQuery () throws KustvaktException {
+        String json = "{\"type\": \"SYSTEM\""
+                + ",\"queryType\": \"QUERY\""
+                + ",\"queryLanguage\": \"poliqarp\"" 
+                + ",\"query\": \"Sommer\"}";
+
+        String qName = "system-query";
+        ClientResponse response = resource().path(API_VERSION).path("query")
+                .path("~system").path(qName)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(adminUser, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).put(ClientResponse.class);
+
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        testDeleteQueryByName(qName, "admin");
+    }
+    
+    @Test
+    public void testCreateSystemQueryUnauthorized () throws KustvaktException {
+        String json = "{\"type\": \"SYSTEM\""
+                + ",\"queryType\": \"QUERY\""
+                + ",\"queryLanguage\": \"poliqarp\"" 
+                + ",\"query\": \"Sommer\"}";
+
+        ClientResponse response = resource().path(API_VERSION).path("query")
+                .path("~"+testUser).path("system-query")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(testUser, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).put(ClientResponse.class);
+
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Unauthorized operation for user: " + testUser,
+                node.at("/errors/0/1").asText());
+    }
+    
+    @Test
+    public void testDeleteQueryUnauthorized () throws KustvaktException {
+        ClientResponse response = resource().path(API_VERSION).path("query")
+                .path("~dory").path("dory-q")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(testUser, "pass"))
+                .delete(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Unauthorized operation for user: " + testUser,
+                node.at("/errors/0/1").asText());
+    }
+    
+    @Test
+    public void testDeleteNonExistingQuery () throws KustvaktException {
+        ClientResponse response = resource().path(API_VERSION).path("query")
+                .path("~dory").path("non-existing-query")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("dory", "pass"))
+                .delete(ClientResponse.class);
+
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Query dory/non-existing-query is not found.",
+                node.at("/errors/0/1").asText());
+        assertEquals("dory/non-existing-query",
+                node.at("/errors/0/2").asText());
     }
 
     @Test
@@ -106,4 +213,15 @@
         return JsonUtils.readTree(entity);
     }
 
+    private void testDeleteQueryByName (String qName, String username)
+            throws KustvaktException {
+        ClientResponse response = resource().path(API_VERSION).path("query")
+                .path("~" + username).path(qName)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .delete(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
index 4b85b4b..3286c51 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
@@ -455,6 +455,24 @@
     public void testCreateSystemVC () throws KustvaktException {
         String json = "{\"type\": \"SYSTEM\""
                 + ",\"queryType\": \"VIRTUAL_CORPUS\""
+                + ",\"corpusQuery\": \"pubDate since 1820\"}";
+
+        String vcName = "new_system_vc";
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("~system").path(vcName)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("admin", "pass"))
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).put(ClientResponse.class);
+
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        testDeleteVC(vcName, "admin");
+    }        
+    
+    @Test
+    public void testCreateSystemVCUnauthorized () throws KustvaktException {
+        String json = "{\"type\": \"SYSTEM\""
+                + ",\"queryType\": \"VIRTUAL_CORPUS\""
                 + ",\"corpusQuery\": \"creationDate since 1820\"}";
 
         ClientResponse response = resource().path(API_VERSION).path("vc")