Reactivate Query Serializer Web-service (close #903)

Change-Id: Ib0a88dccfa6362cb7d3d7946d857b481631e022f
diff --git a/Changes b/Changes
index 3a0ad2e..6b476fc 100644
--- a/Changes
+++ b/Changes
@@ -1,4 +1,6 @@
-# version 1.2
+# version 1.2-SNAPSHOT
+
+- Reactivate Query Serializer Web-service (#903)
 
 # version 1.1
 
diff --git a/src/main/java/de/ids_mannheim/korap/core/service/BasicService.java b/src/main/java/de/ids_mannheim/korap/core/service/BasicService.java
index 129a392..39ba569 100644
--- a/src/main/java/de/ids_mannheim/korap/core/service/BasicService.java
+++ b/src/main/java/de/ids_mannheim/korap/core/service/BasicService.java
@@ -38,7 +38,7 @@
         return combinedCorpusQuery;
     }
     
-    protected User createUser (String username, HttpHeaders headers)
+    public User createUser (String username, HttpHeaders headers)
             throws KustvaktException {
         User user = authenticationManager.getUser(username);
         authenticationManager.setAccessAndLocation(user, headers);
diff --git a/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java b/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
index b2857f2..a33badd 100644
--- a/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
+++ b/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
@@ -78,56 +78,7 @@
     }
 
     @SuppressWarnings("unchecked")
-    public String serializeQuery (String q, String ql, String v, String cq,
-            Integer pageIndex, Integer startPage, Integer pageLength,
-            String context, Boolean cutoff, boolean accessRewriteDisabled,
-            double apiVersion)
-            throws KustvaktException {
-        QuerySerializer ss = new QuerySerializer(apiVersion).setQuery(q, ql, v);
-        if (cq != null)
-            ss.setCollection(cq);
-
-        MetaQueryBuilder meta = new MetaQueryBuilder();
-        if (pageIndex != null)
-            meta.addEntry("startIndex", pageIndex);
-        if (pageIndex == null && startPage != null)
-            meta.addEntry("startPage", startPage);
-        if (pageLength != null)
-            meta.addEntry("count", pageLength);
-        if (context != null)
-            meta.setSpanContext(context);
-        meta.addEntry("cutOff", cutoff);
-
-        ss.setMeta(meta.raw());
-        // return ss.toJSON();
-
-        String query = ss.toJSON();
-        query = rewriteHandler.processQuery(ss.toJSON(), null, apiVersion);
-        return query;
-    }
-
-    public String search (String jsonld, String username, HttpHeaders headers, 
-    		double apiVersion)
-            throws KustvaktException {
-
-        User user = createUser(username, headers);
-
-        JsonNode node = JsonUtils.readTree(jsonld);
-        node = node.at("/meta/snippets");
-        if (node != null && node.asBoolean()) {
-            user.setCorpusAccess(CorpusAccess.ALL);
-        }
-
-        String query = this.rewriteHandler.processQuery(jsonld, user, apiVersion);
-        // MH: todo: should be possible to add the meta part to
-        // the query serialization
-        // User user = controller.getUser(ctx.getUsername());
-        // jsonld = this.processor.processQuery(jsonld, user);
-        return searchKrill.search(query);
-    }
-
-    @SuppressWarnings("unchecked")
-	public String search (double apiVersion, String engine, 
+	public String serializeQuery (User user, double apiVersion, String engine, 
 			String username, HttpHeaders headers,
 			String q, String ql, String v, List<String> cqList, String fields,
 			String pipes, String responsePipes, Integer pageIndex,
@@ -139,8 +90,7 @@
             throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
                     "page must start from 1", "page");
         }
-
-        User user = createUser(username, headers);
+        
         CorpusAccess corpusAccess = user.getCorpusAccess();
 
         // EM: TODO: check if requested fields are public metadata. Currently 
@@ -175,48 +125,88 @@
             query = addWarning(query, StatusCodes.NOT_ALLOWED,
                     "Tokens cannot be shown without access.");
         }
+    	
+        query = rewriteHandler.processQuery(query, user, apiVersion);
+        return query;
+    }
 
-        // Query pipe rewrite
-        query = runPipes(query, pipes);
+    public String search (String jsonld, String username, HttpHeaders headers, 
+    		double apiVersion)
+            throws KustvaktException {
 
-        query = this.rewriteHandler.processQuery(query, user, apiVersion);
-        if (DEBUG) {
-            jlog.debug("the serialized query " + query);
+        User user = createUser(username, headers);
+
+        JsonNode node = JsonUtils.readTree(jsonld);
+        node = node.at("/meta/snippets");
+        if (node != null && node.asBoolean()) {
+            user.setCorpusAccess(CorpusAccess.ALL);
         }
 
-        int hashedKoralQuery = createTotalResultCacheKey(query);
-        boolean hasCutOff = hasCutOff(query);
-        if (config.isTotalResultCacheEnabled() && !hasCutOff) {
-            query = precheckTotalResultCache(hashedKoralQuery, query);
-        }
-        
-        KustvaktConfiguration.BACKENDS searchEngine = this.config
-                .chooseBackend(engine);
-        String result;
-        if (searchEngine.equals(KustvaktConfiguration.BACKENDS.NEO4J)) {
-            result = searchNeo4J(query, pageLength, meta, false);
-        }
-        else if (searchEngine.equals(KustvaktConfiguration.BACKENDS.NETWORK)) {
-            result = searchNetwork.search(query);
-        }
-        else {
-            result = searchKrill.search(query);
-        }
-        // jlog.debug("Query result: " + result);
-        
-        if (config.isTotalResultCacheEnabled()) {
-            result = afterCheckTotalResultCache(hashedKoralQuery, result);
-        }
-        
-        if (!hasCutOff) {
-            result = removeCutOff(result);
-        }
-        
-        checkApiVersion(result, apiVersion);
-        
-        // Response pipe rewrite
-        result = runPipes(result, responsePipes);
-        return result;
+        String query = this.rewriteHandler.processQuery(jsonld, user, apiVersion);
+        // MH: todo: should be possible to add the meta part to
+        // the query serialization
+        // User user = controller.getUser(ctx.getUsername());
+        // jsonld = this.processor.processQuery(jsonld, user);
+        return searchKrill.search(query);
+    }
+
+	public String search (double apiVersion, String engine, 
+			String username, HttpHeaders headers,
+			String q, String ql, String v, List<String> cqList, String fields,
+			String pipes, String responsePipes, Integer pageIndex,
+			Integer pageInteger, String ctx, Integer pageLength, Boolean cutoff,
+			boolean accessRewriteDisabled, boolean showTokens,
+			boolean showSnippet) throws KustvaktException {
+    	
+    	User user = createUser(username, headers);
+    	
+		String query = serializeQuery(user, apiVersion, engine, username, headers, q,
+				ql, v, cqList, fields, pipes, responsePipes, pageIndex,
+				pageInteger, ctx, pageLength, cutoff, accessRewriteDisabled,
+				showTokens, showSnippet);
+		// Query pipe rewrite
+		query = runPipes(query, pipes);
+
+		query = this.rewriteHandler.processQuery(query, user, apiVersion);
+		if (DEBUG) {
+			jlog.debug("the serialized query " + query);
+		}
+
+		int hashedKoralQuery = createTotalResultCacheKey(query);
+		boolean hasCutOff = hasCutOff(query);
+		if (config.isTotalResultCacheEnabled() && !hasCutOff) {
+			query = precheckTotalResultCache(hashedKoralQuery, query);
+		}
+
+		KustvaktConfiguration.BACKENDS searchEngine = this.config
+				.chooseBackend(engine);
+		String result;
+		// EM: disable Neo4J
+		//        if (searchEngine.equals(KustvaktConfiguration.BACKENDS.NEO4J)) {
+		//            result = searchNeo4J(query, pageLength, meta, false);
+		//        }
+		//        else 
+		if (searchEngine.equals(KustvaktConfiguration.BACKENDS.NETWORK)) {
+			result = searchNetwork.search(query);
+		}
+		else {
+			result = searchKrill.search(query);
+		}
+		// jlog.debug("Query result: " + result);
+
+		if (config.isTotalResultCacheEnabled()) {
+			result = afterCheckTotalResultCache(hashedKoralQuery, result);
+		}
+
+		if (!hasCutOff) {
+			result = removeCutOff(result);
+		}
+
+		checkApiVersion(result, apiVersion);
+
+		// Response pipe rewrite
+		result = runPipes(result, responsePipes);
+		return result;
 
     }
     
diff --git a/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java b/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
index a5903f1..45010a7 100644
--- a/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
+++ b/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
@@ -21,6 +21,7 @@
 import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
 import de.ids_mannheim.korap.security.context.TokenContext;
 import de.ids_mannheim.korap.server.KustvaktBaseServer;
+import de.ids_mannheim.korap.user.User;
 import de.ids_mannheim.korap.utils.JsonUtils;
 import de.ids_mannheim.korap.utils.ServiceInfo;
 import de.ids_mannheim.korap.web.KustvaktResponseHandler;
@@ -130,43 +131,54 @@
     //         scope without searching etc. In case not, it helps to compare queries in 
     //         different query languages.
     //     MH: ref query parameter removed!
-    //    @GET
-    //    @Path("{version}/query")
-    //    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
-    public Response serializeQuery (@Context Locale locale,
-            @Context SecurityContext securityContext, @QueryParam("q") String q,
-            @Context ContainerRequestContext requestContext,
+    @GET
+    @Path("{version}/serialize")
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response serializeQuery (@Context SecurityContext securityContext,
+    		@Context ContainerRequestContext requestContext,
+            @Context HttpServletRequest request, @Context HttpHeaders headers,
+            @Context Locale locale, @QueryParam("q") String q,
             @QueryParam("ql") String ql, @QueryParam("v") String v,
-            @QueryParam("context") String context,
+            @QueryParam("context") String ctx,
             @QueryParam("cutoff") Boolean cutoff,
             @QueryParam("count") Integer pageLength,
             @QueryParam("offset") Integer pageIndex,
-            @QueryParam("page") Integer startPage,
+            @QueryParam("page") Integer pageInteger,
+            @QueryParam("fields") String fields,
+            @QueryParam("pipes") String pipes,
+            @QueryParam("response-pipes") String responsePipes,
             @QueryParam("access-rewrite-disabled") boolean accessRewriteDisabled,
-            @QueryParam("cq") String cq) {
-    	
+            @QueryParam("show-tokens") boolean showTokens,
+            @DefaultValue("true") @QueryParam("show-snippet") boolean showSnippet,
+            @QueryParam("cq") List<String> cq,
+            @QueryParam("engine") String engine) {
+
     	List<PathSegment> pathSegments = requestContext.getUriInfo()
     			.getPathSegments();
         String version = pathSegments.get(0).getPath();
-        double apiVersion = Double.parseDouble(version.substring(1));
+        double requestedVersion = Double.parseDouble(version.substring(1));
         
-        TokenContext ctx = (TokenContext) securityContext.getUserPrincipal();
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+
+        String result;
         try {
-            scopeService.verifyScope(ctx, OAuth2Scope.SERIALIZE_QUERY);
-            String result = searchService.serializeQuery(q, ql, v, cq,
-                    pageIndex, startPage, pageLength, context, cutoff,
-                    accessRewriteDisabled, apiVersion);
-            if (DEBUG) {
-                jlog.debug("Query: " + result);
-            }
-            return Response.ok(result).build();
+            scopeService.verifyScope(context, OAuth2Scope.SEARCH);
+            String username = context.getUsername();
+            User user = searchService.createUser(username, headers);
+            result = searchService.serializeQuery(user, requestedVersion, 
+            		engine, username,
+                    headers, q, ql, v, cq, fields, pipes, responsePipes, 
+                    pageIndex, pageInteger, ctx, pageLength, cutoff, 
+                    accessRewriteDisabled, showTokens, showSnippet);
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
         }
+
+        return Response.ok(result).build();
     }
 
-    //    This web service is DISABLED until there is a need for it. 
     @POST
     @Path("{version}/search")
     @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/QuerySerializationControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/QuerySerializationControllerTest.java
index b795936..9c2a071 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/QuerySerializationControllerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/QuerySerializationControllerTest.java
@@ -14,82 +14,436 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
 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.utils.JsonUtils;
+import de.ids_mannheim.korap.web.controller.oauth2.OAuth2TestBase;
 
-// EM: The API is disabled
-@Disabled
-public class QuerySerializationControllerTest extends SpringJerseyTest {
+public class QuerySerializationControllerTest extends OAuth2TestBase {
 
     @Test
-    public void testQuerySerializationFilteredPublic ()
+    public void testQuerySerializationWithCorpusQuery ()
             throws KustvaktException {
         Response response = target().path(API_VERSION)
-                .path("corpus/WPD13/query").queryParam("q", "[orth=der]")
+                .path("serialize").queryParam("q", "[orth=der]")
                 .queryParam("ql", "poliqarp").queryParam("context", "base/s:s")
+                .queryParam("cq", "corpusSigle=WPD13")
                 .request().method("GET");
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String ent = response.readEntity(String.class);
         JsonNode node = JsonUtils.readTree(ent);
+        
+        String expectedJson = """
+        {
+          "meta" : {
+            "context" : "base/s:s",
+            "tokens" : false,
+            "snippets" : true,
+            "timeout" : 10000
+          },
+          "query" : {
+            "@type" : "koral:token",
+            "wrap" : {
+              "@type" : "koral:term",
+              "match" : "match:eq",
+              "key" : "der",
+              "layer" : "orth",
+              "foundry" : "opennlp",
+              "rewrites" : [ {
+                "@type" : "koral:rewrite",
+                "src" : "Kustvakt",
+                "editor" : "Kustvakt",
+                "operation" : "operation:injection",
+                "scope" : "foundry",
+                "_comment" : "Default foundry has been added."
+              } ]
+            }
+          },
+          "corpus" : {
+            "@type" : "koral:docGroup",
+            "operation" : "operation:and",
+            "operands" : [ {
+              "@type" : "koral:doc",
+              "match" : "match:eq",
+              "type" : "type:regex",
+              "value" : "CC.*",
+              "key" : "availability"
+            }, {
+              "@type" : "koral:doc",
+              "match" : "match:eq",
+              "value" : "WPD13",
+              "key" : "corpusSigle"
+            } ],
+            "rewrites" : [ {
+              "@type" : "koral:rewrite",
+              "src" : "Kustvakt",
+              "editor" : "Kustvakt",
+              "operation" : "operation:override",
+              "original" : {
+                "@type" : "koral:doc",
+                "match" : "match:eq",
+                "value" : "WPD13",
+                "key" : "corpusSigle"
+              },
+              "_comment" : "Free corpus access policy has been added."
+            } ]
+          },
+          "@context" : "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld"
+        }
+        """;
+        
+        JsonNode expectedNode = JsonUtils.readTree(expectedJson);
+        
         assertNotNull(node);
-        assertEquals("corpusSigle", node.at(CORPUS_PATH+"/key").asText());
-        assertEquals("WPD13", node.at(CORPUS_PATH+"/value").asText());
+        assertEquals(expectedNode, node);
     }
 
-    @Test
-    public void testQuerySerializationUnexistingResource ()
-            throws KustvaktException {
-        Response response = target().path(API_VERSION)
-                .path("corpus/ZUW19/query").queryParam("q", "[orth=der]")
-                .queryParam("ql", "poliqarp").queryParam("context", "base/s:s")
-                .request().method("GET");
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String ent = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(ent);
-        assertEquals(101, node.at("/errors/0/0").asInt());
-        assertEquals("[Cannot found public Corpus with ids: [ZUW19]]",
-            node.at("/errors/0/2").asText());
-    }
 
     @Test
-    public void testQuerySerializationWithNonPublicCorpus ()
+    public void testQuerySerializationWithPublicAccess ()
             throws KustvaktException {
+        Response tokenResponse = requestTokenWithDoryPassword(superClientId, 
+        		clientSecret);
+        String tokenResponseEntity = tokenResponse.readEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), tokenResponse.getStatus());
+        
+        JsonNode tokenNode = JsonUtils.readTree(tokenResponseEntity);
+        String accessToken = tokenNode.at("/access_token").asText();
+        assertNotNull(accessToken);
+        
         Response response = target().path(API_VERSION)
-                .path("corpus/BRZ10/query").queryParam("q", "[orth=der]")
-                .queryParam("ql", "poliqarp").queryParam("context", "base/s:s")
-                .request().method("GET");
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String ent = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(ent);
-        assertEquals(101, node.at("/errors/0/0").asInt());
-        assertEquals("[Cannot found public Corpus with ids: [BRZ10]]",
-            node.at("/errors/0/2").asText());
-    }
-
-    @Test
-    public void testQuerySerializationWithAuthentication ()
-            throws KustvaktException {
-        Response response = target().path(API_VERSION)
-                .path("corpus/BRZ10/query").queryParam("q", "[orth=der]")
-                .queryParam("ql", "poliqarp").request()
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue("kustvakt",
-                                        "kustvakt2015"))
+                .path("serialize").queryParam("q", "[orth=der]")
+                .queryParam("ql", "poliqarp")
+                .queryParam("cq", "corpusSigle=BRZ10")
+                .request()
+                .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
+                // EM: without X-Forwarded-For header, the request is only granted 
+                // free access, see KustvaktAuthenticationManager
+                // .setAccessAndLocation(User, HttpHeaders)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .method("GET");
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String ent = response.readEntity(String.class);
         JsonNode node = JsonUtils.readTree(ent);
+
+        String expectedJson = """
+    	{
+          "meta" : {
+            "snippets" : true,
+            "tokens" : false,
+            "timeout" : 90000
+          },
+          "query" : {
+            "@type" : "koral:token",
+            "wrap" : {
+              "@type" : "koral:term",
+              "match" : "match:eq",
+              "key" : "der",
+              "layer" : "orth",
+              "foundry" : "opennlp",
+              "rewrites" : [ {
+                "@type" : "koral:rewrite",
+                "src" : "Kustvakt",
+                "editor" : "Kustvakt",
+                "operation" : "operation:injection",
+                "scope" : "foundry",
+                "_comment" : "Default foundry has been added."
+              } ]
+            }
+          },
+          "corpus" : {
+            "@type" : "koral:docGroup",
+            "operation" : "operation:and",
+            "operands" : [ {
+              "operands" : [ {
+                "@type" : "koral:doc",
+                "match" : "match:eq",
+                "type" : "type:regex",
+                "value" : "CC.*",
+                "key" : "availability"
+              }, {
+                "operands" : [ {
+                  "@type" : "koral:doc",
+                  "match" : "match:eq",
+                  "type" : "type:regex",
+                  "value" : "ACA.*",
+                  "key" : "availability"
+                }, {
+                  "@type" : "koral:doc",
+                  "match" : "match:eq",
+                  "type" : "type:regex",
+                  "value" : "QAO-NC",
+                  "key" : "availability"
+                } ],
+                "@type" : "koral:docGroup",
+                "operation" : "operation:or"
+              } ],
+              "@type" : "koral:docGroup",
+              "operation" : "operation:or"
+            }, {
+              "@type" : "koral:doc",
+              "match" : "match:eq",
+              "value" : "BRZ10",
+              "key" : "corpusSigle"
+            } ],
+            "rewrites" : [ {
+              "@type" : "koral:rewrite",
+              "src" : "Kustvakt",
+              "editor" : "Kustvakt",
+              "operation" : "operation:override",
+              "original" : {
+                "@type" : "koral:doc",
+                "match" : "match:eq",
+                "value" : "BRZ10",
+                "key" : "corpusSigle"
+              },
+              "_comment" : "Public corpus access policy has been added."
+            } ]
+          },
+          "@context" : "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld"
+        }
+
+        	""";
+        JsonNode expectedNode = JsonUtils.readTree(expectedJson);
         assertNotNull(node);
-        assertEquals("koral:doc", node.at(CORPUS_PATH+"/@type").asText());
-        assertEquals("corpusSigle", node.at(CORPUS_PATH+"/key").asText());
-        assertEquals("BRZ10", node.at(CORPUS_PATH+"/value").asText());
+        assertEquals(expectedNode, node);
+        // Clean up
+        revokeToken(accessToken, superClientId, clientSecret, ACCESS_TOKEN_TYPE);
     }
 
     @Test
+    public void testQuerySerializationWithAllAccess ()
+            throws KustvaktException {
+        Response tokenResponse = requestTokenWithDoryPassword(superClientId, 
+        		clientSecret);
+        String tokenResponseEntity = tokenResponse.readEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), tokenResponse.getStatus());
+        
+        JsonNode tokenNode = JsonUtils.readTree(tokenResponseEntity);
+        String accessToken = tokenNode.at("/access_token").asText();
+        assertNotNull(accessToken);
+        
+        Response response = target().path(API_VERSION)
+                .path("serialize").queryParam("q", "[orth=der]")
+                .queryParam("ql", "poliqarp")
+                .queryParam("cq", "corpusSigle=BRZ10")
+                .request()
+                .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
+                .header(HttpHeaders.X_FORWARDED_FOR, "10.7.0.15")
+                .method("GET");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String ent = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(ent);
+
+        String expectedJson = """
+    	{
+          "meta" : {
+            "snippets" : true,
+            "tokens" : false,
+            "timeout" : 90000
+          },
+          "query" : {
+            "@type" : "koral:token",
+            "wrap" : {
+              "@type" : "koral:term",
+              "match" : "match:eq",
+              "key" : "der",
+              "layer" : "orth",
+              "foundry" : "opennlp",
+              "rewrites" : [ {
+                "@type" : "koral:rewrite",
+                "src" : "Kustvakt",
+                "editor" : "Kustvakt",
+                "operation" : "operation:injection",
+                "scope" : "foundry",
+                "_comment" : "Default foundry has been added."
+              } ]
+            }
+          },
+          "corpus" : {
+            "@type" : "koral:docGroup",
+            "operation" : "operation:and",
+            "operands" : [ {
+              "operands" : [ {
+                "@type" : "koral:doc",
+                "match" : "match:eq",
+                "type" : "type:regex",
+                "value" : "CC.*",
+                "key" : "availability"
+              }, {
+                "operands" : [ {
+                  "@type" : "koral:doc",
+                  "match" : "match:eq",
+                  "type" : "type:regex",
+                  "value" : "ACA.*",
+                  "key" : "availability"
+                }, {
+                  "operands" : [ {
+                    "@type" : "koral:doc",
+                    "match" : "match:eq",
+                    "type" : "type:regex",
+                    "value" : "QAO-NC",
+                    "key" : "availability"
+                  }, {
+                    "@type" : "koral:doc",
+                    "match" : "match:eq",
+                    "type" : "type:regex",
+                    "value" : "QAO-NC-LOC:ids.*",
+                    "key" : "availability"
+                  } ],
+                  "@type" : "koral:docGroup",
+                  "operation" : "operation:or"
+                } ],
+                "@type" : "koral:docGroup",
+                "operation" : "operation:or"
+              } ],
+              "@type" : "koral:docGroup",
+              "operation" : "operation:or"
+            }, {
+              "@type" : "koral:doc",
+              "match" : "match:eq",
+              "value" : "BRZ10",
+              "key" : "corpusSigle"
+            } ],
+            "rewrites" : [ {
+              "@type" : "koral:rewrite",
+              "src" : "Kustvakt",
+              "editor" : "Kustvakt",
+              "operation" : "operation:override",
+              "original" : {
+                "@type" : "koral:doc",
+                "match" : "match:eq",
+                "value" : "BRZ10",
+                "key" : "corpusSigle"
+              },
+              "_comment" : "All corpus access policy has been added."
+            } ]
+          },
+          "@context" : "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld"
+        }
+
+        	""";
+        JsonNode expectedNode = JsonUtils.readTree(expectedJson);
+        assertNotNull(node);
+        assertEquals(expectedNode, node);
+        // Clean up
+        revokeToken(accessToken, superClientId, clientSecret, ACCESS_TOKEN_TYPE);
+    }
+    
+    @Test
+    public void testQuerySerializationWithVCRef ()
+            throws KustvaktException {
+            Response response = target().path(API_VERSION)
+                    .path("serialize").queryParam("q", "[orth=der]")
+                    .queryParam("ql", "poliqarp").queryParam("context", "base/s:s")
+                    .queryParam("cq", "referTo system-vc")
+                    .request().method("GET");
+            assertEquals(Status.OK.getStatusCode(), response.getStatus());
+            String ent = response.readEntity(String.class);
+            JsonNode node = JsonUtils.readTree(ent);
+            
+            String expectedJson = """
+        	{
+              "meta" : {
+                "context" : "base/s:s",
+                "tokens" : false,
+                "snippets" : true,
+                "timeout" : 10000
+              },
+              "query" : {
+                "@type" : "koral:token",
+                "wrap" : {
+                  "@type" : "koral:term",
+                  "match" : "match:eq",
+                  "key" : "der",
+                  "layer" : "orth",
+                  "foundry" : "opennlp",
+                  "rewrites" : [ {
+                    "@type" : "koral:rewrite",
+                    "src" : "Kustvakt",
+                    "editor" : "Kustvakt",
+                    "operation" : "operation:injection",
+                    "scope" : "foundry",
+                    "_comment" : "Default foundry has been added."
+                  } ]
+                }
+              },
+              "corpus" : {
+                "@type" : "koral:docGroup",
+                "operation" : "operation:and",
+                "operands" : [ {
+                  "@type" : "koral:doc",
+                  "match" : "match:eq",
+                  "type" : "type:regex",
+                  "value" : "CC.*",
+                  "key" : "availability"
+                }, {
+                  "ref" : "system-vc",
+                  "@type" : "koral:docGroupRef"
+                } ],
+                "rewrites" : [ {
+                  "@type" : "koral:rewrite",
+                  "src" : "Kustvakt",
+                  "editor" : "Kustvakt",
+                  "operation" : "operation:override",
+                  "original" : {
+                    "ref" : "system-vc",
+                    "@type" : "koral:docGroupRef"
+                  },
+                  "_comment" : "Free corpus access policy has been added."
+                } ]
+              },
+              "@context" : "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld"
+            }
+            	""";
+            
+            JsonNode expectedNode = JsonUtils.readTree(expectedJson);
+            assertNotNull(node);
+            assertEquals(expectedNode, node);
+    }
+
+    @Test
+    public void testMetaQuerySerialization () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("serialize")
+                .queryParam("context", "sentence").queryParam("count", "20")
+                .queryParam("page", "5").queryParam("cutoff", "true")
+                .queryParam("q", "[pos=ADJA]").queryParam("ql", "poliqarp")
+                .request().method("GET");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String ent = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(ent);
+        assertEquals("sentence", node.at("/meta/context").asText());
+        assertEquals(20, node.at("/meta/count").asInt());
+        assertEquals(5, node.at("/meta/startPage").asInt());
+        assertEquals(true, node.at("/meta/cutOff").asBoolean());
+        assertEquals("koral:term", node.at("/query/wrap/@type").asText());
+        assertEquals("pos", node.at("/query/wrap/layer").asText());
+        assertEquals("match:eq", node.at("/query/wrap/match").asText());
+        assertEquals("ADJA", node.at("/query/wrap/key").asText());
+    }
+
+    @Test
+    public void testMetaQuerySerializationWithOffset ()
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("serialize")
+                .queryParam("context", "sentence").queryParam("count", "20")
+                .queryParam("page", "5").queryParam("offset", "2")
+                .queryParam("cutoff", "true").queryParam("q", "[pos=ADJA]")
+                .queryParam("ql", "poliqarp").request().method("GET");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String ent = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(ent);
+        assertEquals("sentence", node.at("/meta/context").asText());
+        assertEquals(20, node.at("/meta/count").asInt());
+        assertEquals(2, node.at("/meta/startIndex").asInt());
+        assertEquals(true, node.at("/meta/cutOff").asBoolean());
+    }
+    
+    @Disabled("outdated")
+    @Test
     public void testQuerySerializationWithNewCollection ()
             throws KustvaktException {
         // Add Virtual Collection
@@ -133,7 +487,7 @@
         assertFalse(id.isEmpty());
         // query serialization service
         response = target().path(API_VERSION).path("collection").path(id)
-                .path("query").queryParam("q", "[orth=der]")
+                .path("serialize").queryParam("q", "[orth=der]")
                 .queryParam("ql", "poliqarp").queryParam("context", "base/s:s")
                 .request()
                 .header(Attributes.AUTHORIZATION,
@@ -164,65 +518,4 @@
         assertEquals("match:eq",
             node.at(CORPUS_PATH+"/operands/1/match").asText());
     }
-
-    @Test
-    public void testQuerySerializationOfVirtualCollection ()
-            throws KustvaktException {
-        Response response = target().path(API_VERSION)
-                .path("collection/GOE-VC/query").queryParam("q", "[orth=der]")
-                .queryParam("ql", "poliqarp").queryParam("context", "base/s:s")
-                .request().method("GET");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String ent = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(ent);
-        assertNotNull(node);
-        assertEquals("koral:doc",
-            node.at(CORPUS_PATH+"/operands/0/@type").asText());
-        assertEquals("corpusSigle",
-            node.at(CORPUS_PATH+"/operands/0/key").asText());
-        assertEquals("GOE", node.at(CORPUS_PATH+"/operands/0/value").asText());
-        assertEquals("koral:doc",
-            node.at(CORPUS_PATH+"/operands/1/@type").asText());
-        assertEquals("creationDate",
-            node.at(CORPUS_PATH+"/operands/1/key").asText());
-        assertEquals("1810-01-01",
-            node.at(CORPUS_PATH+"/operands/1/value").asText());
-    }
-
-    @Test
-    public void testMetaQuerySerialization () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("query")
-                .queryParam("context", "sentence").queryParam("count", "20")
-                .queryParam("page", "5").queryParam("cutoff", "true")
-                .queryParam("q", "[pos=ADJA]").queryParam("ql", "poliqarp")
-                .request().method("GET");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String ent = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(ent);
-        assertEquals("sentence", node.at("/meta/context").asText());
-        assertEquals(20, node.at("/meta/count").asInt());
-        assertEquals(5, node.at("/meta/startPage").asInt());
-        assertEquals(true, node.at("/meta/cutOff").asBoolean());
-        assertEquals("koral:term", node.at("/query/wrap/@type").asText());
-        assertEquals("pos", node.at("/query/wrap/layer").asText());
-        assertEquals("match:eq", node.at("/query/wrap/match").asText());
-        assertEquals("ADJA", node.at("/query/wrap/key").asText());
-    }
-
-    @Test
-    public void testMetaQuerySerializationWithOffset ()
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("query")
-                .queryParam("context", "sentence").queryParam("count", "20")
-                .queryParam("page", "5").queryParam("offset", "2")
-                .queryParam("cutoff", "true").queryParam("q", "[pos=ADJA]")
-                .queryParam("ql", "poliqarp").request().method("GET");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String ent = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(ent);
-        assertEquals("sentence", node.at("/meta/context").asText());
-        assertEquals(20, node.at("/meta/count").asInt());
-        assertEquals(2, node.at("/meta/startIndex").asInt());
-        assertEquals(true, node.at("/meta/cutOff").asBoolean());
-    }
 }