Add a large-context group

allowing users to access larger context by request [AI-assisted, #745]

The group is automatically generated at initialization, if it doesn't
exist. The size of the context may be larger than the usual max token
context for other users. It is configurable with
"max.token.context.size.large" property in the kustvakt.config file.

Change-Id: I83986a858a3646f7061277cdbba1f4327ebdecfe
diff --git a/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java b/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
index b671c1a..7ce4845 100644
--- a/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
+++ b/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
@@ -58,9 +58,14 @@
 
     private String serverHost;
 
+	// The maximum number of tokens allowed in the context of a match result. 
+    // The context will be cut in QueryContextRewrite, if it exceeds 
+    // this number. A value of 0 means no limit.
     private int maxTokenContext;
 //    private int maxTokenMatch; // EM: Not implemented yet
 
+    private int maxTokenContextLarge;
+    
     private int maxhits;
     private int returnhits;
     private String keystoreLocation;
@@ -241,6 +246,9 @@
         maxTokenContext = Integer.parseInt(properties.getProperty(
                 "max.token.context.size", "0"));
         
+        maxTokenContextLarge = Integer.parseInt(properties.getProperty(
+                "max.token.context.size.large", "0"));
+        
         // Timeout validity in milis
         guestTimeout = Integer.parseInt(properties.getProperty(
                 "timeout.guest", "10000"));
diff --git a/src/main/java/de/ids_mannheim/korap/init/Initializator.java b/src/main/java/de/ids_mannheim/korap/init/Initializator.java
index e13a75b..353cc09 100644
--- a/src/main/java/de/ids_mannheim/korap/init/Initializator.java
+++ b/src/main/java/de/ids_mannheim/korap/init/Initializator.java
@@ -1,6 +1,7 @@
 package de.ids_mannheim.korap.init;
 
 import java.io.IOException;
+import java.sql.SQLException;
 import java.util.EnumSet;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -12,11 +13,15 @@
 import de.ids_mannheim.korap.constant.OAuth2Scope;
 import de.ids_mannheim.korap.constant.QueryType;
 import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.dao.UserGroupDao;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.dao.AccessScopeDao;
 import de.ids_mannheim.korap.oauth2.service.OAuth2InitClientService;
 import de.ids_mannheim.korap.service.QueryServiceImpl;
+import de.ids_mannheim.korap.service.UserGroupService;
 import de.ids_mannheim.korap.user.KorAPUser;
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.web.input.QueryJson;
@@ -44,6 +49,10 @@
     private OAuth2InitClientService clientService;
     @Autowired
     private QueryServiceImpl queryService;
+    @Autowired
+    private UserGroupService userGroupService;
+    @Autowired
+    private UserGroupDao userGroupDao;
     
     private double apiVersion = 1.1;
 
@@ -59,7 +68,8 @@
             clientService.createInitialSuperClient(
                     OAuth2InitClientService.OUTPUT_FILENAME);
         }
-
+        createLargeContextGroup ();
+        
         vcLoader.apiVersion = apiVersion;
         Thread t = new Thread(vcLoader);
         t.start();
@@ -85,6 +95,48 @@
 		q.setQueryType(QueryType.QUERY);
 		queryService.handlePutRequest("system", "system", "system-q", q, 
 				apiVersion);
+
+		createLargeContextGroup ();
+	}
+	
+	private void createLargeContextGroup () throws KustvaktException {
+		String groupName = "LargeContextGroup";
+		String groupAdmin = "korap_admin";
+		String description = "Users allowed to access search results with "
+				+ "larger contexts";
+        boolean groupExists = false;
+        try {
+        	userGroupService.retrieveUserGroupByName(groupName);
+            groupExists = true;
+        }
+        catch (KustvaktException e) {
+            if (e.getStatusCode() != StatusCodes.NO_RESOURCE_FOUND) {
+                throw e;
+            }
+        }
+
+        if (!groupExists) {
+            try {
+                userGroupDao.createGroup(groupName, description, groupAdmin,
+                        UserGroupStatus.ACTIVE);
+                userGroupService.retrieveUserGroupByName(groupName);
+            }
+            // handle DB exceptions, e.g. unique constraint
+            catch (Exception e) {
+                Throwable cause = e;
+                Throwable lastCause = null;
+                while ((cause = cause.getCause()) != null
+                        && !cause.equals(lastCause)) {
+                    if (cause instanceof SQLException) {
+                        break;
+                    }
+                    lastCause = cause;
+                }
+                throw new KustvaktException(StatusCodes.DB_INSERT_FAILED,
+                        cause.getMessage());
+            }
+        }
+
 	}
 
     public void initResourceTest () throws IOException, KustvaktException {
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/QueryContextRewrite.java b/src/main/java/de/ids_mannheim/korap/rewrite/QueryContextRewrite.java
index 6e7fa1c..9f5088a 100644
--- a/src/main/java/de/ids_mannheim/korap/rewrite/QueryContextRewrite.java
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/QueryContextRewrite.java
@@ -7,38 +7,58 @@
 import com.fasterxml.jackson.databind.node.ArrayNode;
 
 import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.dao.UserGroupDao;
+import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.service.UserGroupService;
 import de.ids_mannheim.korap.user.User;
 import de.ids_mannheim.korap.utils.JsonUtils;
 
 @Component
 public class QueryContextRewrite implements RewriteTask.RewriteQuery {
 
+    private static final String LARGE_CONTEXT_GROUP = "LargeContextGroup";
+
     @Autowired
-    private KustvaktConfiguration config;
+    private UserGroupService userGroupService;
+
+    @Autowired
+    private UserGroupDao userGroupDao;
 
     @Override
     public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
             User user, double apiVersion) throws KustvaktException {
-        
-        if (config.getMaxTokenContext() > 0) {
+
+        int maxContext = isInLargeContextGroup(user)
+                ? config.getMaxTokenContextLarge()
+                : config.getMaxTokenContext();
+        if (maxContext > 0) {
             boolean isContextCut = false;
             KoralNode context = node.at("/meta/context");
-            isContextCut = cutContext(context, "left");
-            isContextCut = cutContext(context, "right") || isContextCut;
+            isContextCut = cutContext(context, "left", maxContext);
+            isContextCut = cutContext(context, "right", maxContext) || isContextCut;
             if (isContextCut) context.buildRewrites();
         }
         return node;
     }
+
+    private boolean isInLargeContextGroup (User user)
+            throws KustvaktException {
+        if (user == null) return false;
+        UserGroup group = userGroupDao.retrieveGroupByName(LARGE_CONTEXT_GROUP,
+                false);
+        if (group == null) return false;
+        return userGroupService.isMember(user.getUsername(), group);
+    }
     
-    private boolean cutContext (KoralNode context, String position) 
+    private boolean cutContext (KoralNode context, String position,
+            int maxContextLength)
             throws KustvaktException {
         KoralNode contextPosition = context.at("/" + position);
         String type = contextPosition.at("/0").asText();
 
         if (type.equals("token")) {
             int length = contextPosition.at("/1").asInt();
-            int maxContextLength = config.getMaxTokenContext();
             if (length > maxContextLength) {
                 JsonNode sourceNode = JsonUtils
                         .readTree(contextPosition.toString());
diff --git a/src/main/resources/kustvakt.conf b/src/main/resources/kustvakt.conf
index 2c051ae..8bd0ff9 100644
--- a/src/main/resources/kustvakt.conf
+++ b/src/main/resources/kustvakt.conf
@@ -50,6 +50,7 @@
 # Virtual corpus and queries
 max.user.persistent.queries = 5
 # max.token.context.size = 40
+max.token.context.size.large = 50
 vc.list.statistics.enabled = false
 
 # Availability regex only support |