Renamed web.controller package in core

Moved SearchNetworkEndpoint to core.service package.

Change-Id: I746fcb20aa92ab3252cfc06dcce1f404ef955de3
diff --git a/core/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java b/core/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java
index 1f6abd7..4730a4f 100644
--- a/core/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java
+++ b/core/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java
@@ -9,13 +9,13 @@
 import org.springframework.stereotype.Service;
 
 import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.core.web.controller.AnnotationController;
 import de.ids_mannheim.korap.dao.AnnotationDao;
 import de.ids_mannheim.korap.dto.FoundryDto;
 import de.ids_mannheim.korap.dto.LayerDto;
 import de.ids_mannheim.korap.dto.converter.AnnotationConverter;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.web.controller.AnnotationController;
 
 /** AnnotationService defines the logic behind {@link AnnotationController}.
  * 
diff --git a/core/src/main/java/de/ids_mannheim/korap/core/service/SearchNetworkEndpoint.java b/core/src/main/java/de/ids_mannheim/korap/core/service/SearchNetworkEndpoint.java
new file mode 100644
index 0000000..de6038e
--- /dev/null
+++ b/core/src/main/java/de/ids_mannheim/korap/core/service/SearchNetworkEndpoint.java
@@ -0,0 +1,87 @@
+package de.ids_mannheim.korap.core.service;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.apache.http.HttpStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+
+@Service
+public class SearchNetworkEndpoint {
+
+    private final static Logger jlog = LogManager
+            .getLogger(SearchNetworkEndpoint.class);
+
+    @Autowired
+    private KustvaktConfiguration config;
+
+    public String search (String query) throws KustvaktException {
+        String networkEndpointURL = config.getNetworkEndpointURL();
+        if (networkEndpointURL == null || networkEndpointURL.isEmpty()) {
+            throw new KustvaktException(
+                    StatusCodes.NETWORK_ENDPOINT_NOT_AVAILABLE,
+                    "Network endpoint is not available");
+        }
+        else {
+            try {
+                URL url = new URL(networkEndpointURL);
+                HttpURLConnection connection = (HttpURLConnection) url
+                        .openConnection();
+                connection.setRequestMethod("POST");
+                connection.setRequestProperty("Content-Type",
+                        "application/json; charset=UTF-8");
+                connection.setRequestProperty("Accept", "application/json");
+                connection.setDoOutput(true);
+                OutputStream os = connection.getOutputStream();
+                byte[] input = query.getBytes("utf-8");
+                os.write(input, 0, input.length);
+
+                String entity = null;
+                if (connection.getResponseCode() == HttpStatus.SC_OK) {
+                    BufferedReader br = new BufferedReader(
+                            new InputStreamReader(connection.getInputStream(),
+                                    "utf-8"));
+                    StringBuilder response = new StringBuilder();
+                    String responseLine = null;
+                    while ((responseLine = br.readLine()) != null) {
+                        response.append(responseLine.trim());
+                    }
+                    entity = response.toString();
+                }
+
+                if (entity != null && !entity.isEmpty()) {
+                    return entity;
+                }
+                else {
+                    String message = connection.getResponseCode() + " "
+                            + connection.getResponseMessage();
+                    jlog.warn("Search on network endpoint failed "
+                            + networkEndpointURL + ". Message: " + message);
+
+                    throw new KustvaktException(
+                            StatusCodes.SEARCH_NETWORK_ENDPOINT_FAILED,
+                            "Failed searching at network endpoint: "
+                                    + networkEndpointURL,
+                            message);
+                }
+            }
+            catch (Exception e) {
+                throw new KustvaktException(
+                        StatusCodes.SEARCH_NETWORK_ENDPOINT_FAILED,
+                        "Failed searching at network endpoint: "
+                                + networkEndpointURL,
+                        e.getCause());
+            }
+        }
+    }
+}
diff --git a/core/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java b/core/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
index e6d2368..96b5f62 100644
--- a/core/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
+++ b/core/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
@@ -40,7 +40,6 @@
 import de.ids_mannheim.korap.utils.JsonUtils;
 import de.ids_mannheim.korap.web.ClientsHandler;
 import de.ids_mannheim.korap.web.SearchKrill;
-import de.ids_mannheim.korap.web.SearchNetworkEndpoint;
 
 @Service
 public class SearchService extends BasicService{
@@ -418,7 +417,7 @@
         User user = createUser(username, headers);
         Pattern p = determineAvailabilityPattern(user);
 
-        boolean match_only = foundries == null || foundries.isEmpty();
+//        boolean match_only = foundries == null || foundries.isEmpty();
         String results;
 //        try {
 
diff --git a/core/src/main/java/de/ids_mannheim/korap/core/web/controller/AnnotationController.java b/core/src/main/java/de/ids_mannheim/korap/core/web/controller/AnnotationController.java
new file mode 100644
index 0000000..26d51dc
--- /dev/null
+++ b/core/src/main/java/de/ids_mannheim/korap/core/web/controller/AnnotationController.java
@@ -0,0 +1,125 @@
+package de.ids_mannheim.korap.core.web.controller;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import de.ids_mannheim.korap.core.service.AnnotationService;
+import de.ids_mannheim.korap.dto.FoundryDto;
+import de.ids_mannheim.korap.dto.LayerDto;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.DemoUserFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+
+/**
+ * Provides services regarding annotation related information.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("/{version}/annotation/")
+@ResourceFilters({APIVersionFilter.class, DemoUserFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class AnnotationController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Autowired
+    private AnnotationService annotationService;
+
+    /**
+     * Returns information about all supported layers
+     * 
+     * @return a json serialization of all supported layers
+     */
+    @GET
+    @Path("layers")
+    public List<LayerDto> getLayers () {
+        return annotationService.getLayerDtos();
+    }
+
+
+    /**
+     * Returns a list of foundry descriptions.
+     * 
+     * @param codes
+     *            foundry-layer code or a Kleene-star
+     * @param language
+     *            2-letter language code (description language)
+     * @return a list of foundry, layer, value information in json
+     */
+    @SuppressWarnings("unchecked")
+    @POST
+    @Path("description")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public List<FoundryDto> getFoundryDescriptions (String json) {
+        if (json == null || json.isEmpty()) {
+            throw kustvaktResponseHandler
+                    .throwit(new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                            "Missing a json string.", ""));
+        }
+
+        JsonNode node;
+        try {
+            node = JsonUtils.readTree(json);
+        }
+        catch (KustvaktException e1) {
+            throw kustvaktResponseHandler.throwit(e1);
+        }
+
+        String language;
+        if (!node.has("language")) {
+            language = "en";
+        }
+        else {
+            language = node.get("language").asText();
+            if (language == null || language.isEmpty()) {
+                language = "en";
+            }
+            else if (!(language.equals("en") || language.equals("de"))) {
+                throw kustvaktResponseHandler.throwit(
+                        new KustvaktException(StatusCodes.UNSUPPORTED_VALUE,
+                                "Unsupported value:", language));
+            }
+        }
+
+        List<String> codes;
+        try {
+            codes = JsonUtils.convert(node.get("codes"), List.class);
+        }
+        catch (IOException | NullPointerException e) {
+            throw kustvaktResponseHandler.throwit(new KustvaktException(
+                    StatusCodes.INVALID_ARGUMENT, "Bad argument:", json));
+        }
+        if (codes == null || codes.isEmpty()) {
+            throw kustvaktResponseHandler.throwit(
+                    new KustvaktException(StatusCodes.MISSING_ATTRIBUTE,
+                            "codes is null or empty", "codes"));
+        }
+
+        try {
+            return annotationService.getFoundryDtos(codes, language);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+}
+
diff --git a/core/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java b/core/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
new file mode 100644
index 0000000..acddf23
--- /dev/null
+++ b/core/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
@@ -0,0 +1,490 @@
+package de.ids_mannheim.korap.core.web.controller;// package
+                                             // de.ids_mannheim.korap.ext.web;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import de.ids_mannheim.korap.web.utils.SearchResourceFilters;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.core.service.SearchService;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.utils.ServiceInfo;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AdminFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.DemoUserFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+
+/**
+ * 
+ * @author hanl, margaretha, diewald
+ * @date 29/01/2014
+ * @lastUpdate 05/07/2019
+ * 
+ */
+@Controller
+@Path("/")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        DemoUserFilter.class, PiwikFilter.class })
+public class SearchController {
+
+    private static final boolean DEBUG = false;
+
+    private static Logger jlog = LogManager.getLogger(SearchController.class);
+    private @Context ServletContext context;
+    
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Autowired
+    private SearchService searchService;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
+    private KustvaktConfiguration config;
+    
+    @GET
+    @Path("{version}")
+    public Response index (){
+        return Response
+            .ok(config.getApiWelcomeMessage())
+            .header("X-Index-Revision", searchService.getIndexFingerprint())
+            .build();
+    }
+    
+    @GET
+    @Path("{version}/info")
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response info (){
+        Map<String, Object> m = new HashMap<>();
+        m.put("latest_api_version", config.getCurrentVersion());
+        m.put("supported_api_versions", config.getSupportedVersions());
+        m.put("kustvakt_version", ServiceInfo.getInfo().getVersion());
+        m.put("krill_version", searchService.getKrillVersion());
+        m.put("koral_version", ServiceInfo.getInfo().getKoralVersion());
+        try {
+            return Response.ok(JsonUtils.toJSON(m)).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+    
+    @POST
+    @Path("{version}/index/close")
+    // overrides the whole filters
+    @ResourceFilters({APIVersionFilter.class,AdminFilter.class})
+    public Response closeIndexReader (){
+        try {
+            searchService.closeIndexReader();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+    
+    
+//     EM: This web service is DISABLED until there is a need for it.
+//     ND: In case rewrite is supported, it could be used to check the authorization 
+//         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,
+            @QueryParam("ql") String ql, @QueryParam("v") String v,
+            @QueryParam("context") String context,
+            @QueryParam("cutoff") Boolean cutoff,
+            @QueryParam("count") Integer pageLength,
+            @QueryParam("offset") Integer pageIndex,
+            @QueryParam("page") Integer startPage,
+            @QueryParam("access-rewrite-disabled") boolean accessRewriteDisabled,
+            @QueryParam("cq") String cq) {
+        TokenContext ctx = (TokenContext) securityContext.getUserPrincipal();
+        try {
+            scopeService.verifyScope(ctx, OAuth2Scope.SERIALIZE_QUERY);
+            String result = searchService.serializeQuery(q, ql, v, cq,
+                    pageIndex, startPage, pageLength, context, cutoff,
+                    accessRewriteDisabled);
+            if (DEBUG){
+                jlog.debug("Query: " + result);
+            }
+            return Response.ok(result).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    
+//    This web service is DISABLED until there is a need for it. 
+    @POST
+    @Path("{version}/search")
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    @SearchResourceFilters
+    public Response searchPost (@Context SecurityContext context,
+            @Context Locale locale, 
+            @Context HttpHeaders headers,
+            String jsonld) {
+        
+        if (DEBUG){
+            jlog.debug("Serialized search: " + jsonld);
+        }
+        
+        TokenContext ctx = (TokenContext) context.getUserPrincipal();
+        try {
+            scopeService.verifyScope(ctx, OAuth2Scope.SEARCH);
+            String result = searchService.search(jsonld, ctx.getUsername(),
+                    headers);
+            return Response.ok(result).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /** Performs for the given query 
+     * 
+     * @param securityContext
+     * @param request
+     * @param headers
+     * @param locale
+     * @param q
+     *            query
+     * @param ql
+     *            query language
+     * @param v
+     *            query language version
+     * @param ctx
+     *            result context
+     * @param cutoff
+     *            determines to limit search results to one page only
+     *            or not (default false)
+     * @param pageLength
+     *            the number of results should be included in a page
+     * @param pageIndex 
+     * @param pageInteger page number
+     * @param fields
+     *            metadata fields to be included, separated by comma
+     * @param pipes
+     *            external plugins for additional processing,
+     *            separated by comma
+     * @param accessRewriteDisabled
+     *            determine if access rewrite should be disabled
+     *            (default false)
+     * @param cq
+     *            corpus query defining a virtual corpus
+     * @param engine
+     * @return search results in JSON
+     */
+    @GET
+    @Path("{version}/search")
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    @SearchResourceFilters
+    public Response searchGet (@Context SecurityContext securityContext,
+            @Context HttpServletRequest request,
+            @Context HttpHeaders headers, @Context Locale locale,
+            @QueryParam("q") String q, @QueryParam("ql") String ql,
+            @QueryParam("v") String v, @QueryParam("context") String ctx,
+            @QueryParam("cutoff") Boolean cutoff,
+            @QueryParam("count") Integer pageLength,
+            @QueryParam("offset") Integer pageIndex,
+            @QueryParam("page") Integer pageInteger,
+            @QueryParam("fields") String fields,
+            @QueryParam("pipes") String pipes,
+            @QueryParam("access-rewrite-disabled") boolean accessRewriteDisabled,
+            @QueryParam("show-tokens") boolean showTokens,
+            @DefaultValue("true") @QueryParam("show-snippet") boolean showSnippet,
+            @QueryParam("cq") List<String> cq, 
+            @QueryParam("engine") String engine) {
+
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+
+        String result;
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.SEARCH);
+            result = searchService.search(engine, context.getUsername(),
+                    headers, q, ql, v, cq, fields, pipes, pageIndex,
+                    pageInteger, ctx, pageLength, cutoff,
+                    accessRewriteDisabled, showTokens, showSnippet);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+        return Response.ok(result).build();
+    }
+
+    // EM: legacy support
+    @Deprecated
+    @GET
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    @Path("{version}/corpus/{corpusId}/{docId}/{textId}/{matchId}/matchInfo")
+    @SearchResourceFilters
+    public Response getMatchInfo (@Context SecurityContext ctx,
+            @Context HttpHeaders headers, @Context Locale locale,
+            @PathParam("corpusId") String corpusId,
+            @PathParam("docId") String docId,
+            @PathParam("textId") String textId,
+            @PathParam("matchId") String matchId,
+            @QueryParam("foundry") Set<String> foundries,
+            @QueryParam("layer") Set<String> layers,
+            @QueryParam("spans") Boolean spans, 
+            // Highlights may also be a list of valid highlight classes
+            @QueryParam("hls") Boolean highlights) throws KustvaktException {
+
+        return retrieveMatchInfo(ctx, headers, locale, corpusId, docId, textId,
+                                 matchId, foundries, layers, spans, "true", "false",
+                                 "sentence", highlights);
+    }
+    
+    @GET
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    @Path("{version}/corpus/{corpusId}/{docId}/{textId}/{matchId}")
+    @SearchResourceFilters
+    public Response retrieveMatchInfo (@Context SecurityContext ctx,
+            @Context HttpHeaders headers, @Context Locale locale,
+            @PathParam("corpusId") String corpusId,
+            @PathParam("docId") String docId,
+            @PathParam("textId") String textId,
+            @PathParam("matchId") String matchId,
+            @QueryParam("foundry") Set<String> foundries,
+            @QueryParam("layer") Set<String> layers,
+            @QueryParam("spans") Boolean spans, 
+            @DefaultValue("true") @QueryParam("show-snippet") String snippetStr, 
+            @DefaultValue("false") @QueryParam("show-tokens") String tokensStr, 
+            @QueryParam("expand") String expansion, 
+            // Highlights may also be a list of valid highlight classes
+            @QueryParam("hls") Boolean highlights) throws KustvaktException {
+
+        TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
+        try {
+            scopeService.verifyScope(tokenContext, OAuth2Scope.MATCH_INFO);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+        Boolean expandToSentence = true;
+        if (expansion != null
+                && (expansion.equals("false") || expansion.equals("null"))) {
+            expandToSentence = false;
+        }
+        spans = spans != null ? spans : false;
+        Boolean snippet = true;
+        Boolean tokens = false;
+        if (snippetStr != null
+                && (snippetStr.equals("false") || snippetStr.equals("null")))
+            snippet = false;
+
+        if (tokensStr != null && (tokensStr.equals("true")
+                || tokensStr.equals("1") || tokensStr.equals("yes")))
+            tokens = true;
+
+        highlights = highlights != null ? highlights : false;
+        if (layers == null || layers.isEmpty())
+            layers = new HashSet<>();
+
+        try {
+            String results = searchService.retrieveMatchInfo(corpusId, docId,
+                    textId, matchId, true, foundries,
+                    tokenContext.getUsername(), headers, layers, spans, snippet,
+                    tokens, expandToSentence, highlights);
+            return Response.ok(results).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+    }
+
+    /*
+     * Returns the meta data fields of a certain document
+     */
+    // This is currently identical to LiteService#getMeta(),
+    // but may need auth code to work following policies
+    @GET
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    @Path("{version}/corpus/{corpusId}/{docId}/{textId}")
+    public Response getMetadata (@PathParam("corpusId") String corpusId,
+            @PathParam("docId") String docId,
+            @PathParam("textId") String textId,
+            @QueryParam("fields") String fields,
+            @Context SecurityContext ctx,
+            @Context HttpHeaders headers
+    ) throws KustvaktException {
+        TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
+        try {
+            String results = searchService.retrieveDocMetadata(corpusId, docId,
+                    textId, fields, tokenContext.getUsername(), headers);
+            return Response.ok(results).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+//  EM: This web service requires Karang and is DISABLED.
+//    @POST
+//    @Path("{version}/colloc")
+//    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response getCollocationBase (@QueryParam("q") String query) {
+        String result;
+        try {
+            result = searchService.getCollocationBase(query);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok(result).build();
+    }
+
+    // @GET
+    // @Path("colloc")
+    // public Response getCollocationsAll(@Context SecurityContext
+    // ctx,
+    // @Context Locale locale, @QueryParam("props") String properties,
+    // @QueryParam("sfskip") Integer sfs,
+    // @QueryParam("sflimit") Integer limit, @QueryParam("q") String
+    // query,
+    // @QueryParam("ql") String ql, @QueryParam("context") Integer
+    // context,
+    // @QueryParam("foundry") String foundry,
+    // @QueryParam("paths") Boolean wPaths) {
+    // TokenContext tokenContext = (TokenContext)
+    // ctx.getUserPrincipal();
+    // ColloQuery.ColloQueryBuilder builder;
+    // KoralCollectionQueryBuilder cquery = new
+    // KoralCollectionQueryBuilder();
+    // String result;
+    // try {
+    // User user = controller.getUser(tokenContext.getUsername());
+    // Set<VirtualCollection> resources = ResourceFinder
+    // .search(user, VirtualCollection.class);
+    // for (KustvaktResource c : resources)
+    // cquery.addResource(((VirtualCollection) c).getQuery());
+    //
+    // builder = functions
+    // .buildCollocations(query, ql, properties, context, limit,
+    // sfs, foundry, new ArrayList<Dependency>(), wPaths,
+    // cquery);
+    //
+    // result = graphDBhandler
+    // .getResponse("distCollo", "q", builder.build().toJSON());
+    // }catch (KustvaktException e) {
+    // throw KustvaktResponseHandler.throwit(e);
+    // }catch (JsonProcessingException e) {
+    // throw
+    // KustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT);
+    // }
+    // return Response.ok(result).build();
+    // }
+
+    // /**
+    // * @param locale
+    // * @param properties a json object string containing field, op
+    // and value
+    // for the query
+    // * @param query
+    // * @param context
+    // * @return
+    // */
+    // @GET
+    // @Path("{type}/{id}/colloc")
+    // public Response getCollocations(@Context SecurityContext ctx,
+    // @Context Locale locale, @QueryParam("props") String properties,
+    // @QueryParam("sfskip") Integer sfs,
+    // @QueryParam("sflimit") Integer limit, @QueryParam("q") String
+    // query,
+    // @QueryParam("ql") String ql, @QueryParam("context") Integer
+    // context,
+    // @QueryParam("foundry") String foundry,
+    // @QueryParam("paths") Boolean wPaths, @PathParam("id") String
+    // id,
+    // @PathParam("type") String type) {
+    // ColloQuery.ColloQueryBuilder builder;
+    // type = StringUtils.normalize(type);
+    // id = StringUtils.decodeHTML(id);
+    // TokenContext tokenContext = (TokenContext)
+    // ctx.getUserPrincipal();
+    // String result;
+    // try {
+    // KoralCollectionQueryBuilder cquery = new
+    // KoralCollectionQueryBuilder();
+    // try {
+    // User user = controller.getUser(tokenContext.getUsername());
+    //
+    // KustvaktResource resource = this.resourceHandler
+    // .findbyStrId(id, user, type);
+    //
+    // if (resource instanceof VirtualCollection)
+    // cquery.addResource(
+    // ((VirtualCollection) resource).getQuery());
+    // else if (resource instanceof Corpus)
+    // cquery.addMetaFilter("corpusID",
+    // resource.getPersistentID());
+    // else
+    // throw KustvaktResponseHandler
+    // .throwit(StatusCodes.ILLEGAL_ARGUMENT,
+    // "Type parameter not supported", type);
+    //
+    // }catch (KustvaktException e) {
+    // throw KustvaktResponseHandler.throwit(e);
+    // }catch (NumberFormatException ex) {
+    // throw KustvaktResponseHandler
+    // .throwit(StatusCodes.ILLEGAL_ARGUMENT);
+    // }
+    //
+    // builder = functions
+    // .buildCollocations(query, ql, properties, context, limit,
+    // sfs, foundry, new ArrayList<Dependency>(), wPaths,
+    // cquery);
+    //
+    // result = graphDBhandler
+    // .getResponse("distCollo", "q", builder.build().toJSON());
+    //
+    // }catch (JsonProcessingException e) {
+    // throw
+    // KustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT);
+    // }catch (KustvaktException e) {
+    // throw KustvaktResponseHandler.throwit(e);
+    // }
+    //
+    // return Response.ok(result).build();
+    // }
+
+}
diff --git a/core/src/main/java/de/ids_mannheim/korap/core/web/controller/StatisticController.java b/core/src/main/java/de/ids_mannheim/korap/core/web/controller/StatisticController.java
new file mode 100644
index 0000000..c053d45
--- /dev/null
+++ b/core/src/main/java/de/ids_mannheim/korap/core/web/controller/StatisticController.java
@@ -0,0 +1,111 @@
+package de.ids_mannheim.korap.core.web.controller;
+
+import java.util.List;
+import java.util.Locale;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import de.ids_mannheim.korap.core.service.StatisticService;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.web.CoreResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+
+/**
+ * Web services related to statistics
+ * 
+ * @author hanl
+ * @author margaretha
+ *
+ * @date 08/11/2017
+ * 
+ */
+@Controller
+@Path("{version}/statistics/")
+@ResourceFilters({ APIVersionFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class StatisticController {
+
+    private static final boolean DEBUG = false;
+    private static Logger jlog =
+            LogManager.getLogger(StatisticController.class);
+    @Autowired
+    private CoreResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private StatisticService service;
+
+    /**
+     * Returns statistics of the virtual corpus defined by the given
+     * corpusQuery parameter.
+     * 
+     * @param context
+     *            SecurityContext
+     * @param locale
+     *            Locale
+     * @param cq
+     *            a collection query specifying a virtual corpus
+     * @param corpusQuery
+     *            (DEPRECATED) a collection query specifying a virtual
+     *            corpus
+     * @return statistics of the virtual corpus defined by the given
+     *         corpusQuery parameter.
+     */
+    @GET
+    public Response getStatistics (@Context SecurityContext context,
+            @Context Locale locale, @QueryParam("cq") List<String> cq,
+            @QueryParam("corpusQuery") List<String> corpusQuery) {
+
+        String stats;
+        boolean isDeprecated = false;
+        try {
+            if (cq.isEmpty() && corpusQuery != null && !corpusQuery.isEmpty()) {
+                isDeprecated = true;
+                cq = corpusQuery;
+            }
+            stats = service.retrieveStatisticsForCorpusQuery(cq, isDeprecated);
+            if (DEBUG) {
+                jlog.debug("Stats: " + stats);
+            }
+
+            return Response
+                .ok(stats)
+                .header("X-Index-Revision", service.getIndexFingerprint())
+                .build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response getStatisticsFromKoralQuery (
+            @Context SecurityContext context, @Context Locale locale,
+            String koralQuery) {
+        try {
+            String stats = service.retrieveStatisticsForKoralQuery(koralQuery);
+            return Response
+                .ok(stats)
+                .header("X-Index-Revision", service.getIndexFingerprint())
+                .build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+}