Move the content of the full folder to the root folder (close #644).

Change-Id: Id9243839b6288fa4cd9ea250ad9f659a0ece613c
diff --git a/src/main/java/de/ids_mannheim/korap/annotation/AnnotationParser.java b/src/main/java/de/ids_mannheim/korap/annotation/AnnotationParser.java
new file mode 100644
index 0000000..3f71e9f
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/annotation/AnnotationParser.java
@@ -0,0 +1,246 @@
+package de.ids_mannheim.korap.annotation;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.constant.AnnotationType;
+import de.ids_mannheim.korap.core.entity.Annotation;
+import de.ids_mannheim.korap.core.entity.AnnotationKey;
+import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.dao.AnnotationDao;
+
+/**
+ * Parser for extracting annotation descriptions from Kalamar
+ * javascripts
+ * 
+ * @author margaretha
+ *
+ */
+@Deprecated
+@Component
+public class AnnotationParser {
+
+    private Logger log = LogManager.getLogger(AnnotationDao.class);
+
+    public static final Pattern quotePattern = Pattern.compile("\"([^\"]*)\"");
+
+    @Autowired
+    private AnnotationDao annotationDao;
+
+    private Annotation foundry = null;
+    private AnnotationLayer layer = null;
+    private AnnotationKey key = null;
+
+    private Set<AnnotationKey> keys = new HashSet<>();
+    private Set<Annotation> values = new HashSet<>();
+
+    public void run () throws IOException {
+        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
+                getClass().getClassLoader());
+        Resource[] resources = resolver
+                .getResources("classpath:annotation-scripts/foundries/*.js");
+
+        if (resources.length < 1)
+            return;
+
+        for (Resource r : resources) {
+            //            log.debug(r.getFilename());
+            readFile(r.getInputStream());
+        }
+    }
+
+    private void readFile (InputStream inputStream) throws IOException {
+        BufferedReader br = new BufferedReader(
+                new InputStreamReader(inputStream), 1024);
+
+        foundry = null;
+
+        String line, annotationCode = "", annotationType = "";
+        Matcher m;
+        ArrayList<String> array;
+        while ((line = br.readLine()) != null) {
+            line = line.trim();
+            if (line.startsWith("ah")) {
+                m = quotePattern.matcher(line);
+                if (m.find()) {
+                    annotationCode = m.group(1);
+                    annotationType = computeAnnotationType(annotationCode);
+                }
+                m.reset();
+            }
+            else if (line.startsWith("];")) {
+                if (!keys.isEmpty()) {
+                    layer.setKeys(keys);
+                    annotationDao.updateAnnotationLayer(layer);
+                }
+                if (!values.isEmpty()) {
+                    key.setValues(values);
+                    annotationDao.updateAnnotationKey(key);
+                }
+                keys.clear();
+                values.clear();
+                layer = null;
+                key = null;
+            }
+            else if (line.startsWith("[")) {
+                array = computeValues(line);
+                parseArray(annotationCode, annotationType, array);
+            }
+
+        }
+        br.close();
+    }
+
+    public static ArrayList<String> computeValues (String line) {
+        ArrayList<String> values;
+        Matcher m = quotePattern.matcher(line);
+        values = new ArrayList<String>();
+        while (m.find()) {
+            values.add(m.group(1));
+        }
+        return values;
+    }
+
+    private void parseArray (String annotationCode, String annotationType,
+            ArrayList<String> array) {
+        if (annotationType.equals(AnnotationType.FOUNDRY)) {
+            String code = array.get(1).substring(0, array.get(1).length() - 1);
+            foundry = retrieveOrCreateAnnotation(code, AnnotationType.FOUNDRY,
+                    null, array.get(0));
+        }
+        else if (annotationType.equals(AnnotationType.LAYER)) {
+            String code = array.get(1);
+            if (code.endsWith("=")) {
+                code = code.substring(0, code.length() - 1);
+            }
+            Annotation layer = retrieveOrCreateAnnotation(code, annotationType,
+                    null, array.get(0));
+            try {
+                AnnotationLayer annotationLayer = annotationDao
+                        .retrieveAnnotationLayer(foundry.getCode(),
+                                layer.getCode());
+                if (annotationLayer == null) {
+                    annotationDao.createAnnotationLayer(foundry, layer);
+                }
+            }
+            catch (Exception e) {
+                log.debug("Duplicate annotation layer: " + foundry.getCode()
+                        + "/" + layer.getCode());
+            }
+        }
+        else if (annotationType.equals(AnnotationType.KEY))
+
+        {
+            if (layer == null) {
+                computeLayer(annotationCode);
+            }
+
+            Annotation annotation = null;
+            if (array.size() == 2) {
+                String code = array.get(1);
+                if (code.endsWith("=") || code.endsWith(":")) {
+                    code = code.substring(0, code.length() - 1);
+                }
+                annotation = retrieveOrCreateAnnotation(code, annotationType,
+                        null, array.get(0));
+            }
+            else if (array.size() == 3) {
+                annotation = retrieveOrCreateAnnotation(array.get(0),
+                        annotationType, array.get(1), array.get(2));
+            }
+            if (annotation != null) {
+                AnnotationKey annotationKey = annotationDao
+                        .retrieveAnnotationKey(layer, annotation);
+                if (annotationKey == null) {
+                    annotationDao.createAnnotationKey(layer, annotation);
+                }
+                this.keys.add(annotationKey);
+            }
+        }
+        else if (annotationType.equals(AnnotationType.VALUE)) {
+            if (this.key == null) {
+                computeKey(annotationCode);
+            }
+            Annotation value = retrieveOrCreateAnnotation(array.get(0),
+                    AnnotationType.VALUE, array.get(1), array.get(2));
+            if (value != null) {
+                values.add(value);
+            }
+        }
+    }
+
+    private void computeKey (String code) {
+        String[] codes = code.split("=");
+        if (codes.length > 1) {
+            computeLayer(codes[0]);
+            String keyCode = codes[1];
+            if (keyCode.endsWith(":") || keyCode.endsWith("-")) {
+                keyCode = keyCode.substring(0, keyCode.length() - 1);
+            }
+            Annotation key = annotationDao.retrieveAnnotation(keyCode,
+                    AnnotationType.KEY);
+            this.key = annotationDao.retrieveAnnotationKey(layer, key);
+        }
+
+    }
+
+    private void computeLayer (String code) {
+        String[] codes = code.split("/");
+        if (codes.length > 1) {
+            String layerCode = codes[1];
+            if (layerCode.endsWith("=")) {
+                layerCode = layerCode.substring(0, layerCode.length() - 1);
+            }
+            this.layer = annotationDao.retrieveAnnotationLayer(codes[0],
+                    layerCode);
+            if (layer == null) {
+                log.warn("Layer is null for " + code);
+            }
+        }
+    }
+
+    private Annotation retrieveOrCreateAnnotation (String code, String type,
+            String text, String description) {
+        Annotation annotation = annotationDao.retrieveAnnotation(code, type);
+        if (annotation == null) {
+            annotation = annotationDao.createAnnotation(code, type, text,
+                    description);
+        }
+        return annotation;
+    }
+
+    private String computeAnnotationType (String code) {
+        String[] codes = code.split("/");
+        if (codes.length == 1) {
+            if (codes[0].equals("-")) {
+                return AnnotationType.FOUNDRY;
+            }
+            return AnnotationType.LAYER;
+        }
+        else if (codes.length == 2) {
+            if (codes[1].endsWith(":") || codes[1].endsWith("-")) {
+                return AnnotationType.VALUE;
+            }
+            else {
+                return AnnotationType.KEY;
+            }
+        }
+
+        return "unknown";
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/annotation/ArrayVariables.java b/src/main/java/de/ids_mannheim/korap/annotation/ArrayVariables.java
new file mode 100644
index 0000000..09d652c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/annotation/ArrayVariables.java
@@ -0,0 +1,95 @@
+package de.ids_mannheim.korap.annotation;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import de.ids_mannheim.korap.constant.AnnotationType;
+import de.ids_mannheim.korap.core.entity.Annotation;
+
+/**
+ * Helper class to parse annotation scripts variables. It prints out
+ * corenlp constituency layer for each negranodes.
+ * 
+ * @author margaretha
+ *
+ */
+@Deprecated
+public class ArrayVariables {
+
+    public static HashMap<String, List<Annotation>> annotationMap = new HashMap<>();
+
+    public static void main (String[] args) throws IOException {
+        ArrayVariables variables = new ArrayVariables();
+        variables.extractVariables();
+
+        List<Annotation> negranodes = annotationMap.get("negranodes");
+        for (Annotation n : negranodes) {
+            System.out.println("ah[\"corenlp/c=" + n.getCode() + "-\"] = [");
+            int i = 1;
+            List<Annotation> negraedges = annotationMap.get("negraedges");
+            for (Annotation edge : negraedges) {
+                System.out.print(
+                        "  [\"" + edge.getCode() + "\", \"" + edge.getText()
+                                + "\", \"" + edge.getDescription() + "\"]");
+                if (i < negraedges.size()) {
+                    System.out.println(",");
+                }
+                else {
+                    System.out.println();
+                }
+                i++;
+            }
+            System.out.println("];");
+            System.out.println();
+        }
+    }
+
+    public void extractVariables () throws IOException {
+        String dir = "annotation-scripts/variables";
+        if (dir.isEmpty())
+            return;
+
+        File d = new File(dir);
+        if (!d.isDirectory()) {
+            throw new IOException("Directory " + dir + " is not valid");
+        }
+
+        for (File file : d.listFiles()) {
+            if (!file.exists()) {
+                throw new IOException("File " + file + " is not found.");
+            }
+            readFile(file);
+        }
+
+    }
+
+    private void readFile (File file) throws IOException {
+        BufferedReader br = new BufferedReader(
+                new InputStreamReader(new FileInputStream(file)));
+
+        String line;
+        ArrayList<String> values;
+        List<Annotation> annotations = new ArrayList<>();
+        while ((line = br.readLine()) != null) {
+            line = line.trim();
+            if (line.startsWith("[")) {
+                values = AnnotationParser.computeValues(line);
+
+                Annotation annotation = new Annotation(values.get(0),
+                        AnnotationType.VALUE, values.get(1), values.get(2));
+                annotations.add(annotation);
+            }
+        }
+        br.close();
+
+        String filename = file.getName();
+        filename = filename.substring(0, filename.length() - 3);
+        annotationMap.put(filename, annotations);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/annotation/FreeResourceParser.java b/src/main/java/de/ids_mannheim/korap/annotation/FreeResourceParser.java
new file mode 100644
index 0000000..bb7beec
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/annotation/FreeResourceParser.java
@@ -0,0 +1,84 @@
+package de.ids_mannheim.korap.annotation;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.core.entity.Resource;
+import de.ids_mannheim.korap.dao.AnnotationDao;
+import de.ids_mannheim.korap.dao.ResourceDao;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+
+/**
+ * Parser for extracting data from free-resources.json containing
+ * listing free (non-licensed) corpora.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class FreeResourceParser {
+    private Logger log = LogManager.getLogger(FreeResourceParser.class);
+
+    @Autowired
+    private ResourceDao resourceDao;
+    @Autowired
+    private AnnotationDao annotationDao;
+
+    public static String FREE_RESOURCE_FILE = "free-resources.json";
+    public static ObjectMapper mapper = new ObjectMapper();
+
+    public void run () throws IOException, KustvaktException {
+        InputStream is = null;
+        File f = new File(FREE_RESOURCE_FILE);
+        if (f.exists()) {
+            is = new FileInputStream(f);
+        }
+        else {
+            is = FreeResourceParser.class.getClassLoader()
+                    .getResourceAsStream(FREE_RESOURCE_FILE);
+        }
+
+        JsonNode node = mapper.readTree(is);
+        for (JsonNode resource : node) {
+            String resourceId = resource.at("/id").asText();
+            //            log.debug(resourceId);
+            Set<AnnotationLayer> layers = parseLayers(resource.at("/layers"));
+            try {
+                Resource r = resourceDao.retrieveResource(resourceId);
+                if (r == null) {
+                    resourceDao.createResource(resource.at("/id").asText(),
+                            resource.at("/de_title").asText(),
+                            resource.at("/en_title").asText(),
+                            resource.at("/en_description").asText(), layers);
+                }
+            }
+            catch (Exception e) {
+                log.warn("Failed creating resource: " + e.getMessage());
+            }
+        }
+    }
+
+    private Set<AnnotationLayer> parseLayers (JsonNode layers) {
+        Set<AnnotationLayer> layerSet = new HashSet<>(layers.size());
+        for (JsonNode layer : layers) {
+            String[] codes = layer.asText().split("/");
+            AnnotationLayer annotationLayer = annotationDao
+                    .retrieveAnnotationLayer(codes[0], codes[1]);
+            layerSet.add(annotationLayer);
+        }
+        return layerSet;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java b/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java
new file mode 100644
index 0000000..d78c55d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java
@@ -0,0 +1,100 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.text.ParseException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jwt.SignedJWT;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.JWTSigner;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * Authentication provider using JWT tokens
+ * 
+ * Created by hanl on 5/23/14.
+ */
+@Deprecated
+public class APIAuthentication implements AuthenticationIface {
+
+    private static Logger jlog = LogManager.getLogger(APIAuthentication.class);
+    public static boolean DEBUG = false;
+
+    private JWTSigner signedToken;
+
+    public APIAuthentication (FullConfiguration config) throws JOSEException {
+        this.signedToken = new JWTSigner(config.getSharedSecret(),
+                config.getIssuer(), config.getTokenTTL());
+    }
+
+    /**
+     * EM: for testing
+     * 
+     * @param signedToken
+     */
+    public APIAuthentication (JWTSigner signedToken) {
+        this.signedToken = signedToken;
+    }
+
+    @Override
+    public TokenContext getTokenContext (String authToken)
+            throws KustvaktException {
+        TokenContext context;
+        // Element ein = invalided.get(authToken);
+        try {
+            context = signedToken.getTokenContext(authToken);
+            context.setTokenType(getTokenType());
+        }
+        catch (JOSEException | ParseException ex) {
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
+        }
+        // context = (TokenContext) e.getObjectValue();
+        // throw new KustvaktException(StatusCodes.EXPIRED);
+        return context;
+    }
+
+    @Override
+    public TokenContext createTokenContext (User user, Map<String, Object> attr)
+            throws KustvaktException {
+        TokenContext c = new TokenContext();
+        c.setUsername(user.getUsername());
+        SignedJWT jwt = signedToken.createJWT(user, attr);
+        try {
+            c.setExpirationTime(
+                    jwt.getJWTClaimsSet().getExpirationTime().getTime());
+            if (DEBUG) {
+                jlog.debug(jwt.getJWTClaimsSet()
+                        .getClaim(Attributes.AUTHENTICATION_TIME));
+            }
+            Date authTime = jwt.getJWTClaimsSet()
+                    .getDateClaim(Attributes.AUTHENTICATION_TIME);
+            ZonedDateTime time = ZonedDateTime.ofInstant(authTime.toInstant(),
+                    ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+            c.setAuthenticationTime(time);
+        }
+        catch (ParseException e) {
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
+        }
+        c.setTokenType(getTokenType());
+        c.setToken(jwt.serialize());
+        // id_tokens.put(new Element(c.getToken(), c));
+        return c;
+    }
+
+    @Override
+    public TokenType getTokenType () {
+        return TokenType.API;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationIface.java b/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationIface.java
new file mode 100644
index 0000000..de0137a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationIface.java
@@ -0,0 +1,24 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.util.Map;
+
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+
+public interface AuthenticationIface {
+
+    public TokenContext getTokenContext (String authToken)
+            throws KustvaktException;
+
+    public TokenContext createTokenContext (User user, Map<String, Object> attr)
+            throws KustvaktException;
+
+    //    void removeUserSession (String token) throws KustvaktException;
+    //
+    //    public TokenContext refresh (TokenContext context) throws KustvaktException;
+
+    public TokenType getTokenType ();
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java
new file mode 100644
index 0000000..6ee6dc6
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java
@@ -0,0 +1,78 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import de.ids_mannheim.korap.config.KustvaktCacheable;
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import jakarta.ws.rs.core.HttpHeaders;
+
+/**
+ * @author hanl
+ * @date 15/06/2015
+ */
+public abstract class AuthenticationManager extends KustvaktCacheable {
+
+    private Map<TokenType, AuthenticationIface> providers;
+
+    public AuthenticationManager () {
+        super();
+    }
+
+    public AuthenticationManager (String cache) {
+        super(cache, "key:" + cache);
+        this.providers = new HashMap<>();
+    }
+
+    public void setProviders (Set<AuthenticationIface> providers) {
+        for (AuthenticationIface i : providers) {
+            this.providers.put(i.getTokenType(), i);
+        }
+    }
+
+    protected AuthenticationIface getProvider (TokenType scheme,
+            TokenType default_iface) {
+
+        // Debug FB: loop a Map
+
+        /*for (Map.Entry<String, AuthenticationIface> entry : this.providers.entrySet()) 
+        {
+        System.out.println("Debug: provider: Key : " + entry.getKey() + " Value : " + entry.getValue());
+        }
+        */
+        // todo: configurable authentication schema
+        if (scheme == null) {
+            return this.providers.get(default_iface);
+        }
+        else {
+            return this.providers.get(scheme);
+        }
+    }
+
+    public abstract TokenContext getTokenContext (TokenType type, String token,
+            String host, String useragent) throws KustvaktException;
+
+    public abstract User getUser (String username) throws KustvaktException;
+
+    public abstract User authenticate (AuthenticationMethod method,
+            String username, String password, Map<String, Object> attributes)
+            throws KustvaktException;
+
+    public abstract TokenContext createTokenContext (User user,
+            Map<String, Object> attr, TokenType type) throws KustvaktException;
+
+    public abstract void setAccessAndLocation (User user, HttpHeaders headers);
+
+    public String providerList () {
+        return "provider list: " + this.providers.toString();
+    }
+
+    public abstract User getUser (String username, String method)
+            throws KustvaktException;
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java b/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
new file mode 100644
index 0000000..026b5b3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
@@ -0,0 +1,98 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.authentication.http.TransferEncoding;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.Scopes;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.dao.UserDao;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.StringUtils;
+import de.ids_mannheim.korap.utils.TimeUtils;
+
+/**
+ * Implementation of encoding and decoding access token is moved to
+ * {@link TransferEncoding}. Moreover, implementation of HTTP
+ * Authentication framework, i.e. creation of authorization header,
+ * is defined in {@link HttpAuthorizationHandler}.
+ * 
+ * Basic authentication is intended to be used with a database. It is
+ * currently only used for testing using a dummy DAO (@see
+ * {@link UserDao})
+ * without passwords.
+ * 
+ * <br /><br />
+ * Latest changes:
+ * <ul>
+ * <li>Added userdao check
+ * </li>
+ * </ul>
+ * 
+ * 
+ * @author margaretha
+ * @date 01/03/2018
+ * 
+ * @author hanl
+ * @date 28/04/2015
+ */
+public class BasicAuthentication implements AuthenticationIface {
+
+    @Autowired
+    private TransferEncoding transferEncoding;
+    @Autowired
+    private FullConfiguration config;
+    // @Autowired
+    // private EncryptionIface crypto;
+    @Autowired
+    private UserDao dao;
+
+    @Override
+    public TokenContext getTokenContext (String authToken)
+            throws KustvaktException {
+        String[] values = transferEncoding.decodeBase64(authToken);
+        User user = dao.getAccount(values[0]);
+        ZonedDateTime authenticationTime = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+
+        if (user != null) {
+            TokenContext c = new TokenContext();
+            c.setUsername(values[0]);
+            c.setAuthenticationTime(authenticationTime);
+            c.setExpirationTime(TimeUtils.plusSeconds(this.config.getTokenTTL())
+                    .getMillis());
+            c.setTokenType(getTokenType());
+            // todo: for production mode, set true
+            c.setSecureRequired(false);
+            // EM: is this secure?
+            c.setToken(StringUtils.stripTokenType(authToken));
+            // fixme: you can make queries, but user sensitive data is
+            // off limits?!
+            c.addContextParameter(Attributes.SCOPE,
+                    Scopes.Scope.search.toString());
+            return c;
+        }
+        return null;
+    }
+
+    // not supported!
+    @Override
+    public TokenContext createTokenContext (User user, Map<String, Object> attr)
+            throws KustvaktException {
+        return null;
+    }
+
+    @Override
+    public TokenType getTokenType () {
+        return TokenType.BASIC;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java
new file mode 100644
index 0000000..83b979c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java
@@ -0,0 +1,73 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.KorAPUser;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import jakarta.ws.rs.core.HttpHeaders;
+
+public class DummyAuthenticationManager extends AuthenticationManager {
+
+    @Autowired
+    private KustvaktConfiguration config;
+
+    public DummyAuthenticationManager () {}
+
+    @Override
+    public TokenContext getTokenContext (TokenType type, String token,
+            String host, String useragent) throws KustvaktException {
+        TokenContext c = new TokenContext();
+        c.setUsername("guest");
+        c.setHostAddress(host);
+        c.setUserAgent(useragent);
+        c.setExpirationTime(
+                TimeUtils.plusSeconds(config.getShortTokenTTL()).getMillis());
+        c.setTokenType(TokenType.BASIC);
+        c.setToken("dummyToken");
+        return c;
+    }
+
+    @Override
+    public User getUser (String username) throws KustvaktException {
+        KorAPUser user = new KorAPUser();
+        user.setUsername(username);
+        return user;
+    }
+
+    @Override
+    public User authenticate (AuthenticationMethod method, String username,
+            String password, Map<String, Object> attributes)
+            throws KustvaktException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public TokenContext createTokenContext (User user, Map<String, Object> attr,
+            TokenType type) throws KustvaktException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public void setAccessAndLocation (User user, HttpHeaders headers) {
+        // TODO Auto-generated method stub
+
+    }
+
+    @Override
+    public User getUser (String username, String method)
+            throws KustvaktException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
new file mode 100644
index 0000000..e5f5a54
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
@@ -0,0 +1,580 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.eclipse.jetty.http.HttpHeader;
+import org.springframework.beans.factory.annotation.Autowired;
+
+// import com.novell.ldap.*; search() funktioniert nicht korrekt, ausgewechselt gegen unboundID's Bibliothek 20.04.17/FB
+//Using JAR from unboundID:
+import com.unboundid.ldap.sdk.LDAPException;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.URIParam;
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.EmptyResultException;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.exceptions.WrappedException;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.interfaces.EntityHandlerIface;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.DemoUser;
+import de.ids_mannheim.korap.user.KorAPUser;
+import de.ids_mannheim.korap.user.ShibbolethUser;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import de.ids_mannheim.korap.user.User.Location;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import de.ids_mannheim.korap.validator.Validator;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MultivaluedMap;
+
+/**
+ * contains the logic to authentication and registration processes.
+ * Uses
+ * interface implementations (AuthenticationIface) for different
+ * databases and
+ * handlers
+ * 
+ * @author hanl
+ */
+public class KustvaktAuthenticationManager extends AuthenticationManager {
+
+    private static Logger jlog = LogManager
+            .getLogger(KustvaktAuthenticationManager.class);
+    public static boolean DEBUG = false;
+
+    private EncryptionIface crypto;
+    private EntityHandlerIface entHandler;
+    @Autowired
+    private FullConfiguration config;
+    @Autowired
+    private Validator validator;
+
+    public KustvaktAuthenticationManager (EncryptionIface crypto,
+                                          FullConfiguration config) {
+        super("id_tokens");
+        this.config = config;
+        this.crypto = crypto;
+        // todo: load via beancontext
+        //		try {
+        //			this.validator = new ApacheValidator();
+        //		} catch (IOException e) {
+        //			e.printStackTrace();
+        //		}
+    }
+
+    /**
+     * get session object if token was a session token
+     * 
+     * @param token
+     * @param host
+     * @param useragent
+     * @return
+     * @throws KustvaktException
+     */
+    @Override
+    public TokenContext getTokenContext (TokenType type, String token,
+            String host, String userAgent) throws KustvaktException {
+
+        AuthenticationIface provider = getProvider(type, null);
+
+        if (provider == null) {
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+                    "Authentication provider for token type " + type
+                            + " is not found.",
+                    type.displayName());
+        }
+
+        TokenContext context = provider.getTokenContext(token);
+        context.setHostAddress(host);
+        context.setUserAgent(userAgent);
+        // if (!matchStatus(host, useragent, context))
+        // provider.removeUserSession(token);
+        return context;
+    }
+
+    @Override
+    public User getUser (String username) throws KustvaktException {
+        // User user;
+        // Object value = this.getCacheValue(username);
+
+        if (User.UserFactory.isDemo(username))
+            return User.UserFactory.getDemoUser();
+
+        // if (value != null) {
+        // Map map = (Map) value;
+        // user = User.UserFactory.toUser(map);
+        // }
+        // else {
+        // user = entHandler.getAccount(username);
+        // this.storeInCache(username, user.toCache());
+        // todo: not valid. for the duration of the session, the host should not
+        // change!
+        // }
+        // todo:
+        // user.addField(Attributes.HOST, context.getHostAddress());
+        // user.addField(Attributes.USER_AGENT, context.getUserAgent());
+
+        //EM:copied from EntityDao
+        KorAPUser user = new KorAPUser(); // oder eigentlich new DemoUser oder new DefaultUser.
+        user.setUsername(username);
+        return user;
+        //		return entHandler.getAccount(username);
+    }
+
+    @Override
+    public User getUser (String username, String method)
+            throws KustvaktException {
+        KorAPUser user = new KorAPUser();
+        user.setUsername(username);
+        String email = null;
+        switch (method.toLowerCase()) {
+            case "ldap":
+                email = config.getTestEmail();
+                break;
+            default:
+                email = config.getTestEmail();
+                break;
+        }
+        user.setEmail(email);
+        return user;
+    }
+
+    public TokenContext refresh (TokenContext context)
+            throws KustvaktException {
+        AuthenticationIface provider = getProvider(context.getTokenType(),
+                null);
+        if (provider == null) {
+            // todo:
+        }
+
+        try {
+            //			provider.removeUserSession(context.getToken());
+            User user = getUser(context.getUsername());
+            return provider.createTokenContext(user, context.params());
+        }
+        catch (KustvaktException e) {
+            throw new WrappedException(e, StatusCodes.LOGIN_FAILED);
+        }
+    }
+
+    /**
+     * EM: fix type is not flexible
+     * 
+     * @param type
+     * @param attributes
+     *            contains username and password to authenticate the
+     *            user.
+     *            Depending of the authentication schema, may contain
+     *            other
+     *            values as well
+     * @return User
+     * @throws KustvaktException
+     */
+    @Override
+    public User authenticate (AuthenticationMethod method, String username,
+            String password, Map<String, Object> attributes)
+            throws KustvaktException {
+        User user;
+        switch (method) {
+            case SHIBBOLETH:
+                // todo:
+                user = authenticateShib(attributes);
+                break;
+            case LDAP:
+                // IdM/LDAP: (09.02.17/FB)
+                user = authenticateIdM(username, password, attributes);
+                break;
+            // EM: added a dummy authentication for testing
+            case TEST:
+                user = getUser(username);
+                break;
+            default:
+                user = authenticate(username, password, attributes);
+                break;
+        }
+        return user;
+    }
+
+    // a. set location depending on X-Forwarded-For.
+    // X-Forwarded-For: clientIP, ProxyID, ProxyID...
+    // the following private address spaces may be used to define intranet
+    // spaces:
+    // 10.0.0.0 - 10.255.255.255 (10/8 prefix)
+    // 172.16.0.0 - 172.31.255.255 (172.16/12 prefix)
+    // 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)
+    // b. set corpusAccess depending on location:
+    // c. DemoUser only gets corpusAccess=FREE.
+    // 16.05.17/FB
+
+    @Override
+    public void setAccessAndLocation (User user, HttpHeaders headers) {
+        MultivaluedMap<String, String> headerMap = headers.getRequestHeaders();
+        Location location = Location.EXTERN;
+        CorpusAccess corpusAccess = CorpusAccess.FREE;
+
+        if (user instanceof DemoUser) {
+            // to be absolutely sure:
+            user.setCorpusAccess(User.CorpusAccess.FREE);
+            if (DEBUG) {
+                jlog.debug("setAccessAndLocation: DemoUser: location="
+                        + user.locationtoString() + " access="
+                        + user.accesstoString());
+            }
+            return;
+        }
+
+        if (headerMap != null && headerMap
+                .containsKey(HttpHeader.X_FORWARDED_FOR.toString())) {
+
+            String[] vals = headerMap
+                    .getFirst(HttpHeader.X_FORWARDED_FOR.toString()).split(",");
+            String clientAddress = vals[0];
+
+            try {
+                InetAddress ip = InetAddress.getByName(clientAddress);
+                if (ip.isSiteLocalAddress()) {
+                    location = Location.INTERN;
+                    corpusAccess = CorpusAccess.ALL;
+                }
+                else {
+                    corpusAccess = CorpusAccess.PUB;
+                }
+
+                if (DEBUG) {
+                    jlog.debug(String.format(
+                            "X-Forwarded-For : '%s' (%d values) -> %s\n",
+                            Arrays.toString(vals), vals.length, vals[0]));
+                    jlog.debug(String.format(
+                            "X-Forwarded-For : location = %s corpusAccess = %s\n",
+                            location == Location.INTERN ? "INTERN" : "EXTERN",
+                            corpusAccess == CorpusAccess.ALL ? "ALL"
+                                    : corpusAccess == CorpusAccess.PUB ? "PUB"
+                                            : "FREE"));
+                }
+
+            }
+            catch (UnknownHostException e) {
+                // TODO Auto-generated catch block
+                e.printStackTrace();
+            }
+
+            user.setLocation(location);
+            user.setCorpusAccess(corpusAccess);
+
+            if (DEBUG) {
+                jlog.debug("setAccessAndLocation: KorAPUser: location="
+                        + user.locationtoString() + ", access="
+                        + user.accesstoString());
+            }
+
+        }
+    } // getAccess
+
+    @Override
+    public TokenContext createTokenContext (User user, Map<String, Object> attr,
+            TokenType type) throws KustvaktException {
+        //  use api token
+        AuthenticationIface provider = getProvider(type, TokenType.API);
+
+        // EM: not in the new DB
+        //		if (attr.get(Attributes.SCOPES) != null)
+        //			this.getUserData(user, UserDetails.class);
+
+        TokenContext context = provider.createTokenContext(user, attr);
+        if (context == null)
+            throw new KustvaktException(StatusCodes.NOT_SUPPORTED);
+        context.setUserAgent((String) attr.get(Attributes.USER_AGENT));
+        context.setHostAddress(Attributes.HOST);
+        return context;
+    }
+
+    @Deprecated
+    private User authenticateShib (Map<String, Object> attributes)
+            throws KustvaktException {
+        // todo use persistent id, since eppn is not unique
+        String eppn = (String) attributes.get(Attributes.EPPN);
+
+        if (eppn == null || eppn.isEmpty())
+            throw new KustvaktException(StatusCodes.INVALID_REQUEST);
+
+        if (!attributes.containsKey(Attributes.EMAIL)
+                && validator.isValid(eppn, Attributes.EMAIL))
+            attributes.put(Attributes.EMAIL, eppn);
+
+        User user = null;
+        return user;
+    }
+
+    // todo: what if attributes null?
+    private User authenticate (String username, String password,
+            Map<String, Object> attr) throws KustvaktException {
+
+        Map<String, Object> attributes = validator.validateMap(attr);
+        User unknown;
+        // just to make sure that the plain password does not appear anywhere in
+        // the logs!
+
+        try {
+            validator.validateEntry(username, Attributes.USERNAME);
+        }
+        catch (KustvaktException e) {
+            throw new WrappedException(e, StatusCodes.LOGIN_FAILED, username);
+        }
+
+        if (username == null || username.isEmpty())
+            throw new WrappedException(
+                    new KustvaktException(username,
+                            StatusCodes.BAD_CREDENTIALS),
+                    StatusCodes.LOGIN_FAILED);
+        else {
+            try {
+                unknown = entHandler.getAccount(username);
+            }
+            catch (EmptyResultException e) {
+                // mask exception to disable user guessing in possible attacks
+                throw new WrappedException(
+                        new KustvaktException(username,
+                                StatusCodes.BAD_CREDENTIALS),
+                        StatusCodes.LOGIN_FAILED, username);
+            }
+            catch (KustvaktException e) {
+                jlog.error("Error: {}", e);
+                throw new WrappedException(e, StatusCodes.LOGIN_FAILED,
+                        attributes.toString());
+            }
+        }
+
+        if (DEBUG) {
+            jlog.debug(
+                    "Authentication: found username " + unknown.getUsername());
+        }
+        if (unknown instanceof KorAPUser) {
+            if (password == null || password.isEmpty())
+                throw new WrappedException(
+                        new KustvaktException(unknown.getId(),
+                                StatusCodes.BAD_CREDENTIALS),
+                        StatusCodes.LOGIN_FAILED, username);
+
+            KorAPUser user = (KorAPUser) unknown;
+            boolean check = crypto.checkHash(password, user.getPassword());
+
+            if (!check) {
+                // the fail counter only applies for wrong password
+                jlog.warn("Wrong Password!");
+                throw new WrappedException(
+                        new KustvaktException(user.getId(),
+                                StatusCodes.BAD_CREDENTIALS),
+                        StatusCodes.LOGIN_FAILED, username);
+            }
+
+            // bad credentials error has precedence over account locked or
+            // unconfirmed codes
+            // since latter can lead to account guessing of third parties
+            if (user.isAccountLocked()) {
+                URIParam param = (URIParam) user.getField(URIParam.class);
+
+                if (param.hasValues()) {
+                    if (DEBUG) {
+                        jlog.debug("Account is not yet activated for user '"
+                                + user.getUsername() + "'");
+                    }
+                    if (TimeUtils.getNow().isAfter(param.getUriExpiration())) {
+                        jlog.error(
+                                "URI token is expired. Deleting account for user "
+                                        + user.getUsername());
+                        throw new WrappedException(
+                                new KustvaktException(unknown.getId(),
+                                        StatusCodes.EXPIRED,
+                                        "account confirmation uri has expired!",
+                                        param.getUriFragment()),
+                                StatusCodes.LOGIN_FAILED, username);
+                    }
+                    throw new WrappedException(
+                            new KustvaktException(unknown.getId(),
+                                    StatusCodes.ACCOUNT_NOT_CONFIRMED),
+                            StatusCodes.LOGIN_FAILED, username);
+                }
+                jlog.error("ACCESS DENIED: account not active for '"
+                        + unknown.getUsername() + "'");
+                throw new WrappedException(
+                        new KustvaktException(unknown.getId(),
+                                StatusCodes.ACCOUNT_DEACTIVATED),
+                        StatusCodes.LOGIN_FAILED, username);
+            }
+
+        }
+        else if (unknown instanceof ShibbolethUser) {
+            // todo
+        }
+        if (DEBUG) {
+            jlog.debug("Authentication done: " + unknown);
+        }
+        return unknown;
+    }
+
+    /**
+     * authenticate using IdM (Identitätsmanagement) accessed by LDAP.
+     * 
+     * @param username
+     * @param password
+     * @param attr
+     * @return
+     * @throws KustvaktException
+     * @date 09.02.17/FB
+     */
+    // todo: what if attributes null?
+
+    private User authenticateIdM (String username, String password,
+            Map<String, Object> attr) throws KustvaktException {
+
+        User unknown = null;
+        // just to make sure that the plain password does not appear anywhere in
+        // the logs!
+        if (DEBUG) {
+            jlog.debug("Debug: authenticateIdM: entering for '%s'...\n",
+                    username);
+        }
+        /**
+         * wozu Apache Validatoren für User/Passwort für IdM/LDAP?
+         * siehe
+         * validation.properties. Abgeschaltet 21.04.17/FB try {
+         * validator.validateEntry(username, Attributes.USERNAME); }
+         * catch
+         * (KustvaktException e) { throw new WrappedException(e,
+         * StatusCodes.LOGIN_FAILED, username); }
+         */
+        if (username == null || username.isEmpty() || password == null
+                || password.isEmpty())
+            throw new WrappedException(
+                    new KustvaktException(username,
+                            StatusCodes.BAD_CREDENTIALS),
+                    StatusCodes.LOGIN_FAILED);
+
+        // LDAP Access:
+        try {
+            // todo: unknown = ...
+            int ret = LdapAuth3.login(username, password,
+                    config.getLdapConfig());
+            if (DEBUG) {
+                jlog.debug(
+                        "Debug: autenticationIdM: Ldap.login(%s) returns: %d.\n",
+                        username, ret);
+            }
+            if (ret != LdapAuth3.LDAP_AUTH_ROK) {
+                jlog.error("LdapAuth3.login(username='" + username
+                        + "') returns '" + ret + "'='"
+                        + LdapAuth3.getErrMessage(ret) + "'!");
+
+                // mask exception to disable user guessing in possible attacks
+                /*
+                 * by Hanl throw new WrappedException(new
+                 * KustvaktException(username, StatusCodes.BAD_CREDENTIALS),
+                 * StatusCodes.LOGIN_FAILED, username);
+                 */
+                throw new WrappedException(
+                        new KustvaktException(username,
+                                StatusCodes.LDAP_BASE_ERRCODE + ret,
+                                LdapAuth3.getErrMessage(ret), null),
+                        StatusCodes.LOGIN_FAILED, username);
+            }
+        }
+        catch (LDAPException e) {
+
+            jlog.error("Error: username='" + username + "' -> '" + e + "'!");
+            // mask exception to disable user guessing in possible attacks
+            /*
+             * by Hanl: throw new WrappedException(new
+             * KustvaktException(username, StatusCodes.BAD_CREDENTIALS),
+             * StatusCodes.LOGIN_FAILED, username);
+             */
+            throw new WrappedException(new KustvaktException(username,
+                    StatusCodes.LDAP_BASE_ERRCODE + LdapAuth3.LDAP_AUTH_RINTERR,
+                    LdapAuth3.getErrMessage(LdapAuth3.LDAP_AUTH_RINTERR), null),
+                    StatusCodes.LOGIN_FAILED, username);
+        }
+
+        // Create a User
+        // TODO: KorAPUser für solche mit einem bestehenden Account
+        // DefaultUser sonst.
+        User user = new KorAPUser();
+        user.setUsername(username);
+        /*
+         * folgender Code funktioniert hier noch nicht, da die Headers noch
+         * nicht ausgewertet worden sind - 23.05.17/FB Object o =
+         * attr.get(Attributes.LOCATION); String loc = (String)o.toString(); int
+         * location = Integer.parseInt(loc); user.setLocation(location);
+         * user.setCorpusAccess(Integer.parseInt(attr.get(Attributes.
+         * CORPUS_ACCESS).toString()));
+         */
+        unknown = user;
+
+        if (DEBUG) {
+            jlog.trace(
+                    "Authentication: found username " + unknown.getUsername());
+        }
+        if (unknown instanceof KorAPUser) {
+            /*
+             * password already checked using LDAP: if (password == null ||
+             * password.isEmpty()) throw new WrappedException(new
+             * KustvaktException( unknown.getId(), StatusCodes.BAD_CREDENTIALS),
+             * StatusCodes.LOGIN_FAILED, username);
+             * 
+             * KorAPUser user = (KorAPUser) unknown; boolean check =
+             * crypto.checkHash(password, user.getPassword());
+             * 
+             * if (!check) { // the fail counter only applies for wrong password
+             * jlog.warn("Wrong Password!"); processLoginFail(unknown); throw
+             * new WrappedException(new KustvaktException(user.getId(),
+             * StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
+             * username); }
+             */
+            // bad credentials error has precedence over account locked or
+            // unconfirmed codes
+            // since latter can lead to account guessing of third parties
+            /*
+             * if (user.isAccountLocked()) {
+             * 
+             * URIParam param = (URIParam) user.getField(URIParam.class);
+             * 
+             * if (param.hasValues()) {
+             * jlog.debug("Account is not yet activated for user '{}'",
+             * user.getUsername()); if
+             * (TimeUtils.getNow().isAfter(param.getUriExpiration())) {
+             * jlog.error( "URI token is expired. Deleting account for user {}",
+             * user.getUsername()); deleteAccount(user); throw new
+             * WrappedException(new KustvaktException( unknown.getId(),
+             * StatusCodes.EXPIRED, "account confirmation uri has expired!",
+             * param.getUriFragment()), StatusCodes.LOGIN_FAILED, username); }
+             * throw new WrappedException(new KustvaktException(
+             * unknown.getId(), StatusCodes.ACCOUNT_NOT_CONFIRMED),
+             * StatusCodes.LOGIN_FAILED, username); }
+             * jlog.error("ACCESS DENIED: account not active for '{}'",
+             * unknown.getUsername()); throw new WrappedException(new
+             * KustvaktException( unknown.getId(),
+             * StatusCodes.ACCOUNT_DEACTIVATED), StatusCodes.LOGIN_FAILED,
+             * username); }
+             */
+
+        }
+        //		else if (unknown instanceof ShibbolethUser) {
+        //			// todo
+        //		}
+
+        if (DEBUG) {
+            jlog.debug("Authentication done: " + username);
+        }
+        return unknown;
+
+    } // authenticateIdM
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java b/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java
new file mode 100644
index 0000000..79a9462
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java
@@ -0,0 +1,106 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class LDAPConfig {
+    public final boolean useSSL;
+    public final String host;
+    public final int port;
+    public final String searchBase;
+    public final String sLoginDN;
+    public final String searchFilter;
+    public final String sPwd;
+    public final String trustStorePath;
+    public final String additionalCipherSuites;
+    public final boolean useEmbeddedServer;
+    public final String emailAttribute;
+    public final String ldif;
+    public final String authFilter;
+    public final String userNotBlockedFilter;
+
+    public LDAPConfig (String ldapConfigFilename)
+            throws LdapConfigurationException {
+        Map<String, String> ldapConfig = null;
+        try {
+            ldapConfig = loadProp(ldapConfigFilename);
+        }
+        catch (IOException e) {
+            System.out.println(
+                    "Error: LDAPAuth.login: cannot load Property file!");
+        }
+
+        useSSL = Boolean
+                .parseBoolean(ldapConfig.getOrDefault("useSSL", "false"));
+        host = ldapConfig.getOrDefault("host", "localhost");
+        port = Integer.parseInt(
+                ldapConfig.getOrDefault("port", (useSSL ? "636" : "389")));
+        searchBase = getConfigOrThrow(ldapConfig, "searchBase");
+        sLoginDN = getConfigOrThrow(ldapConfig, "sLoginDN");
+        searchFilter = getConfigOrThrow(ldapConfig, "searchFilter");
+        authFilter = ldapConfig.getOrDefault("authFilter", null);
+        userNotBlockedFilter = ldapConfig.getOrDefault("userNotBlockedFilter",
+                null);
+        sPwd = ldapConfig.getOrDefault("pwd", "");
+        trustStorePath = ldapConfig.getOrDefault("trustStore", "");
+        additionalCipherSuites = ldapConfig
+                .getOrDefault("additionalCipherSuites", "");
+        useEmbeddedServer = Boolean.parseBoolean(
+                ldapConfig.getOrDefault("useEmbeddedServer", "false"));
+        emailAttribute = ldapConfig.getOrDefault("emailAttribute", "mail");
+        ldif = ldapConfig.getOrDefault("ldifFile", null);
+    }
+
+    static HashMap<String, String> typeCastConvert (Properties prop) {
+        Map<String, String> step2 = (Map<String, String>) (Map) prop;
+        return new HashMap<>(step2);
+    }
+
+    public static HashMap<String, String> loadProp (String sConfFile)
+            throws IOException {
+        FileInputStream in;
+        Properties prop;
+
+        try {
+            in = new FileInputStream(sConfFile);
+        }
+        catch (IOException ex) {
+            System.err.printf(
+                    "Error: LDAP.loadProp: cannot load Property file '%s'!\n",
+                    sConfFile);
+            ex.printStackTrace();
+            return null;
+        }
+
+        prop = new Properties();
+
+        try {
+            prop.load(in);
+            return typeCastConvert(prop);
+        }
+        catch (IOException ex) {
+            ex.printStackTrace();
+        }
+
+        return new HashMap<>();
+    }
+
+    private String getConfigOrThrow (Map<String, String> ldapConfig,
+            String attribute) throws LdapConfigurationException {
+        String value = ldapConfig.get(attribute);
+        if (value != null)
+            return value;
+        else
+            throw new LdapConfigurationException(attribute + " is not set");
+    }
+
+    private class LdapConfigurationException extends RuntimeException {
+        public LdapConfigurationException (String s) {
+            super(s);
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java b/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
new file mode 100644
index 0000000..845a6d5
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
@@ -0,0 +1,392 @@
+/*
+ *   user authentication via LDAP
+ */
+
+package de.ids_mannheim.korap.authentication;
+
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.SSLSocketFactory;
+
+import org.apache.commons.text.StringSubstitutor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.Filter;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.util.NotNull;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+import com.unboundid.util.ssl.TrustStoreTrustManager;
+
+import de.ids_mannheim.korap.server.EmbeddedLdapServer;
+
+/**
+ * LDAP Login
+ *
+ * @author bodmer, margaretha, kupietz
+ */
+public class LdapAuth3 {
+
+    public static final int LDAP_AUTH_ROK = 0;
+    public static final int LDAP_AUTH_RCONNECT = 1; // cannot connect to LDAP Server
+    public static final int LDAP_AUTH_RINTERR = 2; // internal error: cannot verify User+Pwd.
+    public static final int LDAP_AUTH_RUNKNOWN = 3; // User Account or Pwd unknown;
+    public static final int LDAP_AUTH_RLOCKED = 4; // User Account locked;
+    public static final int LDAP_AUTH_RNOTREG = 5; // User known, but has not registered to KorAP/C2 Service yet;
+    public static final int LDAP_AUTH_RNOEMAIL = 6; // cannot obtain email for sUserDN
+    public static final int LDAP_AUTH_RNAUTH = 7; // User Account or Pwd unknown, or not authorized
+    final static Boolean DEBUGLOG = false;        // log debug output.
+
+    private static Logger jlog = LogManager.getLogger(LdapAuth3.class);
+
+    public static String getErrMessage (int code) {
+        switch (code) {
+            case LDAP_AUTH_ROK:
+                return "LDAP Authentication successful.";
+            case LDAP_AUTH_RCONNECT:
+                return "LDAP Authentication: connecting to LDAP Server failed!";
+            case LDAP_AUTH_RINTERR:
+                return "LDAP Authentication failed due to an internal error!";
+            case LDAP_AUTH_RUNKNOWN:
+                return "LDAP Authentication failed due to unknown user or password!";
+            case LDAP_AUTH_RLOCKED:
+                return "LDAP Authentication: known user is locked!";
+            case LDAP_AUTH_RNOTREG:
+                return "LDAP Authentication: known user, but not registered for this service!";
+            case LDAP_AUTH_RNOEMAIL:
+                return "LDAP Authentication: known user, but cannot obtain email!";
+            case LDAP_AUTH_RNAUTH:
+                return "LDAP Authentication: unknown user or password, or user is locked or not authorized!";
+            default:
+                return "LDAP Authentication failed with unknown error code!";
+        }
+    }
+
+    public static int login (String login, String password,
+            String ldapConfigFilename) throws LDAPException {
+        LDAPConfig ldapConfig = new LDAPConfig(ldapConfigFilename);
+
+        login = Filter.encodeValue(login);
+        password = Filter.encodeValue(password);
+
+        if (ldapConfig.useEmbeddedServer) {
+            try {
+                EmbeddedLdapServer.startIfNotRunning(ldapConfig);
+            }
+            catch (GeneralSecurityException | UnknownHostException
+                    | LDAPException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        LdapAuth3Result ldapAuth3Result = search(login, password, ldapConfig,
+                !ldapConfig.searchFilter.contains("${password}"), true);
+        SearchResult srchRes = ldapAuth3Result.getSearchResultValue();
+
+        if (ldapAuth3Result.getErrorCode() != 0 || srchRes == null
+                || srchRes.getEntryCount() == 0) {
+            if (DEBUGLOG)
+                System.out.printf("Finding '%s': no entry found!\n", login);
+            return ldapAuth3Result.getErrorCode();
+        }
+
+        return LDAP_AUTH_ROK;
+    }
+
+    @NotNull
+    public static LdapAuth3Result search (String login, String password,
+            LDAPConfig ldapConfig, boolean bindWithFoundDN,
+            boolean applyExtraFilters) {
+        Map<String, String> valuesMap = new HashMap<>();
+        valuesMap.put("login", login);
+        valuesMap.put("password", password);
+        StringSubstitutor sub = new StringSubstitutor(valuesMap);
+        String searchFilterInstance = sub.replace(ldapConfig.searchFilter);
+
+        valuesMap.clear();
+        valuesMap.put("login", login);
+        sub = new StringSubstitutor(valuesMap);
+        String insensitiveSearchFilter = sub.replace(ldapConfig.searchFilter);
+
+        if (DEBUGLOG) {
+            //System.out.printf("LDAP Version      = %d.\n", LDAPConnection.LDAP_V3);
+            System.out.printf("LDAP Host & Port  = '%s':%d.\n", ldapConfig.host,
+                    ldapConfig.port);
+            System.out.printf("Login User = '%s'\n", login);
+            System.out.println("LDAPS " + ldapConfig.useSSL);
+        }
+
+        LDAPConnection lc;
+
+        if (ldapConfig.useSSL) {
+            try {
+                SSLUtil sslUtil;
+                if (ldapConfig.trustStorePath != null
+                        && !ldapConfig.trustStorePath.isEmpty()) {
+                    sslUtil = new SSLUtil(new TrustStoreTrustManager(
+                            ldapConfig.trustStorePath));
+                }
+                else {
+                    sslUtil = new SSLUtil(new TrustAllTrustManager());
+                }
+                if (ldapConfig.additionalCipherSuites != null
+                        && !ldapConfig.additionalCipherSuites.isEmpty()) {
+                    addSSLCipherSuites(ldapConfig.additionalCipherSuites);
+                }
+                SSLSocketFactory socketFactory = sslUtil
+                        .createSSLSocketFactory();
+                lc = new LDAPConnection(socketFactory);
+            }
+            catch (GeneralSecurityException e) {
+                System.err.printf(
+                        "Error: login: Connecting to LDAPS Server: failed: '%s'!\n",
+                        e);
+                ldapTerminate(null);
+                return new LdapAuth3Result(null, LDAP_AUTH_RCONNECT);
+            }
+        }
+        else {
+            lc = new LDAPConnection();
+        }
+        try {
+            lc.connect(ldapConfig.host, ldapConfig.port);
+            if (DEBUGLOG && ldapConfig.useSSL)
+                System.out.println("LDAPS Connection = OK\n");
+            if (DEBUGLOG && !ldapConfig.useSSL)
+                System.out.println("LDAP Connection = OK\n");
+        }
+        catch (LDAPException e) {
+            String fullStackTrace = org.apache.commons.lang.exception.ExceptionUtils
+                    .getFullStackTrace(e);
+            System.err.printf(
+                    "Error: login: Connecting to LDAP Server: failed: '%s'!\n",
+                    fullStackTrace);
+            ldapTerminate(lc);
+            return new LdapAuth3Result(null, LDAP_AUTH_RCONNECT);
+        }
+        if (DEBUGLOG)
+            System.out.printf("Debug: isConnected=%d\n",
+                    lc.isConnected() ? 1 : 0);
+
+        try {
+            // bind to server:
+            if (DEBUGLOG)
+                System.out.printf("Binding with '%s' ...\n",
+                        ldapConfig.sLoginDN);
+            lc.bind(ldapConfig.sLoginDN, ldapConfig.sPwd);
+            if (DEBUGLOG)
+                System.out.print("Binding: OK.\n");
+        }
+        catch (LDAPException e) {
+            System.err.printf("Error: login: Binding failed: '%s'!\n", e);
+            ldapTerminate(lc);
+            return new LdapAuth3Result(null, LDAP_AUTH_RINTERR);
+        }
+
+        if (DEBUGLOG)
+            System.out.printf("Debug: isConnected=%d\n",
+                    lc.isConnected() ? 1 : 0);
+
+        if (DEBUGLOG)
+            System.out.printf("Finding user '%s'...\n", login);
+
+        SearchResult srchRes = null;
+        try {
+            if (DEBUGLOG)
+                System.out.printf("Searching with searchFilter: '%s'.\n",
+                        insensitiveSearchFilter);
+
+            srchRes = lc.search(ldapConfig.searchBase, SearchScope.SUB,
+                    searchFilterInstance);
+
+            if (DEBUGLOG)
+                System.out.printf("Found '%s': %d entries.\n", login,
+                        srchRes.getEntryCount());
+        }
+        catch (LDAPSearchException e) {
+            System.err.printf("Error: Search for User failed: '%s'!\n", e);
+        }
+
+        if (srchRes == null || srchRes.getEntryCount() == 0) {
+            if (DEBUGLOG)
+                System.out.printf("Finding '%s': no entry found!\n", login);
+            ldapTerminate(lc);
+            return new LdapAuth3Result(null, LDAP_AUTH_RUNKNOWN);
+        }
+
+        if (bindWithFoundDN) {
+            String matchedDN = srchRes.getSearchEntries().get(0).getDN();
+            if (DEBUGLOG)
+                System.out.printf("Requested bind for found user %s' failed.\n",
+                        matchedDN);
+            try {
+                // bind to server:
+                if (DEBUGLOG)
+                    System.out.printf("Binding with '%s' ...\n", matchedDN);
+                BindResult bindResult = lc.bind(matchedDN, password);
+                if (DEBUGLOG)
+                    System.out.print("Binding: OK.\n");
+                if (!bindResult.getResultCode().equals(ResultCode.SUCCESS)) {
+                    ldapTerminate(lc);
+                    return new LdapAuth3Result(null, LDAP_AUTH_RUNKNOWN);
+                }
+            }
+            catch (LDAPException e) {
+                System.err.printf("Error: login: Binding failed: '%s'!\n", e);
+                ldapTerminate(lc);
+                return new LdapAuth3Result(null, LDAP_AUTH_RUNKNOWN);
+            }
+        }
+
+        if (applyExtraFilters) {
+            if (ldapConfig.authFilter != null
+                    && !ldapConfig.authFilter.isEmpty()) {
+                srchRes = applyAdditionalFilter(login, ldapConfig,
+                        ldapConfig.authFilter, searchFilterInstance, lc);
+                if (srchRes == null || srchRes.getEntryCount() == 0) {
+                    ldapTerminate(lc);
+                    return new LdapAuth3Result(null, LDAP_AUTH_RNOTREG);
+                }
+            }
+
+            if (ldapConfig.userNotBlockedFilter != null
+                    && !ldapConfig.userNotBlockedFilter.isEmpty()) {
+                srchRes = applyAdditionalFilter(login, ldapConfig,
+                        ldapConfig.userNotBlockedFilter, searchFilterInstance,
+                        lc);
+                if (srchRes == null || srchRes.getEntryCount() == 0) {
+                    ldapTerminate(lc);
+                    return new LdapAuth3Result(null, LDAP_AUTH_RLOCKED);
+                }
+            }
+        }
+
+        ldapTerminate(lc);
+        return new LdapAuth3Result(srchRes, LDAP_AUTH_ROK);
+    }
+
+    private static SearchResult applyAdditionalFilter (String login,
+            LDAPConfig ldapConfig, String searchFilterInstance,
+            String extraFilter, LDAPConnection lc) {
+        SearchResult srchRes;
+        srchRes = null;
+        try {
+            String combindedFilterInstance = "(&" + searchFilterInstance
+                    + extraFilter + ")";
+            if (DEBUGLOG)
+                System.out.printf("Searching with additional Filter: '%s'.\n",
+                        extraFilter);
+            srchRes = lc.search(ldapConfig.searchBase, SearchScope.SUB,
+                    combindedFilterInstance);
+            if (DEBUGLOG)
+                System.out.printf("Found '%s': %d entries.\n", login,
+                        srchRes.getEntryCount());
+        }
+        catch (LDAPSearchException e) {
+            System.err.printf("Error: Search for User failed: '%s'!\n", e);
+        }
+        return srchRes;
+    }
+
+    public static String getEmail (String sUserDN, String ldapConfigFilename)
+            throws LDAPException {
+        String sUserPwd = "*";
+        LDAPConfig ldapConfig = new LDAPConfig(ldapConfigFilename);
+        final String emailAttribute = ldapConfig.emailAttribute;
+
+        SearchResult searchResult = search(sUserDN, sUserPwd, ldapConfig, false,
+                false).getSearchResultValue();
+
+        if (searchResult == null) {
+            return null;
+        }
+
+        for (SearchResultEntry entry : searchResult.getSearchEntries()) {
+            String mail = entry.getAttributeValue(emailAttribute);
+            if (mail != null) {
+                return mail;
+            }
+        }
+        return null;
+    }
+
+    public static String getUsername (String sUserDN, String ldapConfigFilename)
+            throws LDAPException {
+        String sUserPwd = "*";
+        LDAPConfig ldapConfig = new LDAPConfig(ldapConfigFilename);
+        final String idsC2Attribute = "idsC2Profile";
+        final String uidAttribute = "uid";
+
+        SearchResult searchResult = search(sUserDN, sUserPwd, ldapConfig, false,
+                false).getSearchResultValue();
+
+        if (searchResult == null) {
+            return null;
+        }
+
+        String username = null;
+        for (SearchResultEntry entry : searchResult.getSearchEntries()) {
+            username = entry.getAttributeValue(idsC2Attribute);
+            if (username == null) {
+                username = entry.getAttributeValue(uidAttribute);
+                jlog.warn("idsC2Profile not found for uid: " + username);
+            }
+        }
+        return username;
+    }
+
+    public static void ldapTerminate (LDAPConnection lc) {
+        if (DEBUGLOG)
+            System.out.println("Terminating...");
+
+        if (lc != null) {
+            lc.close(null);
+        }
+        if (DEBUGLOG)
+            System.out.println("closing connection: done.\n");
+    }
+
+    private static void addSSLCipherSuites (String ciphersCsv) {
+        // add e.g. TLS_RSA_WITH_AES_256_GCM_SHA384
+        Set<String> ciphers = new HashSet<>();
+        ciphers.addAll(SSLUtil.getEnabledSSLCipherSuites());
+        ciphers.addAll(Arrays.asList(ciphersCsv.split(", *")));
+        SSLUtil.setEnabledSSLCipherSuites(ciphers);
+    }
+
+    public static class LdapAuth3Result {
+        final int errorCode;
+        final Object value;
+
+        public LdapAuth3Result (Object value, int errorCode) {
+            this.errorCode = errorCode;
+            this.value = value;
+        }
+
+        public int getErrorCode () {
+            return errorCode;
+        }
+
+        public Object getValue () {
+            return value;
+        }
+
+        public SearchResult getSearchResultValue () {
+            return (SearchResult) value;
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/LoginCounter.java b/src/main/java/de/ids_mannheim/korap/authentication/LoginCounter.java
new file mode 100644
index 0000000..1e08647
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/LoginCounter.java
@@ -0,0 +1,68 @@
+package de.ids_mannheim.korap.authentication;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.utils.TimeUtils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * @author hanl
+ * @date 11/11/2014
+ */
+public class LoginCounter {
+
+    private static Logger jlog = LogManager.getLogger(LoginCounter.class);
+    private final Map<String, Long[]> failedLogins;
+    private KustvaktConfiguration config;
+
+    public static boolean DEBUG = false;
+
+    public LoginCounter (KustvaktConfiguration config) {
+        if (DEBUG) {
+            jlog.debug("init login counter for authentication management");
+        }
+        this.config = config;
+        this.failedLogins = new HashMap<>();
+    }
+
+    public void resetFailedCounter (String username) {
+        failedLogins.remove(username);
+    }
+
+    public void registerFail (String username) {
+        long expires = TimeUtils.plusSeconds(config.getLoginAttemptTTL())
+                .getMillis();
+        long fail = 1;
+        Long[] set = failedLogins.get(username);
+        if (set != null)
+            fail = set[0] + 1;
+        else
+            set = new Long[2];
+        set[0] = fail;
+        set[1] = expires;
+
+        failedLogins.put(username, set);
+        jlog.warn("user failed to login "
+                + Arrays.asList(failedLogins.get(username)));
+    }
+
+    public boolean validate (String username) {
+        Long[] set = failedLogins.get(username);
+        if (set != null) {
+            if (TimeUtils.isExpired(set[1])) {
+                failedLogins.remove(username);
+                return true;
+            }
+            else if (set[0] < config.getLoginAttemptNum())
+                return true;
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java b/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
new file mode 100644
index 0000000..3f569fb
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
@@ -0,0 +1,69 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeServiceImpl;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * Authentication provider for Bearer tokens
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class OAuth2Authentication implements AuthenticationIface {
+
+    @Autowired
+    private AccessTokenDao accessDao;
+    @Autowired
+    private OAuth2ScopeServiceImpl scopeService;
+
+    @Override
+    public TokenContext getTokenContext (String authToken)
+            throws KustvaktException {
+
+        AccessToken accessToken = accessDao.retrieveAccessToken(authToken);
+        if (accessToken.isRevoked()) {
+            throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+                    "Access token is invalid", OAuth2Error.INVALID_TOKEN);
+        }
+
+        String scopes = scopeService
+                .convertAccessScopesToString(accessToken.getScopes());
+
+        TokenContext c = new TokenContext();
+        c.setUsername(accessToken.getUserId());
+        c.setExpirationTime(
+                accessToken.getExpiryDate().toInstant().toEpochMilli());
+        c.setToken(authToken);
+        c.setTokenType(TokenType.BEARER);
+        c.addContextParameter(Attributes.SCOPE, scopes);
+        c.setAuthenticationTime(accessToken.getUserAuthenticationTime());
+        return c;
+    }
+
+    @Override
+    public TokenContext createTokenContext (User user, Map<String, Object> attr)
+            throws KustvaktException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public TokenType getTokenType () {
+        return TokenType.BEARER;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/SessionAuthentication.java b/src/main/java/de/ids_mannheim/korap/authentication/SessionAuthentication.java
new file mode 100644
index 0000000..7d63c56
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/SessionAuthentication.java
@@ -0,0 +1,96 @@
+package de.ids_mannheim.korap.authentication;
+
+import java.util.Map;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joda.time.DateTime;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.TimeUtils;
+
+/**
+ * implementation of the AuthenticationIface to handle korap
+ * authentication
+ * internals
+ * 
+ * @author hanl
+ */
+@Deprecated
+public class SessionAuthentication implements AuthenticationIface {
+
+    private static final Logger jlog = LogManager
+            .getLogger(SessionAuthentication.class);
+    public static boolean DEBUG = false;
+
+    public static SessionFactory sessions;
+    private ScheduledThreadPoolExecutor scheduled;
+    private EncryptionIface crypto;
+    private KustvaktConfiguration config;
+
+    public SessionAuthentication (KustvaktConfiguration config,
+                                  EncryptionIface crypto) {
+        jlog.info("initialize session authentication handler");
+        this.crypto = crypto;
+        this.config = config;
+        this.scheduled = new ScheduledThreadPoolExecutor(1);
+        this.sessions = new SessionFactory(this.config.isAllowMultiLogIn(),
+                this.config.getInactiveTime());
+        this.scheduled.scheduleAtFixedRate(this.sessions,
+                this.config.getInactiveTime() / 2,
+                this.config.getInactiveTime(), TimeUnit.SECONDS);
+    }
+
+    @Override
+    public TokenContext getTokenContext (String authenticationToken)
+            throws KustvaktException {
+        if (DEBUG) {
+            jlog.debug(
+                    "retrieving user session for user " + authenticationToken);
+        }
+        return this.sessions.getSession(authenticationToken);
+    }
+
+    @Override
+    public TokenContext createTokenContext (User user, Map<String, Object> attr)
+            throws KustvaktException {
+        DateTime now = TimeUtils.getNow();
+        DateTime ex = TimeUtils.getExpiration(now.getMillis(),
+                config.getShortTokenTTL());
+        String token = crypto.createToken(true, user.getUsername(),
+                now.getMillis());
+        TokenContext ctx = new TokenContext();
+        ctx.setUsername(user.getUsername());
+        ctx.setTokenType(TokenType.SESSION);
+        ctx.setToken(token);
+        ctx.setExpirationTime(ex.getMillis() + (1000));
+        ctx.setHostAddress(attr.get(Attributes.HOST).toString());
+        ctx.setUserAgent(attr.get(Attributes.USER_AGENT).toString());
+        this.sessions.putSession(token, ctx);
+        if (DEBUG) {
+            jlog.debug(ctx.toJson());
+            jlog.debug("session " + sessions.getSession(token).toString());
+            jlog.info("create session for user: " + user.getUsername());
+        }
+        return ctx;
+    }
+
+    //    @Override
+    //    public void removeUserSession (String token) {
+    //        this.sessions.removeSession(token);
+    //    }
+
+    @Override
+    public TokenType getTokenType () {
+        return TokenType.SESSION;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/SessionFactory.java b/src/main/java/de/ids_mannheim/korap/authentication/SessionFactory.java
new file mode 100644
index 0000000..55b3e42
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/SessionFactory.java
@@ -0,0 +1,183 @@
+package de.ids_mannheim.korap.authentication;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.DemoUser;
+import de.ids_mannheim.korap.utils.ConcurrentMultiMap;
+import de.ids_mannheim.korap.utils.TimeUtils;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joda.time.DateTime;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * session object to hold current user sessions and track inactive
+ * time to close
+ * unused sessions. Inactive sessions are not enforced until user
+ * makes a
+ * request through thrift
+ * 
+ * @author hanl
+ */
+//todo: use simple ehcache!
+public class SessionFactory implements Runnable {
+
+    private static Logger jlog = LogManager.getLogger(SessionFactory.class);
+    public static boolean DEBUG = false;
+
+    public static ConcurrentMap<String, TokenContext> sessionsObject;
+    public static ConcurrentMap<String, DateTime> timeCheck;
+    public static ConcurrentMultiMap<String, String> loggedInRecord;
+    //    private final ConcurrentMultiMap<String, Long> failedLogins;
+    private final boolean multipleEnabled;
+    private final int inactive;
+
+    public SessionFactory (boolean multipleEnabled, int inactive) {
+        if (DEBUG) {
+            jlog.debug("allow multiple sessions per user: " + multipleEnabled);
+        }
+        this.multipleEnabled = multipleEnabled;
+        this.inactive = inactive;
+        this.sessionsObject = new ConcurrentHashMap<>();
+        this.timeCheck = new ConcurrentHashMap<>();
+        this.loggedInRecord = new ConcurrentMultiMap<>();
+    }
+
+    public boolean hasSession (TokenContext context) {
+        if (context.getUsername().equalsIgnoreCase(DemoUser.DEMOUSER_NAME))
+            return false;
+
+        List<String> value = loggedInRecord.get(context.getUsername());
+        return value != null && !value.isEmpty();
+    }
+
+    // todo: remove this!
+    @Cacheable("session")
+    public TokenContext getSession (String token) throws KustvaktException {
+        if (DEBUG) {
+            jlog.debug("logged in users: " + loggedInRecord);
+        }
+        TokenContext context = sessionsObject.get(token);
+        if (context != null) {
+            // fixme: set context to respecitve expiratin interval and return context. handler checks expiration later!
+            if (isUserSessionValid(token)) {
+                resetInterval(token);
+            }
+            else
+                throw new KustvaktException(StatusCodes.EXPIRED);
+
+        }
+        return context;
+    }
+
+    //todo: ?!
+    @CacheEvict(value = "session", key = "#session.token")
+    public void putSession (final String token, final TokenContext activeUser)
+            throws KustvaktException {
+        if (!hasSession(activeUser) | multipleEnabled) {
+            loggedInRecord.put(activeUser.getUsername(), token);
+            sessionsObject.put(token, activeUser);
+            timeCheck.put(token, TimeUtils.getNow());
+        }
+        else {
+            removeAll(activeUser);
+            throw new KustvaktException(StatusCodes.ALREADY_LOGGED_IN);
+        }
+    }
+
+    public void removeAll (final TokenContext activeUser) {
+        for (String existing : loggedInRecord.get(activeUser.getUsername())) {
+            timeCheck.remove(existing);
+            sessionsObject.remove(existing);
+        }
+        loggedInRecord.remove(activeUser.getUsername());
+    }
+
+    @CacheEvict(value = "session", key = "#session.token")
+    public void removeSession (String token) {
+        String username = sessionsObject.get(token).getUsername();
+        loggedInRecord.remove(username, token);
+        if (loggedInRecord.get(username).isEmpty())
+            loggedInRecord.remove(username);
+        timeCheck.remove(token);
+        sessionsObject.remove(token);
+    }
+
+    /**
+     * reset inactive time interval to 0
+     * 
+     * @param token
+     */
+    private void resetInterval (String token) {
+        timeCheck.put(token, TimeUtils.getNow());
+    }
+
+    /**
+     * if user possesses a valid non-expired session token
+     * 
+     * @param token
+     * @return validity of user to request a backend function
+     */
+    private boolean isUserSessionValid (String token) {
+        if (timeCheck.containsKey(token)) {
+            if (TimeUtils
+                    .plusSeconds(timeCheck.get(token).getMillis(), inactive)
+                    .isAfterNow()) {
+                if (DEBUG) {
+                    jlog.debug("user has session");
+                }
+                return true;
+            }
+            else if (DEBUG) {
+                jlog.debug(
+                        "user with token " + token + " has an invalid session");
+            }
+        }
+        return false;
+    }
+
+    /**
+     * clean inactive sessions from session object
+     * TODO: persist userdata to database when session times out!
+     */
+    private void timeoutMaintenance () {
+        jlog.trace("running session cleanup thread");
+        Set<String> inactive = new HashSet<>();
+        for (Entry<String, DateTime> entry : timeCheck.entrySet()) {
+            if (!isUserSessionValid(entry.getKey())) {
+                TokenContext user = sessionsObject.get(entry.getKey());
+                jlog.trace(
+                        "removing user session for user " + user.getUsername());
+                inactive.add(user.getUsername());
+                removeSession(entry.getKey());
+            }
+        }
+        // fixme: not doing anything!
+        if (inactive.size() > 0) {
+            if (DEBUG) {
+                jlog.trace(
+                        "removing inactive user session for users " + inactive);
+            }
+        }
+    }
+
+    /**
+     * run cleanup-thread
+     */
+    @Override
+    public void run () {
+        timeoutMaintenance();
+        if (loggedInRecord.size() > 0)
+            jlog.debug("logged users: " + loggedInRecord.toString());
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/http/AuthorizationData.java b/src/main/java/de/ids_mannheim/korap/authentication/http/AuthorizationData.java
new file mode 100644
index 0000000..a0ebd57
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/http/AuthorizationData.java
@@ -0,0 +1,23 @@
+package de.ids_mannheim.korap.authentication.http;
+
+import de.ids_mannheim.korap.constant.AuthenticationScheme;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes the values stored in Authorization header of HTTP
+ * requests.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+public class AuthorizationData {
+
+    private String token;
+    private AuthenticationScheme authenticationScheme;
+    private String username;
+    private String password;
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/http/HttpAuthorizationHandler.java b/src/main/java/de/ids_mannheim/korap/authentication/http/HttpAuthorizationHandler.java
new file mode 100644
index 0000000..241e9ce
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/http/HttpAuthorizationHandler.java
@@ -0,0 +1,66 @@
+package de.ids_mannheim.korap.authentication.http;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.constant.AuthenticationScheme;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Implementation of Basic HTTP authentication scheme (see RFC 7253
+ * and 7617) for client asking for authorization and sending user
+ * data.
+ * 
+ * @author margaretha
+ * 
+ */
+@Component
+public class HttpAuthorizationHandler {
+
+    public static String createBasicAuthorizationHeaderValue (String username,
+            String password) throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(password, "password");
+
+        String credentials = TransferEncoding.encodeBase64(username, password);
+        return AuthenticationScheme.BASIC.displayName() + " " + credentials;
+    }
+
+    public AuthorizationData parseAuthorizationHeaderValue (
+            String authorizationHeader) throws KustvaktException {
+        ParameterChecker.checkStringValue(authorizationHeader,
+                "authorization header");
+
+        String[] values = authorizationHeader.split(" ");
+        if (values.length != 2) {
+            throw new KustvaktException(StatusCodes.AUTHENTICATION_FAILED,
+                    "Cannot parse authorization header value "
+                            + authorizationHeader
+                            + ". Use this format: [authentication "
+                            + "scheme] [authentication token]",
+                    authorizationHeader);
+        }
+
+        AuthorizationData data = new AuthorizationData();
+        String scheme = values[0];
+        try {
+            data.setAuthenticationScheme(
+                    AuthenticationScheme.valueOf(scheme.toUpperCase()));
+        }
+        catch (IllegalArgumentException e) {
+            throw new KustvaktException(StatusCodes.AUTHENTICATION_FAILED,
+                    "Authentication scheme is not supported.", scheme);
+        }
+        data.setToken(values[1]);
+        return data;
+    }
+
+    public AuthorizationData parseBasicToken (AuthorizationData data)
+            throws KustvaktException {
+        String[] credentials = TransferEncoding.decodeBase64(data.getToken());
+        data.setUsername(credentials[0]);
+        data.setPassword(credentials[1]);
+        return data;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/http/TransferEncoding.java b/src/main/java/de/ids_mannheim/korap/authentication/http/TransferEncoding.java
new file mode 100644
index 0000000..305406c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/authentication/http/TransferEncoding.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.authentication.http;
+
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * TransferEncoding contains encoding and decoding methods for data
+ * transfer,
+ * e.g. transfering credentials using basic Http authentication.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class TransferEncoding {
+
+    /**
+     * Encodes username and password using Base64.
+     * 
+     * @param username
+     *            username
+     * @param password
+     *            password
+     * @return
+     */
+    public static String encodeBase64 (String username, String password) {
+        String s = username + ":" + password;
+        return new String(Base64.encodeBase64(s.getBytes()));
+    }
+
+    /**
+     * Decodes the given string using Base64.
+     * 
+     * @param encodedStr
+     * @return username and password as an array of strings.
+     * @throws KustvaktException
+     */
+    public static String[] decodeBase64 (String encodedStr)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(encodedStr, "encoded string");
+        String decodedStr = new String(Base64.decodeBase64(encodedStr));
+
+        if (decodedStr.contains(":") && decodedStr.split(":").length == 2) {
+            String[] strArr = decodedStr.split(":");
+            if ((strArr[0] != null && !strArr[0].isEmpty())
+                    && (strArr[1] != null && !strArr[1].isEmpty())) {
+                return decodedStr.split(":");
+            }
+
+        }
+
+        throw new IllegalArgumentException(
+                "Unknown Base64 encoding format: " + decodedStr);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/Attributes.java b/src/main/java/de/ids_mannheim/korap/config/Attributes.java
new file mode 100644
index 0000000..dde3291
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/Attributes.java
@@ -0,0 +1,178 @@
+package de.ids_mannheim.korap.config;
+
+public class Attributes {
+
+    public static final String AUTHENTICATION_TIME = "auth_time";
+    public static final String DEFAULT_TIME_ZONE = "Europe/Berlin";
+
+    public static final String AUTHORIZATION = "Authorization";
+    // moved to de.ids_mannheim.korap.config.AuthenticationScheme
+    //    public static final String SESSION_AUTHENTICATION = "session_token";
+    //    public static final String API_AUTHENTICATION = "api_token";
+    //    public static final String OAUTH2_AUTHORIZATION = "bearer";
+    //    public static final String BASIC_AUTHENTICATION = "basic";
+
+    public static final String LOCATION = "location"; // location of Client: User.INTERN/EXTERN
+    public static final String CORPUS_ACCESS = "corpusAccess"; // User.ALL/PUB/FREE.
+
+    public static final String CLIENT_ID = "client_id";
+    public static final String CLIENT_SECRET = "client_secret";
+    public static final String SCOPE = "scope";
+
+    public static final String PUBLIC_GROUP = "public";
+
+    public static final String SERVICE_ACCESS = "service_access";
+    public static final String USER = "KorapUser";
+    public static final String SHIBUSER = "ShibUser";
+    public static final String DEMO_DISPLAY = "Anonymous";
+    public static final String DEMOUSER_PASSWORD = "demo";
+
+    public static final String SETTINGS = "LocalSettings";
+    //    public static final String STORAGE_SETTINGS = "StorageSettings";
+
+    public static final String QUERY_ABBREVIATION = "Q";
+    public static final String LAYER = "layer";
+
+    public static final String TYPE = "type";
+
+    public static final String ID = "ID";
+    @Deprecated
+    //refactor
+    public static final String UID = "accountID";
+    public static final String USERNAME = "username";
+    public static final String PASSWORD = "password";
+    public static final String GENDER = "gender";
+    public static final String FIRSTNAME = "firstName";
+    public static final String LASTNAME = "lastName";
+    public static final String PHONE = "phone";
+    public static final String INSTITUTION = "institution";
+    public static final String EMAIL = "email";
+    public static final String ADDRESS = "address";
+    public static final String COUNTRY = "country";
+    public static final String IPADDRESS = "ipaddress";
+    public static final String IS_ADMIN = "admin";
+    // deprecated, use created
+    public static final String ACCOUNT_CREATION = "account_creation";
+    public static final String ACCOUNTLOCK = "account_lock";
+    public static final String ACCOUNTLINK = "account_link";
+    public static final String URI = "uri";
+    public static final String URI_FRAGMENT = "uri_fragment";
+    public static final String URI_EXPIRATION = "uri_expiration";
+    public static final String PRIVATE_USAGE = "privateUsage";
+
+    /**
+     * token context
+     */
+    public static final String TOKEN = "token";
+    public static final String TOKEN_TYPE = "token_type";
+    public static final String TOKEN_EXPIRATION = "expires";
+    public static final String TOKEN_CREATION = "tokenCreated";
+    public static final String USER_AGENT = "User-Agent";
+    public static final String HOST = "userIP";
+
+    public static final String QUERY_PARAM_URI = "uri";
+    public static final String QUERY_PARAM_USER = "user";
+
+    /**
+     * shibboleth attribute names
+     */
+    public static final String EPPN = "eppn";
+    public static final String COMMON_NAME = "cn";
+    public static final String SURNAME = "sn";
+
+    public static final String EDUPERSON = "eduPersonPrincipalName";
+    public static final String CN = "cn";
+    public static final String MAIL = "mail";
+    public static final String EDU_AFFIL = "eduPersonScopedAffiliation";
+
+    /**
+     * resource mappings
+     */
+
+    public static final String RID = "id";
+    public static final String OWNER = "owner";
+    public static final String NAME = "name";
+    public static final String DESCRIPTION = "description";
+
+    public static final String CORPUS_SIGLE = "corpusSigle";
+    public static final String DOC_SIGLE = "docSigle";
+    public static final String TEXT_SIGLE = "textSigle";
+
+    public static final String AVAILABILITY = "availability";
+
+    public static final String REF_CORPUS = "refCorpus";
+    public static final String QUERY = "query";
+    public static final String CACHE = "cache";
+    public static final String DOCIDS = "docIDs";
+    public static final String FOUNDRIES = "foundries";
+    public static final String DEFAULT_VALUE = "defaultColl";
+
+    public static final String FILE_FORMAT_FOR_EXPORT = "fileFormatForExport";
+    public static final String FILENAME_FOR_EXPORT = "fileNameForExport";
+    @Deprecated
+    public static final String ITEM_FOR_SIMPLE_ANNOTATION = "itemForSimpleAnnotation";
+    public static final String LEFT_CONTEXT_ITEM_FOR_EXPORT = "leftContextItemForExport";
+    public static final String LEFT_CONTEXT_SIZE_FOR_EXPORT = "leftContextSizeForExport";
+    public static final String LOCALE = "locale";
+    public static final String LEFT_CONTEXT_ITEM = "leftContextItem";
+    public static final String LEFT_CONTEXT_SIZE = "leftContextSize";
+    public static final String RIGHT_CONTEXT_ITEM = "rightContextItem";
+    public static final String RIGHT_CONTEXT_ITEM_FOR_EXPORT = "rightContextItemForExport";
+    public static final String RIGHT_CONTEXT_SIZE = "rightContextSize";
+    public static final String RIGHT_CONTEXT_SIZE_FOR_EXPORT = "rightContextSizeForExport";
+    public static final String SELECTED_COLLECTION = "selectedCollection";
+    public static final String QUERY_LANGUAGE = "queryLanguage";
+    public static final String PAGE_LENGTH = "pageLength";
+    public static final String METADATA_QUERY_EXPERT_MODUS = "metadataQueryExpertModus";
+    @Deprecated
+    public static final String SEARCH_SETTINGS_TAB = "searchSettingsTab";
+    @Deprecated
+    public static final String SELECTED_BROWSER_PROPERTY = "selectedBrowserProperty";
+    @Deprecated
+    public static final String SELECTED_CONTEXT_ITEM = "selectedContextItem";
+    @Deprecated
+    public static final String SELECTED_GRAPH_TYPE = "selectedGraphType";
+    @Deprecated
+    public static final String SELECTED_SORT_TYPE = "selectedSortType";
+    @Deprecated
+    public static final String SELECTED_VIEW_FOR_SEARCH_RESULTS = "selectedViewForSearchResults";
+    public static final String COLLECT_AUDITING_DATA = "collectData";
+
+    /**
+     * default layers
+     */
+    public static final String DEFAULT_FOUNDRY_POS = "pos-foundry";
+    public static final String DEFAULT_FOUNDRY_LEMMA = "lemma-foundry";
+    public static final String DEFAULT_FOUNDRY_CONSTITUENT = "constituent-foundry";
+    public static final String DEFAULT_FOUNDRY_RELATION = "relation-foundry";
+    public static final String DEFAULT_FOUNDRY_MORPHOLOGY = "morphology-foundry";
+    public static final String DEFAULT_FOUNDRY_STRUCTURE = "structure-foundry";
+
+    /**
+     * db column keys
+     */
+
+    public static final String SELF_REF = "self";
+
+    public static final String SYM_USE = "sym_use";
+    public static final String COMMERCIAL = "commercial";
+    public static final String LICENCE = "licence";
+    public static final String QUERY_ONLY = "query_only";
+    public static final String EXPORT = "export";
+    public static final String TIME_SPANS = "spans";
+    public static final String RANGE = "range";
+
+    public static final String GROUP_ID = "group_id";
+    public static final String CREATED = "created";
+    public static final String CREATOR = "creator";
+    public static final String ENABLED = "enabled";
+    public static final String EXPIRE = "expired";
+    public static final String TARGET_ID = "target_id";
+    public static final String IP_RANG = "ip_range";
+    public static final String PERSISTENT_ID = "persistent_id";
+    public static final String DISABLED = "disabled";
+    public static final String USER_ID = "user_id";
+    public static final String PARENT_ID = "parent_id";
+    //    public static final String
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/BeanInjectable.java b/src/main/java/de/ids_mannheim/korap/config/BeanInjectable.java
new file mode 100644
index 0000000..54f432e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/BeanInjectable.java
@@ -0,0 +1,10 @@
+package de.ids_mannheim.korap.config;
+
+/**
+ * @author hanl
+ * @date 26/02/2016
+ */
+public interface BeanInjectable {
+
+    <T extends ContextHolder> void insertBeans (T beans);
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/BeansFactory.java b/src/main/java/de/ids_mannheim/korap/config/BeansFactory.java
new file mode 100644
index 0000000..2616870
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/BeansFactory.java
@@ -0,0 +1,124 @@
+package de.ids_mannheim.korap.config;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.springframework.context.support.FileSystemXmlApplicationContext;
+
+import de.ids_mannheim.korap.interfaces.KustvaktTypeInterface;
+import de.ids_mannheim.korap.web.CoreResponseHandler;
+
+/**
+ * User: hanl
+ * Date: 10/9/13
+ * Time: 11:20 AM
+ */
+public class BeansFactory {
+
+    private static ContextHolder beanHolder;
+
+    //todo: allow this for external plugin systems that are not kustvakt specific
+    @Deprecated
+    public static void setCustomBeansHolder (ContextHolder holder) {
+        beanHolder = holder;
+    }
+
+    public static synchronized ContextHolder getKustvaktContext () {
+        return beanHolder;
+    }
+
+    public static synchronized ContextHolder getKustvaktContext (int i) {
+        return beanHolder;
+    }
+
+    public static synchronized TypeBeanFactory getTypeFactory () {
+        return new TypeBeanFactory();
+    }
+
+    public static int loadClasspathContext (String ... files) {
+        ApplicationContext context;
+        if (files.length == 0)
+            throw new IllegalArgumentException(
+                    "Spring XML config file is not specified.");
+        else
+            context = new ClassPathXmlApplicationContext(files);
+        ContextHolder h = new ContextHolder(context) {};
+        BeansFactory.beanHolder = h;
+        //        return BeansFactory.beanHolder.indexOf(h);
+        return 0;
+    }
+
+    public static synchronized int addApplicationContext (
+            ApplicationContext context) {
+        ContextHolder h = new ContextHolder(context) {};
+        BeansFactory.beanHolder = h;
+        //        return BeansFactory.beanHolder.indexOf(h);
+        return 0;
+    }
+
+    public static synchronized void setKustvaktContext (ContextHolder holder) {
+        BeansFactory.beanHolder = holder;
+    }
+
+    public static synchronized int setApplicationContext (
+            ApplicationContext context) {
+        ContextHolder h = new ContextHolder(context) {};
+        BeansFactory.beanHolder = h;
+        return 0;
+    }
+
+    public static synchronized int loadFileContext (String filepath) {
+        ApplicationContext context = new FileSystemXmlApplicationContext(
+                "file:" + filepath);
+        ContextHolder h = new ContextHolder(context) {};
+        BeansFactory.beanHolder = h;
+        return 0;
+    }
+
+    public static void closeApplication () {
+        BeansFactory.beanHolder = null;
+    }
+
+    //todo: set response handler
+    @Deprecated
+    public static CoreResponseHandler getResponseHandler () {
+        return null;
+    }
+
+    public BeansFactory () {}
+
+    public static class TypeBeanFactory {
+
+        public <T> T getTypeInterfaceBean (Collection objs, Class type) {
+            for (Object o : objs) {
+                if (o instanceof KustvaktTypeInterface) {
+                    Class t = ((KustvaktTypeInterface) o).type();
+                    if (type.equals(t))
+                        return (T) o;
+                }
+            }
+            throw new RuntimeException(
+                    "Could not find typed bean in context for class '" + type
+                            + "'");
+        }
+
+        @Deprecated
+        public <T> T getTypedBean (Collection objs, Class type) {
+            for (Object o : objs) {
+                Type gtype = o.getClass().getGenericSuperclass();
+                if (gtype instanceof ParameterizedType) {
+                    ParameterizedType ptype = (ParameterizedType) gtype;
+                    Object ctype = ptype.getActualTypeArguments()[0];
+                    if (ctype.equals(type))
+                        return (T) o;
+                }
+            }
+            throw new RuntimeException(
+                    "Could not find typed bean in context for class '" + type
+                            + "'");
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/ConfigLoader.java b/src/main/java/de/ids_mannheim/korap/config/ConfigLoader.java
new file mode 100644
index 0000000..a8ec0a0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/ConfigLoader.java
@@ -0,0 +1,57 @@
+package de.ids_mannheim.korap.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Created by hanl on 08.06.16.
+ */
+public class ConfigLoader {
+
+    private static final Logger jlog = LogManager.getLogger(ConfigLoader.class);
+
+    private ConfigLoader () {}
+
+    public static InputStream loadConfigStream (String name) {
+        InputStream stream = null;
+        try {
+            File f = new File(System.getProperty("user.dir"), name);
+
+            if (f.exists()) {
+                jlog.info("Loading config '" + name + "' from file!");
+                stream = new FileInputStream(f);
+            }
+            else {
+                jlog.info("Loading config '" + name + "' from classpath!");
+                stream = ConfigLoader.class.getClassLoader()
+                        .getResourceAsStream(name);
+            }
+        }
+        catch (IOException e) {
+            // do nothing
+        }
+        if (stream == null)
+            throw new RuntimeException(
+                    "Config file '" + name + "' could not be loaded ...");
+        return stream;
+    }
+
+    public static Properties loadProperties (String name) {
+        Properties p = new Properties();
+        try {
+            p.load(loadConfigStream(name));
+        }
+        catch (IOException e) {
+            throw new RuntimeException("Properties from config file '" + name
+                    + "' could not be loaded ...");
+        }
+        return p;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/Configurable.java b/src/main/java/de/ids_mannheim/korap/config/Configurable.java
new file mode 100644
index 0000000..af656a2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/Configurable.java
@@ -0,0 +1,13 @@
+package de.ids_mannheim.korap.config;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author hanl
+ * @date 27/07/2015
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Configurable {
+    String value();
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/ContextHolder.java b/src/main/java/de/ids_mannheim/korap/config/ContextHolder.java
new file mode 100644
index 0000000..3d26bcc
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/ContextHolder.java
@@ -0,0 +1,72 @@
+package de.ids_mannheim.korap.config;
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+
+import de.ids_mannheim.korap.web.CoreResponseHandler;
+
+/**
+ * @author hanl
+ * @date 26/02/2016
+ */
+public abstract class ContextHolder {
+
+    public static final String KUSTVAKT_DB = "kustvakt_db";
+    public static final String KUSTVAKT_ENCRYPTION = "kustvakt_encryption";
+    public static final String KUSTVAKT_AUDITING = "kustvakt_auditing";
+    public static final String KUSTVAKT_CONFIG = "kustvakt_config";
+    public static final String KUSTVAKT_USERDATA = "kustvakt_userdata";
+    public static final String KUSTVAKT_RESOURCES = "kustvakt_resources";
+
+    public static final String KUSTVAKT_AUTHENTICATION_MANAGER = "kustvakt_authenticationmanager";
+    public static final String KUSTVAKT_AUTHPROVIDERS = "kustvakt_authproviders";
+    public static final String KUSTVAKT_USERDB = "kustvakt_userdb";
+    public static final String KUSTVAKT_ADMINDB = "kustvakt_admindb";
+    public static final String KUSTVAKT_POLICIES = "kustvakt_policies";
+
+    private ApplicationContext context = null;
+    private DefaultHandler handler;
+
+    public ContextHolder (ApplicationContext context) {
+        this.handler = new DefaultHandler();
+        this.context = context;
+        // todo: better method?!
+        new CoreResponseHandler();
+    }
+
+    protected <T> T getBean (Class<T> clazz) {
+        if (this.context != null) {
+            try {
+                return context.getBean(clazz);
+            }
+            catch (NoSuchBeanDefinitionException e) {
+                // do nothing
+            }
+        }
+        return this.handler.getDefault(clazz);
+    }
+
+    protected <T> T getBean (String name) {
+        T bean = null;
+        if (this.context != null) {
+            try {
+                bean = (T) context.getBean(name);
+            }
+            catch (NoSuchBeanDefinitionException e) {
+                // do nothing
+                bean = (T) this.handler.getDefault(name);
+            }
+        }
+
+        return bean;
+    }
+
+    @Deprecated
+    public <T extends KustvaktConfiguration> T getConfiguration () {
+        return (T) getBean(KUSTVAKT_CONFIG);
+    }
+
+    private void close () {
+        this.context = null;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/DefaultHandler.java b/src/main/java/de/ids_mannheim/korap/config/DefaultHandler.java
new file mode 100644
index 0000000..df7be47
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/DefaultHandler.java
@@ -0,0 +1,55 @@
+package de.ids_mannheim.korap.config;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author hanl
+ * @date 17/06/2015
+ */
+public class DefaultHandler {
+
+    private Map<String, Object> defaults;
+
+    public DefaultHandler () {
+        this.defaults = new HashMap<>();
+        loadClasses();
+    }
+
+    private void loadClasses () {
+        Set<Class<?>> cls = KustvaktClassLoader
+                .loadFromAnnotation(Configurable.class);
+        for (Class clazz : cls) {
+            Configurable c = (Configurable) clazz
+                    .getAnnotation(Configurable.class);
+            try {
+                this.defaults.put(c.value(), clazz.newInstance());
+            }
+            catch (InstantiationException | IllegalAccessException e) {
+                throw new RuntimeException("Could not instantiate class");
+            }
+        }
+    }
+
+    public Object getDefault (String name) {
+        return this.defaults.get(name);
+    }
+
+    public <T> T getDefault (Class<T> tClass) {
+        for (Object o : this.defaults.values()) {
+            if (o.getClass().equals(tClass))
+                return (T) o;
+        }
+        return null;
+    }
+
+    public void remove (String name) {
+        this.defaults.remove(name);
+    }
+
+    @Override
+    public String toString () {
+        return defaults.toString();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
new file mode 100644
index 0000000..0a904d3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -0,0 +1,493 @@
+package de.ids_mannheim.korap.config;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.utils.TimeUtils;
+
+/**
+ * Configuration for Kustvakt full version including properties
+ * concerning authentication and licenses.
+ * 
+ * @author margaretha
+ *
+ */
+
+public class FullConfiguration extends KustvaktConfiguration {
+    public static Logger jlog = LogManager.getLogger(FullConfiguration.class);
+    // mail configuration
+    private boolean isMailEnabled;
+    private String testEmail;
+    private String noReply;
+    private String emailAddressRetrieval;
+
+    private String groupInvitationTemplate;
+
+    private String ldapConfig;
+
+    private String freeOnlyRegex;
+    private String publicOnlyRegex;
+    private String allOnlyRegex;
+
+    private List<String> freeRegexList;
+    private List<String> publicRegexList;
+    private List<String> allRegexList;
+
+    private String authenticationScheme;
+
+    private boolean isSoftDeleteAutoGroup;
+    private boolean isSoftDeleteGroup;
+    private boolean isSoftDeleteGroupMember;
+
+    private EncryptionIface.Encryption secureHashAlgorithm;
+
+    private AuthenticationMethod OAuth2passwordAuthentication;
+    private String nativeClientHost;
+    private Set<String> clientCredentialsScopes;
+    private int maxAuthenticationAttempts;
+
+    private int accessTokenLongExpiry;
+    private int accessTokenExpiry;
+    private int refreshTokenLongExpiry;
+    private int refreshTokenExpiry;
+    private int authorizationCodeExpiry;
+
+    private int maxNumberOfUserQueries;
+
+    private URL issuer;
+
+    private String namedVCPath;
+
+    private boolean createInitialSuperClient;
+
+    public FullConfiguration (Properties properties) throws Exception {
+        super(properties);
+    }
+
+    public FullConfiguration () {
+        super();
+    }
+
+    @Override
+    public void load (Properties properties) throws Exception {
+
+        super.load(properties);
+        // EM: regex used for storing vc
+        setLicenseRegex(properties);
+
+        // EM: pattern for matching availability in Krill matches
+        setLicensePatterns(properties);
+        setDeleteConfiguration(properties);
+        setMailConfiguration(properties);
+        ldapConfig = properties.getProperty("ldap.config");
+
+        setSecurityConfiguration(properties);
+        setOAuth2Configuration(properties);
+
+        setNamedVCPath(properties.getProperty("krill.namedVC", ""));
+
+        //        Cache cache = CacheManager.newInstance().getCache("named_vc");
+        //        CacheConfiguration config = cache.getCacheConfiguration();
+        //        config.setMaxBytesLocalHeap(properties.getProperty("cache.max.bytes.local.heap", "256m"));
+        //        config.setMaxBytesLocalDisk(properties.getProperty("cache.max.bytes.local.disk", "2G"));
+        //        jlog.info("max local heap:"+config.getMaxBytesLocalHeapAsString());
+        //        jlog.info("max local disk:"+config.getMaxBytesLocalDiskAsString());
+
+        setMaxNumberOfUserQueries(Integer.parseInt(
+                properties.getProperty("max.user.persistent.queries", "20")));
+    }
+
+    private void setSecurityConfiguration (Properties properties)
+            throws MalformedURLException {
+        setSecureHashAlgorithm(Enum.valueOf(EncryptionIface.Encryption.class,
+                properties.getProperty("security.secure.hash.algorithm",
+                        "BCRYPT")));
+
+        String issuerStr = properties.getProperty("security.jwt.issuer",
+                "https://korap.ids-mannheim.de");
+
+        if (!issuerStr.startsWith("http")) {
+            issuerStr = "http://" + issuerStr;
+        }
+        setIssuer(new URL(issuerStr));
+    }
+
+    private void setOAuth2Configuration (Properties properties) {
+        setOAuth2passwordAuthentication(
+                Enum.valueOf(AuthenticationMethod.class, properties.getProperty(
+                        "oauth2.password.authentication", "TEST")));
+        setNativeClientHost(properties.getProperty("oauth2.native.client.host",
+                "korap.ids-mannheim.de"));
+        setCreateInitialSuperClient(Boolean.valueOf(properties
+                .getProperty("oauth2.initial.super.client", "false")));
+
+        setMaxAuthenticationAttempts(Integer
+                .parseInt(properties.getProperty("oauth2.max.attempts", "1")));
+
+        String clientScopes = properties
+                .getProperty("oauth2.client.credentials.scopes", "client_info");
+        setClientCredentialsScopes(Arrays.stream(clientScopes.split(" "))
+                .collect(Collectors.toSet()));
+
+        accessTokenExpiry = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("oauth2.access.token.expiry", "1D"));
+        refreshTokenExpiry = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("oauth2.refresh.token.expiry", "90D"));
+        authorizationCodeExpiry = TimeUtils.convertTimeToSeconds(properties
+                .getProperty("oauth2.authorization.code.expiry", "10M"));
+
+        setAccessTokenLongExpiry(TimeUtils.convertTimeToSeconds(properties
+                .getProperty("oauth2.access.token.long.expiry", "365D")));
+        setRefreshTokenLongExpiry(TimeUtils.convertTimeToSeconds(properties
+                .getProperty("oauth2.refresh.token.long.expiry", "365D")));
+    }
+
+    private void setMailConfiguration (Properties properties) {
+        setMailEnabled(Boolean
+                .valueOf(properties.getProperty("mail.enabled", "false")));
+        if (isMailEnabled) {
+            // other properties must be set in the kustvakt.conf
+            setTestEmail(
+                    properties.getProperty("mail.receiver", "test@localhost"));
+            setNoReply(properties.getProperty("mail.sender"));
+            setGroupInvitationTemplate(
+                    properties.getProperty("template.group.invitation"));
+            setEmailAddressRetrieval(
+                    properties.getProperty("mail.address.retrieval", "test"));
+        }
+    }
+
+    private void setDeleteConfiguration (Properties properties) {
+        setSoftDeleteGroup(
+                parseDeleteConfig(properties.getProperty("delete.group", "")));
+        setSoftDeleteAutoGroup(parseDeleteConfig(
+                properties.getProperty("delete.auto.group", "")));
+        setSoftDeleteGroupMember(parseDeleteConfig(
+                properties.getProperty("delete.group.member", "")));
+    }
+
+    private boolean parseDeleteConfig (String deleteConfig) {
+        return deleteConfig.equals("soft") ? true : false;
+    }
+
+    private void setLicensePatterns (Properties properties) {
+        setFreeLicensePattern(compilePattern(getFreeOnlyRegex()));
+        setPublicLicensePattern(compilePattern(
+                getFreeOnlyRegex() + "|" + getPublicOnlyRegex()));
+        setAllLicensePattern(compilePattern(getFreeOnlyRegex() + "|"
+                + getPublicOnlyRegex() + "|" + getAllOnlyRegex()));
+    }
+
+    private void setLicenseRegex (Properties properties) {
+        setFreeOnlyRegex(properties.getProperty("availability.regex.free", ""));
+        freeRegexList = splitAndAddToList(getFreeOnlyRegex());
+
+        setPublicOnlyRegex(
+                properties.getProperty("availability.regex.public", ""));
+        publicRegexList = splitAndAddToList(getPublicOnlyRegex());
+
+        setAllOnlyRegex(properties.getProperty("availability.regex.all", ""));
+        allRegexList = splitAndAddToList(getAllOnlyRegex());
+    }
+
+    private List<String> splitAndAddToList (String regex) {
+        List<String> list;
+        if (regex.contains("|")) {
+            String[] regexes = regex.split("\\|");
+            list = new ArrayList<>(regexes.length);
+            for (String s : regexes) {
+                list.add(s.trim());
+            }
+        }
+        else {
+            list = new ArrayList<>(1);
+            list.add(regex);
+        }
+        return list;
+    }
+
+    private Pattern compilePattern (String patternStr) {
+        if (!patternStr.isEmpty()) {
+            return Pattern.compile(patternStr);
+        }
+        else {
+            return null;
+        }
+    }
+
+    public String getLdapConfig () {
+        return ldapConfig;
+    }
+
+    public Pattern getPublicLicensePattern () {
+        return publicLicensePattern;
+    }
+
+    public void setPublicLicensePattern (Pattern publicLicensePattern) {
+        this.publicLicensePattern = publicLicensePattern;
+    }
+
+    public Pattern getFreeLicensePattern () {
+        return freeLicensePattern;
+    }
+
+    public void setFreeLicensePattern (Pattern freeLicensePattern) {
+        this.freeLicensePattern = freeLicensePattern;
+    }
+
+    public Pattern getAllLicensePattern () {
+        return allLicensePattern;
+    }
+
+    public void setAllLicensePattern (Pattern allLicensePattern) {
+        this.allLicensePattern = allLicensePattern;
+    }
+
+    public String getAuthenticationScheme () {
+        return authenticationScheme;
+    }
+
+    public void setAuthenticationScheme (String authenticationScheme) {
+        this.authenticationScheme = authenticationScheme;
+    }
+
+    public List<String> getFreeRegexList () {
+        return freeRegexList;
+    }
+
+    public void setFreeRegexList (List<String> freeRegexList) {
+        this.freeRegexList = freeRegexList;
+    }
+
+    public List<String> getPublicRegexList () {
+        return publicRegexList;
+    }
+
+    public void setPublicRegexList (List<String> publicRegexList) {
+        this.publicRegexList = publicRegexList;
+    }
+
+    public List<String> getAllRegexList () {
+        return allRegexList;
+    }
+
+    public void setAllRegexList (List<String> allRegexList) {
+        this.allRegexList = allRegexList;
+    }
+
+    public String getFreeOnlyRegex () {
+        return freeOnlyRegex;
+    }
+
+    public void setFreeOnlyRegex (String freeOnlyRegex) {
+        this.freeOnlyRegex = freeOnlyRegex;
+    }
+
+    public String getPublicOnlyRegex () {
+        return publicOnlyRegex;
+    }
+
+    public void setPublicOnlyRegex (String publicOnlyRegex) {
+        this.publicOnlyRegex = publicOnlyRegex;
+    }
+
+    public String getAllOnlyRegex () {
+        return allOnlyRegex;
+    }
+
+    public void setAllOnlyRegex (String allOnlyRegex) {
+        this.allOnlyRegex = allOnlyRegex;
+    }
+
+    public boolean isSoftDeleteGroup () {
+        return isSoftDeleteGroup;
+    }
+
+    public void setSoftDeleteGroup (boolean isSoftDeleteGroup) {
+        this.isSoftDeleteGroup = isSoftDeleteGroup;
+    }
+
+    public boolean isSoftDeleteGroupMember () {
+        return isSoftDeleteGroupMember;
+    }
+
+    public void setSoftDeleteGroupMember (boolean isSoftDeleteGroupMember) {
+        this.isSoftDeleteGroupMember = isSoftDeleteGroupMember;
+    }
+
+    public boolean isSoftDeleteAutoGroup () {
+        return isSoftDeleteAutoGroup;
+    }
+
+    public void setSoftDeleteAutoGroup (boolean isSoftDeleteAutoGroup) {
+        this.isSoftDeleteAutoGroup = isSoftDeleteAutoGroup;
+    }
+
+    public String getTestEmail () {
+        return testEmail;
+    }
+
+    public void setTestEmail (String testEmail) {
+        this.testEmail = testEmail;
+    }
+
+    public boolean isMailEnabled () {
+        return isMailEnabled;
+    }
+
+    public void setMailEnabled (boolean isMailEnabled) {
+        this.isMailEnabled = isMailEnabled;
+    }
+
+    public String getNoReply () {
+        return noReply;
+    }
+
+    public void setNoReply (String noReply) {
+        this.noReply = noReply;
+    }
+
+    public String getGroupInvitationTemplate () {
+        return groupInvitationTemplate;
+    }
+
+    public void setGroupInvitationTemplate (String groupInvitationTemplate) {
+        this.groupInvitationTemplate = groupInvitationTemplate;
+    }
+
+    public String getEmailAddressRetrieval () {
+        return emailAddressRetrieval;
+    }
+
+    public void setEmailAddressRetrieval (String emailAddressRetrieval) {
+        this.emailAddressRetrieval = emailAddressRetrieval;
+    }
+
+    public EncryptionIface.Encryption getSecureHashAlgorithm () {
+        return secureHashAlgorithm;
+    }
+
+    public void setSecureHashAlgorithm (
+            EncryptionIface.Encryption secureHashAlgorithm) {
+        this.secureHashAlgorithm = secureHashAlgorithm;
+    }
+
+    public AuthenticationMethod getOAuth2passwordAuthentication () {
+        return OAuth2passwordAuthentication;
+    }
+
+    public void setOAuth2passwordAuthentication (
+            AuthenticationMethod oAuth2passwordAuthentication) {
+        OAuth2passwordAuthentication = oAuth2passwordAuthentication;
+    }
+
+    public String getNativeClientHost () {
+        return nativeClientHost;
+    }
+
+    public void setNativeClientHost (String nativeClientHost) {
+        this.nativeClientHost = nativeClientHost;
+    }
+
+    public int getMaxAuthenticationAttempts () {
+        return maxAuthenticationAttempts;
+    }
+
+    public void setMaxAuthenticationAttempts (int maxAuthenticationAttempts) {
+        this.maxAuthenticationAttempts = maxAuthenticationAttempts;
+    }
+
+    public Set<String> getClientCredentialsScopes () {
+        return clientCredentialsScopes;
+    }
+
+    public void setClientCredentialsScopes (
+            Set<String> clientCredentialsScopes) {
+        this.clientCredentialsScopes = clientCredentialsScopes;
+    }
+
+    public URL getIssuer () {
+        return issuer;
+    }
+
+    public void setIssuer (URL issuer) {
+        this.issuer = issuer;
+    }
+
+    public int getAccessTokenExpiry () {
+        return accessTokenExpiry;
+    }
+
+    public void setAccessTokenExpiry (int accessTokenExpiry) {
+        this.accessTokenExpiry = accessTokenExpiry;
+    }
+
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
+
+    public int getAuthorizationCodeExpiry () {
+        return authorizationCodeExpiry;
+    }
+
+    public void setAuthorizationCodeExpiry (int authorizationCodeExpiry) {
+        this.authorizationCodeExpiry = authorizationCodeExpiry;
+    }
+
+    public String getNamedVCPath () {
+        return namedVCPath;
+    }
+
+    public void setNamedVCPath (String namedVCPath) {
+        this.namedVCPath = namedVCPath;
+    }
+
+    public int getAccessTokenLongExpiry () {
+        return accessTokenLongExpiry;
+    }
+
+    public void setAccessTokenLongExpiry (int accessTokenLongExpiry) {
+        this.accessTokenLongExpiry = accessTokenLongExpiry;
+    }
+
+    public int getRefreshTokenLongExpiry () {
+        return refreshTokenLongExpiry;
+    }
+
+    public void setRefreshTokenLongExpiry (int refreshTokenLongExpiry) {
+        this.refreshTokenLongExpiry = refreshTokenLongExpiry;
+    }
+
+    public boolean createInitialSuperClient () {
+        return createInitialSuperClient;
+    }
+
+    public void setCreateInitialSuperClient (boolean initialSuperClient) {
+        this.createInitialSuperClient = initialSuperClient;
+    }
+
+    public int getMaxNumberOfUserQueries () {
+        return maxNumberOfUserQueries;
+    }
+
+    public void setMaxNumberOfUserQueries (int maxNumberOfUserQueries) {
+        this.maxNumberOfUserQueries = maxNumberOfUserQueries;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java b/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
new file mode 100644
index 0000000..d8f3341
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
@@ -0,0 +1,207 @@
+package de.ids_mannheim.korap.config;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.ParseException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joda.time.DateTime;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.MACVerifier;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.JWTClaimsSet.Builder;
+import com.nimbusds.jwt.SignedJWT;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.GenericUserData;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.Userdata;
+import de.ids_mannheim.korap.utils.TimeUtils;
+
+/**
+ * @author hanl
+ * @date 19/05/2014
+ */
+public class JWTSigner {
+
+    private static Logger jlog = LogManager.getLogger(JWTSigner.class);
+    public static boolean DEBUG = false;
+
+    private URL issuer;
+    private JWSSigner signer;
+    private JWSVerifier verifier;
+    private final int defaultttl;
+
+    public JWTSigner (final byte[] secret, URL issuer, final int defaulttl)
+            throws JOSEException {
+        this.issuer = issuer;
+        this.signer = new MACSigner(secret);
+        this.verifier = new MACVerifier(secret);
+        this.defaultttl = defaulttl;
+    }
+
+    public JWTSigner (final byte[] secret, String issuer)
+            throws MalformedURLException, JOSEException {
+        this(secret, new URL(issuer), 72 * 60 * 60);
+    }
+
+    public SignedJWT createJWT (User user, Map<String, Object> attr) {
+        return signContent(user, attr, defaultttl);
+    }
+
+    public SignedJWT signContent (User user, Map<String, Object> attr,
+            int ttl) {
+        String scopes;
+
+        Builder csBuilder = new JWTClaimsSet.Builder();
+        csBuilder.issuer(this.issuer.toString());
+
+        if ((scopes = (String) attr.get(Attributes.SCOPE)) != null) {
+            Userdata data = new GenericUserData();
+            data.readQuietly(attr, false);
+            Scopes claims = Scopes.mapScopes(scopes, data);
+            Map<String, Object> map = claims.toMap();
+            for (String key : map.keySet()) {
+                csBuilder.claim(key, map.get(key));
+            }
+        }
+
+        csBuilder.subject(user.getUsername());
+        if (attr.get(Attributes.CLIENT_ID) != null) {
+            csBuilder.audience((String) attr.get(Attributes.CLIENT_ID));
+        }
+        csBuilder.expirationTime(TimeUtils.getNow().plusSeconds(ttl).toDate());
+        csBuilder.claim(Attributes.AUTHENTICATION_TIME,
+                attr.get(Attributes.AUTHENTICATION_TIME));
+
+        JWTClaimsSet jwtClaimsSet = csBuilder.build();
+        if (DEBUG)
+            jlog.debug(jwtClaimsSet.getClaim(Attributes.AUTHENTICATION_TIME));
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
+                jwtClaimsSet);
+        try {
+            signedJWT.sign(signer);
+        }
+        catch (JOSEException e) {
+            e.printStackTrace();
+        }
+        return signedJWT;
+    }
+
+    /**
+     * @param username
+     * @param json
+     * @return
+     */
+    public SignedJWT signContent (String username, String userclient,
+            String json, int ttl) {
+        Builder cs = new JWTClaimsSet.Builder();
+        cs.subject(username);
+        if (!json.isEmpty())
+            cs.claim("data", json);
+        cs.expirationTime(TimeUtils.getNow().plusSeconds(ttl).toDate());
+        cs.issuer(this.issuer.toString());
+
+        if (!userclient.isEmpty())
+            cs.claim("userip", userclient);
+
+        SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
+                cs.build());
+        try {
+            signedJWT.sign(signer);
+        }
+        catch (JOSEException e) {
+            return null;
+        }
+        return signedJWT;
+    }
+
+    public SignedJWT signContent (String username, String userclient,
+            String json) {
+        return signContent(username, userclient, json, defaultttl);
+    }
+
+    public SignedJWT createSignedToken (String username) {
+        return createSignedToken(username, defaultttl);
+    }
+
+    // add client info
+    public SignedJWT createSignedToken (String username, int ttl) {
+        return signContent(username, "", "", ttl);
+    }
+
+    public SignedJWT verifyToken (String token) throws KustvaktException {
+        SignedJWT client;
+        try {
+            client = SignedJWT.parse(token);
+            if (!client.verify(verifier))
+                throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+                        "Json Web Signature (JWS) object verification failed.");
+
+            if (!new DateTime(client.getJWTClaimsSet().getExpirationTime())
+                    .isAfterNow())
+                throw new KustvaktException(StatusCodes.EXPIRED,
+                        "Authentication token is expired", token);
+        }
+        catch (ParseException | JOSEException e) {
+            // todo: message or entity, how to treat??!
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+                    "Token could not be verified", token);
+        }
+        return client;
+    }
+
+    // does not care about expiration times
+    public String retrieveContent (String signedContent)
+            throws KustvaktException {
+        SignedJWT jwt;
+        try {
+            jwt = SignedJWT.parse(signedContent);
+            if (!jwt.verify(verifier))
+                throw new KustvaktException(StatusCodes.INVALID_REQUEST,
+                        "token invalid", signedContent);
+            return jwt.getJWTClaimsSet().getStringClaim("data");
+        }
+        catch (ParseException | JOSEException e) {
+            return null;
+        }
+    }
+
+    public TokenContext getTokenContext (String idtoken)
+            throws ParseException, JOSEException, KustvaktException {
+        SignedJWT signedJWT = verifyToken(idtoken);
+
+        TokenContext c = new TokenContext();
+        c.setUsername(signedJWT.getJWTClaimsSet().getSubject());
+        List<String> audienceList = signedJWT.getJWTClaimsSet().getAudience();
+        if (audienceList != null && !audienceList.isEmpty())
+            c.addContextParameter(Attributes.CLIENT_ID,
+                    signedJWT.getJWTClaimsSet().getAudience().get(0));
+        c.setExpirationTime(
+                signedJWT.getJWTClaimsSet().getExpirationTime().getTime());
+
+        Instant instant = Instant.ofEpochMilli((long) signedJWT
+                .getJWTClaimsSet().getClaim(Attributes.AUTHENTICATION_TIME));
+        ZonedDateTime zonedAuthTime = ZonedDateTime.ofInstant(instant,
+                ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+        c.setAuthenticationTime(zonedAuthTime);
+        c.setToken(idtoken);
+        c.addParams(signedJWT.getJWTClaimsSet().getClaims());
+        return c;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/KustvaktCacheable.java b/src/main/java/de/ids_mannheim/korap/config/KustvaktCacheable.java
new file mode 100644
index 0000000..c8971ea
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/KustvaktCacheable.java
@@ -0,0 +1,120 @@
+package de.ids_mannheim.korap.config;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import de.ids_mannheim.korap.utils.ServiceInfo;
+import de.ids_mannheim.korap.utils.StringUtils;
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+import net.sf.ehcache.config.CacheConfiguration;
+import net.sf.ehcache.config.PersistenceConfiguration;
+import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
+
+/**
+ * @author hanl
+ * @date 03/02/2016
+ */
+public abstract class KustvaktCacheable {
+
+    private static boolean loaded = false;
+    private String prefix;
+    private String name;
+
+    public KustvaktCacheable (String cache_name, String prefix) {
+        init();
+        if (!enabled())
+            createDefaultFileCache(cache_name);
+        this.prefix = prefix;
+        this.name = cache_name;
+    }
+
+    public KustvaktCacheable () {
+        // TODO Auto-generated constructor stub
+    }
+
+    private static Cache getCache (String name) {
+        return CacheManager.getInstance().getCache(name);
+    }
+
+    private void createDefaultFileCache (String name) {
+        Cache default_cache = new Cache(new CacheConfiguration(name, 20000)
+                .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
+                .eternal(false).timeToLiveSeconds(15000).timeToIdleSeconds(5000)
+                .diskExpiryThreadIntervalSeconds(0)
+                .persistence(new PersistenceConfiguration().strategy(
+                        PersistenceConfiguration.Strategy.LOCALTEMPSWAP)));
+        if (!CacheManager.getInstance().cacheExists(name))
+            CacheManager.getInstance().addCache(default_cache);
+    }
+
+    public void init () {
+        if (!loaded) {
+            if (ServiceInfo.getInfo().getCacheable()) {
+                String file = "ehcache.xml";
+                InputStream in = ConfigLoader.loadConfigStream(file);
+                CacheManager.newInstance(in);
+                loaded = true;
+            }
+            else {
+                CacheManager.create();
+            }
+        }
+    }
+
+    public boolean hasCacheEntry (Object key) {
+        return getCache(this.name).isKeyInCache(createKey(key.toString()));
+    }
+
+    public boolean enabled () {
+        // check that caching is enabled
+        return ServiceInfo.getInfo().getCacheable();
+    }
+
+    public Object getCacheValue (Object key) {
+        Element e = getCache(this.name).get(createKey(key.toString()));
+        if (e != null)
+            return e.getObjectValue();
+        return null;
+    }
+
+    public long getCacheCreationTime (Object key) {
+        Element e = getCache(this.name).get(createKey(key.toString()));
+        if (e != null)
+            return e.getCreationTime();
+        return -1;
+    }
+
+    public void storeInCache (Object key, Object value) {
+        getCache(this.name).put(new Element(createKey(key.toString()), value));
+    }
+
+    public void removeCacheEntry (Object key) {
+        getCache(this.name).remove(createKey(key.toString()));
+    }
+
+    public void clearCache () {
+        Cache c = getCache(this.name);
+        if (enabled()) {
+            c.removeAll();
+            //            c.clearStatistics();
+
+        }
+    }
+
+    private String createKey (String input) {
+        return StringUtils.toSHAHash(this.prefix + "@" + input);
+    }
+
+    public Map<Object, Element> getAllCacheElements () {
+        Cache cache = getCache(name);
+        return cache.getAll(cache.getKeysWithExpiryCheck());
+    }
+
+    public List getKeysWithExpiryCheck () {
+        Cache cache = getCache(name);
+        return cache.getKeysWithExpiryCheck();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/KustvaktClassLoader.java b/src/main/java/de/ids_mannheim/korap/config/KustvaktClassLoader.java
new file mode 100644
index 0000000..860d373
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/KustvaktClassLoader.java
@@ -0,0 +1,51 @@
+package de.ids_mannheim.korap.config;
+
+import org.reflections.Reflections;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Set;
+
+/**
+ * @author hanl
+ * @date 10/06/2015
+ */
+public class KustvaktClassLoader {
+
+    private static final Reflections reflections = new Reflections(
+            "de.ids_mannheim.korap");
+
+    private KustvaktClassLoader () {}
+
+    /**
+     * loads interface implementations in current classpath
+     * 
+     * @param iface
+     * @param <T>
+     * @return
+     */
+    public static <T> Set<Class<? extends T>> loadSubTypes (Class<T> iface) {
+        return reflections.getSubTypesOf(iface);
+    }
+
+    public static Set<Class<?>> loadFromAnnotation (
+            Class<? extends Annotation> annotation) {
+        return reflections.getTypesAnnotatedWith(annotation);
+    }
+
+    public static <T> Class<? extends T> getTypeClass (Class type,
+            Class<T> iface) {
+        Set<Class<? extends T>> c = KustvaktClassLoader.loadSubTypes(iface);
+        for (Class<? extends T> o : c) {
+            Type ctype = o.getGenericInterfaces()[0];
+            if (ctype instanceof ParameterizedType) {
+                ParameterizedType ptype = (ParameterizedType) ctype;
+                Class tclass = (Class) ptype.getActualTypeArguments()[0];
+                if (tclass.equals(type))
+                    return o;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java b/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
new file mode 100644
index 0000000..b838073
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
@@ -0,0 +1,280 @@
+package de.ids_mannheim.korap.config;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import de.ids_mannheim.korap.util.KrillProperties;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes configuration for Kustvakt by importing properties
+ * from kustvakt.conf file and setting default values if they are
+ * not configured.
+ * 
+ * MH: if configuration class is extended, loadSubTypes method should
+ * be
+ * overriden
+ * 
+ * @author hanl
+ * @author margaretha
+ */
+
+@Setter
+@Getter
+public class KustvaktConfiguration {
+
+    public static final Map<String, Object> KUSTVAKT_USER = new HashMap<>();
+    public static final String DATA_FOLDER = "data";
+
+    private String vcInCaching;
+
+    private String indexDir;
+    private int port;
+    // todo: make exclusive so that the containg languages can really
+    // only be used then
+    private List<String> queryLanguages;
+
+    private String serverHost;
+
+    private int maxhits;
+    private int returnhits;
+    private String keystoreLocation;
+    private String keystorePassword;
+    private Properties mailProperties;
+    private String host;
+    private String shibUserMapping;
+    private String userConfig;
+    private int inactiveTime;
+    private int loginAttemptTTL;
+    private long loginAttemptNum;
+    private boolean allowMultiLogIn;
+    private int loadFactor;
+    @Deprecated
+    private int validationStringLength;
+    @Deprecated
+    private int validationEmaillength;
+
+    private byte[] sharedSecret;
+    private int longTokenTTL;
+    private int tokenTTL;
+    private int shortTokenTTL;
+    private String[] rewrite_strategies;
+
+    private String default_pos;
+    private String default_morphology;
+    private String default_lemma;
+    private String default_orthography;
+    private String default_dep;
+    private String default_const;
+    private String apiWelcomeMessage;
+    private String defaultStructureFoundry;
+    private ArrayList<String> foundries;
+    private ArrayList<String> layers;
+
+    private String baseURL;
+    private Properties properties;
+
+    private Set<String> supportedVersions;
+    private String currentVersion;
+
+    // deprec?!
+    private final BACKENDS DEFAULT_ENGINE = BACKENDS.LUCENE;
+    private String networkEndpointURL;
+
+    // license patterns
+    protected Pattern publicLicensePattern;
+    protected Pattern freeLicensePattern;
+    protected Pattern allLicensePattern;
+
+    // random code generator
+    private String secureRandomAlgorithm;
+    private String messageDigestAlgorithm;
+
+    // EM: metadata restriction
+    // another variable might be needed to define which metadata fields are restricted 
+    private boolean isMetadataRestricted = false;
+
+    // EM: Maybe needed when we support pipe registration
+    @Deprecated
+    public static Map<String, String> pipes = new HashMap<>();
+
+    public KustvaktConfiguration (Properties properties) throws Exception {
+        load(properties);
+        //        readPipesFile("pipes");
+        KrillProperties.setProp(properties);
+    }
+
+    public KustvaktConfiguration () {}
+
+    public void loadBasicProperties (Properties properties) {
+        port = Integer.valueOf(properties.getProperty("server.port", "8095"));
+        baseURL = properties.getProperty("kustvakt.base.url", "/api/*");
+        setSecureRandomAlgorithm(
+                properties.getProperty("security.secure.random.algorithm", ""));
+        setMessageDigestAlgorithm(
+                properties.getProperty("security.md.algorithm", "MD5"));
+    }
+
+    /**
+     * loading of the properties and mapping to parameter variables
+     * 
+     * @param properties
+     * @return
+     * @throws Exception
+     */
+    protected void load (Properties properties) throws Exception {
+        loadBasicProperties(properties);
+
+        apiWelcomeMessage = properties.getProperty("api.welcome.message",
+                "Welcome to KorAP API!");
+        currentVersion = properties.getProperty("current.api.version", "v1.0");
+
+        String supportedVersions = properties
+                .getProperty("supported.api.versions", "");
+
+        this.supportedVersions = new HashSet<>();
+        if (!supportedVersions.isEmpty()) {
+            List<String> versionArray = Arrays
+                    .asList(supportedVersions.split(" "));
+            this.supportedVersions.addAll(versionArray);
+        }
+        this.supportedVersions.add(currentVersion);
+
+        maxhits = Integer.valueOf(properties.getProperty("maxhits", "50000"));
+        returnhits = Integer
+                .valueOf(properties.getProperty("returnhits", "50000"));
+        indexDir = properties.getProperty("krill.indexDir", "");
+
+        // server options
+        serverHost = String
+                .valueOf(properties.getProperty("server.host", "localhost"));
+        String queries = properties.getProperty("korap.ql", "");
+        String[] qls = queries.split(",");
+        queryLanguages = new ArrayList<>();
+        for (String querylang : qls)
+            queryLanguages.add(querylang.trim().toUpperCase());
+
+        default_const = properties.getProperty("default.foundry.constituent",
+                "corenlp");
+        default_dep = properties.getProperty("default.foundry.dependency",
+                "malt");
+        default_lemma = properties.getProperty("default.foundry.lemma", "tt");
+        default_morphology = properties
+                .getProperty("default.foundry.morphology", "marmot");
+        default_pos = properties.getProperty("default.foundry.partOfSpeech",
+                "tt");
+        default_orthography = properties
+                .getProperty("default.foundry.orthography", "opennlp");
+        defaultStructureFoundry = properties
+                .getProperty("default.foundry.structure", "base");
+
+        // security configuration
+        inactiveTime = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.idleTimeoutDuration", "10M"));
+        allowMultiLogIn = Boolean
+                .valueOf(properties.getProperty("security.multipleLogIn"));
+
+        loginAttemptNum = Long.parseLong(
+                properties.getProperty("security.loginAttemptNum", "3"));
+        loginAttemptTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.authAttemptTTL", "30M"));
+
+        loadFactor = Integer.valueOf(
+                properties.getProperty("security.encryption.loadFactor", "15"));
+        validationStringLength = Integer.valueOf(properties
+                .getProperty("security.validation.stringLength", "150"));
+        validationEmaillength = Integer.valueOf(properties
+                .getProperty("security.validation.emailLength", "40"));
+
+        sharedSecret = properties.getProperty("security.sharedSecret", "")
+                .getBytes();
+
+        longTokenTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.longTokenTTL", "100D"));
+        tokenTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.tokenTTL", "72H"));
+        shortTokenTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.shortTokenTTL", "3H"));
+
+        // network endpoint
+        networkEndpointURL = properties.getProperty("network.endpoint.url", "");
+    }
+
+    @Deprecated
+    public void readPipesFile (String filename) throws IOException {
+        File file = new File(filename);
+        if (file.exists()) {
+            BufferedReader br = new BufferedReader(
+                    new InputStreamReader(new FileInputStream(file)));
+
+            String line = null;
+            while ((line = br.readLine()) != null) {
+                String[] parts = line.split("\t");
+                if (parts.length != 2) {
+                    continue;
+                }
+                else {
+                    pipes.put(parts[0], parts[1]);
+                }
+            }
+            br.close();
+        }
+    }
+
+    /**
+     * set properties
+     * 
+     * @param props
+     * @throws IOException
+     */
+    // public void setProperties (Properties props) throws IOException
+    // {
+    // this.load(props);
+    // }
+
+    /**
+     * properties can be overloaded after spring init
+     * 
+     * @param stream
+     * @throws Exception
+     */
+    public void setPropertiesAsStream (InputStream stream) throws Exception {
+        try {
+
+            Properties p = new Properties();
+            p.load(stream);
+            this.load(p);
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    public BACKENDS chooseBackend (String value) {
+        if (value == null || value.equals("null"))
+            return DEFAULT_ENGINE;
+        else
+            return Enum.valueOf(BACKENDS.class, value.toUpperCase());
+    }
+
+    public enum BACKENDS {
+        NEO4J, LUCENE, NETWORK
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java b/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
new file mode 100644
index 0000000..fda0ee8
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
@@ -0,0 +1,203 @@
+package de.ids_mannheim.korap.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.zip.GZIPInputStream;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.cache.VirtualCorpusCache;
+import de.ids_mannheim.korap.constant.QueryType;
+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.service.QueryService;
+import de.ids_mannheim.korap.util.QueryException;
+import de.ids_mannheim.korap.web.SearchKrill;
+
+/**
+ * <p>Loads predefined virtual corpora at server start up and caches
+ * them, if the VC have not been cached before. If there are changes
+ * in the index, the cache will be updated.
+ * </p>
+ * 
+ * <p>
+ * All predefined VC are set as SYSTEM VC. The filenames are used as
+ * VC names. Acceptable file extensions are .jsonld.gz or .jsonld. The
+ * VC should be located at the folder indicated by
+ * <em>krill.namedVC</em>
+ * specified in kustvakt.conf.
+ * </p>
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class NamedVCLoader implements Runnable {
+    @Autowired
+    private FullConfiguration config;
+    @Autowired
+    private SearchKrill searchKrill;
+    @Autowired
+    private QueryService vcService;
+
+    public static Logger jlog = LogManager.getLogger(NamedVCLoader.class);
+    public static boolean DEBUG = false;
+
+    @Override
+    public void run () {
+        try {
+            loadVCToCache();
+        }
+        catch (IOException | QueryException e) {
+            //            e.printStackTrace();
+            throw new RuntimeException(e.getMessage(), e.getCause());
+        }
+    }
+
+    /**
+     * Used for testing
+     * 
+     * @param filename
+     * @param filePath
+     * @throws IOException
+     * @throws QueryException
+     * @throws KustvaktException
+     */
+    public void loadVCToCache (String filename, String filePath)
+            throws IOException, QueryException, KustvaktException {
+
+        InputStream is = NamedVCLoader.class.getResourceAsStream(filePath);
+        String json = IOUtils.toString(is, "utf-8");
+        if (json != null) {
+            cacheVC(filename, json);
+            vcService.storeQuery("system", filename, ResourceType.SYSTEM,
+                    QueryType.VIRTUAL_CORPUS, json, null, null, null, true,
+                    "system", null, null);
+        }
+    }
+
+    public void loadVCToCache () throws IOException, QueryException {
+
+        String dir = config.getNamedVCPath();
+        if (dir.isEmpty())
+            return;
+
+        File d = new File(dir);
+        if (!d.isDirectory()) {
+            throw new IOException("Directory " + dir + " is not valid");
+        }
+
+        jlog.info(Arrays.toString(d.list()));
+
+        for (File file : d.listFiles()) {
+            if (!file.exists()) {
+                throw new IOException("File " + file + " is not found.");
+            }
+
+            String filename = file.getName();
+            String[] strArr = readFile(file, filename);
+            filename = strArr[0];
+            String json = strArr[1];
+            if (json != null) {
+                cacheVC(filename, json);
+                storeVCinDB(filename, json);
+            }
+        }
+    }
+
+    private String[] readFile (File file, String filename) throws IOException {
+        String json = null;
+        long start = System.currentTimeMillis();
+        if (filename.endsWith(".jsonld")) {
+            filename = filename.substring(0, filename.length() - 7);
+            json = FileUtils.readFileToString(file, "utf-8");
+        }
+        else if (filename.endsWith(".jsonld.gz")) {
+            filename = filename.substring(0, filename.length() - 10);
+            GZIPInputStream gzipInputStream = new GZIPInputStream(
+                    new FileInputStream(file));
+            ByteArrayOutputStream bos = new ByteArrayOutputStream(512);
+            bos.write(gzipInputStream);
+            json = bos.toString("utf-8");
+            bos.close();
+        }
+        else {
+            System.err.println("File " + filename
+                    + " is not allowed. Filename must ends with .jsonld or .jsonld.gz");
+        }
+        long end = System.currentTimeMillis();
+        if (DEBUG) {
+            jlog.debug("READ " + filename + " duration: " + (end - start));
+        }
+
+        return new String[] { filename, json };
+    }
+
+    /**
+     * Caches the given VC if the VC is not found in cache and updates
+     * the VC if it exists and there are changes in the index.
+     * 
+     * @param vcId
+     *            vc-name
+     * @param koralQuery
+     * @throws IOException
+     * @throws QueryException
+     */
+    private void cacheVC (String vcId, String koralQuery)
+            throws IOException, QueryException {
+        config.setVcInCaching(vcId);
+        if (VirtualCorpusCache.contains(vcId)) {
+            jlog.info("Checking {} in cache ", vcId);
+        }
+        else {
+            jlog.info("Storing {} in cache ", vcId);
+        }
+
+        long start, end;
+        start = System.currentTimeMillis();
+        VirtualCorpusCache.store(vcId, searchKrill.getIndex());
+        end = System.currentTimeMillis();
+        jlog.info("Duration : {}", (end - start));
+        config.setVcInCaching("");
+    }
+
+    /**
+     * Stores the VC if it doesn't exist in the database.
+     * 
+     * @param vcId
+     * @param koralQuery
+     */
+    private void storeVCinDB (String vcId, String koralQuery) {
+        try {
+            vcService.searchQueryByName("system", vcId, "system",
+                    QueryType.VIRTUAL_CORPUS);
+        }
+        catch (KustvaktException e) {
+            if (e.getStatusCode() == StatusCodes.NO_RESOURCE_FOUND) {
+                try {
+                    jlog.info("Storing {} in database ", vcId);
+                    vcService.storeQuery("system", vcId, ResourceType.SYSTEM,
+                            QueryType.VIRTUAL_CORPUS, koralQuery, null, null,
+                            null, true, "system", null, null);
+                }
+                catch (KustvaktException e1) {
+                    throw new RuntimeException(e1);
+                }
+            }
+            else {
+                throw new RuntimeException(e);
+            }
+        }
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/ParamFields.java b/src/main/java/de/ids_mannheim/korap/config/ParamFields.java
new file mode 100644
index 0000000..621bffb
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/ParamFields.java
@@ -0,0 +1,41 @@
+package de.ids_mannheim.korap.config;
+
+import lombok.Getter;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * @author hanl
+ * @date 21/07/2015
+ */
+// could also be an array or list!
+public class ParamFields extends HashMap<String, ParamFields.Param> {
+
+    public void add (Param param) {
+        this.put(param.getClass().getName(), param);
+    }
+
+    public <T extends Param> T get (Class<T> cl) {
+        return (T) this.get(cl.getName());
+    }
+
+    public <T extends Param> T remove (Class<T> cl) {
+        return (T) this.remove(cl.getName());
+    }
+
+    public void addAll (Collection<Param> params) {
+        for (Param p : params)
+            super.put(p.getClass().getName(), p);
+    }
+
+    @Getter
+    public abstract static class Param {
+
+        public boolean hasValues () {
+            return false;
+        }
+
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/QueryBuilderUtil.java b/src/main/java/de/ids_mannheim/korap/config/QueryBuilderUtil.java
new file mode 100644
index 0000000..fb838b1
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/QueryBuilderUtil.java
@@ -0,0 +1,22 @@
+package de.ids_mannheim.korap.config;
+
+import de.ids_mannheim.korap.query.serialize.MetaQueryBuilder;
+
+/**
+ * @author hanl
+ * @date 25/06/2015
+ */
+public class QueryBuilderUtil {
+
+    public static MetaQueryBuilder defaultMetaBuilder (Integer pageIndex,
+            Integer pageInteger, Integer pageLength, String ctx,
+            Boolean cutoff) {
+        MetaQueryBuilder meta = new MetaQueryBuilder();
+        meta.addEntry("startIndex", pageIndex)
+                .addEntry("startPage", pageInteger)
+                .addEntry("count", pageLength).setSpanContext(ctx)
+                .addEntry("cutOff", cutoff);
+        return meta;
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/Scopes.java b/src/main/java/de/ids_mannheim/korap/config/Scopes.java
new file mode 100644
index 0000000..295cb4d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/Scopes.java
@@ -0,0 +1,87 @@
+package de.ids_mannheim.korap.config;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.Userdata;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 09/12/2014
+ */
+public class Scopes {
+
+    public enum Scope {
+        profile, email, queries, account, preferences, search
+
+    }
+
+    private static final String[] profile = { Attributes.EMAIL,
+            Attributes.FIRSTNAME, Attributes.LASTNAME, Attributes.INSTITUTION,
+            Attributes.ADDRESS, Attributes.PHONE, Attributes.GENDER,
+            Attributes.COUNTRY };
+
+    private static final Enum[] SERVICE_DEFAULTS = { Scope.account,
+            Scope.preferences, Scope.search, Scope.queries };
+
+    public static Scopes getProfileScopes (Userdata values) {
+        Scopes r = new Scopes();
+        for (String key : profile) {
+            Object v = values.get(key);
+            if (v != null)
+                r.values.put(key, v);
+        }
+        return r;
+    }
+
+    /**
+     * expects space separated values
+     * 
+     * @param scopes
+     * @return
+     */
+    //todo: test
+    public static Scope[] mapScopes (String scopes) {
+        List<Enum> s = new ArrayList<>();
+        for (String value : scopes.split(" "))
+            s.add(Scope.valueOf(value.toLowerCase()));
+        return s.toArray(new Scope[s.size()]);
+    }
+
+    public static Scopes mapScopes (String scopes, Userdata details) {
+        Scopes m = new Scopes();
+        if (scopes != null && !scopes.isEmpty()) {
+            Scope[] scopearr = mapScopes(scopes);
+            for (Scope s : scopearr) {
+                Object v = details.get(s.toString());
+                if (v != null)
+                    m.values.put(s.toString(), v);
+            }
+            if (scopes.contains(Scope.profile.toString()))
+                m.values.putAll(Scopes.getProfileScopes(details).values);
+            m.values.put(Attributes.SCOPE, scopes);
+        }
+        return m;
+    }
+
+    private Map<String, Object> values;
+
+    private Scopes () {
+        this.values = new HashMap<>();
+    }
+
+    public String toEntity () throws KustvaktException {
+        if (this.values.isEmpty())
+            return "";
+        return JsonUtils.toJSON(this.values);
+    }
+
+    public Map<String, Object> toMap () {
+        return new HashMap<>(this.values);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/URIParam.java b/src/main/java/de/ids_mannheim/korap/config/URIParam.java
new file mode 100644
index 0000000..9cba8b6
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/URIParam.java
@@ -0,0 +1,26 @@
+package de.ids_mannheim.korap.config;
+
+import lombok.Getter;
+
+/**
+ * @author hanl
+ * @date 15/07/15
+ */
+@Getter
+public class URIParam extends ParamFields.Param {
+
+    private final String uriFragment;
+    private final Long uriExpiration;
+
+    public URIParam (String uri, Long expire) {
+        this.uriFragment = uri;
+        this.uriExpiration = expire;
+    }
+
+    @Override
+    public boolean hasValues () {
+        return this.uriFragment != null && !this.uriFragment.isEmpty()
+                && this.uriExpiration != null;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/AnnotationType.java b/src/main/java/de/ids_mannheim/korap/constant/AnnotationType.java
new file mode 100644
index 0000000..2c6c64d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/AnnotationType.java
@@ -0,0 +1,14 @@
+package de.ids_mannheim.korap.constant;
+
+/**
+ * Defines various annotation types as constants
+ * 
+ * @author margaretha
+ *
+ */
+public class AnnotationType {
+    public static String FOUNDRY = "foundry";
+    public static String LAYER = "layer";
+    public static String KEY = "key";
+    public static String VALUE = "value";
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java b/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java
new file mode 100644
index 0000000..488c676
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java
@@ -0,0 +1,19 @@
+package de.ids_mannheim.korap.constant;
+
+/**
+ * Lists possible actual authentication methods. Multiple
+ * {@link AuthenticationScheme} may use an identical
+ * authentication method.
+ * 
+ * @author margaretha
+ * 
+ * @see AuthenticationScheme
+ *
+ */
+public enum AuthenticationMethod {
+    LDAP,
+    // not available
+    SHIBBOLETH, DATABASE,
+    // by pass authentication for testing
+    TEST;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/AuthenticationScheme.java b/src/main/java/de/ids_mannheim/korap/constant/AuthenticationScheme.java
new file mode 100644
index 0000000..e3068f7
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/AuthenticationScheme.java
@@ -0,0 +1,24 @@
+package de.ids_mannheim.korap.constant;
+
+import org.apache.commons.lang.WordUtils;
+
+/**
+ * Lists possible authentication schemes used in the Authorization
+ * header
+ * of HTTP requests.
+ * 
+ * @author margaretha
+ *
+ */
+public enum AuthenticationScheme {
+    // standard http
+    BASIC, BEARER,
+    // custom
+    // SESSION, has not been supported yet 
+    @Deprecated
+    API;
+
+    public String displayName () {
+        return WordUtils.capitalizeFully(name());
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/GroupMemberStatus.java b/src/main/java/de/ids_mannheim/korap/constant/GroupMemberStatus.java
new file mode 100644
index 0000000..445e31e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/GroupMemberStatus.java
@@ -0,0 +1,17 @@
+package de.ids_mannheim.korap.constant;
+
+/**
+ * Defines possible statuses of a user-group member
+ * 
+ * @author margaretha
+ *
+ */
+public enum GroupMemberStatus {
+    ACTIVE,
+    // membership invitation was sent and has not been accepted 
+    // or rejected yet
+    PENDING,
+    // either membership invitation was rejected or the member was 
+    // deleted by a user-group admin
+    DELETED;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java b/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java
new file mode 100644
index 0000000..f24956b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java
@@ -0,0 +1,40 @@
+package de.ids_mannheim.korap.constant;
+
+/**
+ * Defines all possible authorization scopes
+ * 
+ * @author margaretha
+ *
+ */
+public enum OAuth2Scope {
+
+    ALL, @Deprecated
+    ADMIN,
+
+    AUTHORIZE,
+
+    LIST_USER_CLIENT, INSTALL_USER_CLIENT, UNINSTALL_USER_CLIENT,
+
+    CLIENT_INFO, REGISTER_CLIENT, DEREGISTER_CLIENT, RESET_CLIENT_SECRET,
+
+    SEARCH, SERIALIZE_QUERY, MATCH_INFO,
+
+    USER_INFO,
+
+    USER_GROUP_INFO, CREATE_USER_GROUP, DELETE_USER_GROUP,
+
+    DELETE_USER_GROUP_MEMBER, ADD_USER_GROUP_MEMBER,
+
+    EDIT_USER_GROUP_MEMBER_ROLE, ADD_USER_GROUP_MEMBER_ROLE, DELETE_USER_GROUP_MEMBER_ROLE,
+
+    CREATE_VC, VC_INFO, EDIT_VC, DELETE_VC,
+
+    SHARE_VC, DELETE_VC_ACCESS, VC_ACCESS_INFO,
+
+    CREATE_DEFAULT_SETTING, READ_DEFAULT_SETTING, DELETE_DEFAULT_SETTING;
+
+    @Override
+    public String toString () {
+        return super.toString().toLowerCase();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java b/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java
new file mode 100644
index 0000000..f91e511
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java
@@ -0,0 +1,30 @@
+package de.ids_mannheim.korap.constant;
+
+/**
+ * Defines some predefined roles used in the system.
+ * 
+ * @author margaretha
+ *
+ */
+public enum PredefinedRole {
+    USER_GROUP_ADMIN(1), USER_GROUP_MEMBER(2), VC_ACCESS_ADMIN(
+            3), VC_ACCESS_MEMBER(
+                    4), QUERY_ACCESS_ADMIN(5), QUERY_ACCESS_MEMBER(6);
+
+    private int id;
+    private String name;
+
+    PredefinedRole (int i) {
+        this.id = i;
+        this.name = name().toLowerCase().replace("_", " ");
+    }
+
+    public int getId () {
+        return id;
+    }
+
+    @Override
+    public String toString () {
+        return this.name;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java b/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java
new file mode 100644
index 0000000..0466fa2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java
@@ -0,0 +1,16 @@
+package de.ids_mannheim.korap.constant;
+
+import de.ids_mannheim.korap.entity.Privilege;
+import de.ids_mannheim.korap.entity.Role;
+
+/**
+ * Defines the privilege or permissions of users or admins
+ * based on their roles.
+ * 
+ * @author margaretha
+ * @see Privilege
+ * @see Role
+ */
+public enum PrivilegeType {
+    READ, WRITE, DELETE;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/QueryAccessStatus.java b/src/main/java/de/ids_mannheim/korap/constant/QueryAccessStatus.java
new file mode 100644
index 0000000..3b4f786
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/QueryAccessStatus.java
@@ -0,0 +1,20 @@
+package de.ids_mannheim.korap.constant;
+
+import de.ids_mannheim.korap.entity.QueryAccess;
+
+/**
+ * Defines possible statuses of {@link QueryAccess}
+ * 
+ * @author margaretha
+ * @see QueryAccess
+ *
+ */
+public enum QueryAccessStatus {
+
+    ACTIVE, DELETED,
+    // has not been used yet
+    PENDING,
+    // access for hidden group
+    // maybe not necessary?
+    HIDDEN;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/QueryType.java b/src/main/java/de/ids_mannheim/korap/constant/QueryType.java
new file mode 100644
index 0000000..5251bde
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/QueryType.java
@@ -0,0 +1,12 @@
+package de.ids_mannheim.korap.constant;
+
+import org.apache.commons.lang.StringUtils;
+
+public enum QueryType {
+
+    QUERY, VIRTUAL_CORPUS;
+
+    public String displayName () {
+        return StringUtils.capitalize(name().toLowerCase().replace("_", " "));
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/ResourceType.java b/src/main/java/de/ids_mannheim/korap/constant/ResourceType.java
new file mode 100644
index 0000000..327e0ab
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/ResourceType.java
@@ -0,0 +1,40 @@
+package de.ids_mannheim.korap.constant;
+
+import de.ids_mannheim.korap.entity.QueryDO;
+
+/**
+ * Defines types of {@link QueryDO}
+ * 
+ * @author margaretha
+ *
+ */
+/*
+ * TODO (nd):
+ *   This should probably be renamed to something like RessourceType,
+ *   as QueryReferences will use the same types.
+ */
+public enum ResourceType {
+    /**
+     * available for all
+     */
+    SYSTEM,
+    // 
+    /**
+     * available to project group members
+     * 
+     */
+    PROJECT,
+    /**
+     * available only for the creator
+     */
+    PRIVATE,
+    /**
+     * available for all, but not listed for all
+     */
+    PUBLISHED;
+
+    public String displayName () {
+        return name().toLowerCase();
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/TokenType.java b/src/main/java/de/ids_mannheim/korap/constant/TokenType.java
new file mode 100644
index 0000000..d238ac2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/TokenType.java
@@ -0,0 +1,26 @@
+package de.ids_mannheim.korap.constant;
+
+import org.apache.commons.lang.StringUtils;
+
+import de.ids_mannheim.korap.security.context.TokenContext;
+
+/**
+ * Defines the types of authentication tokens. Token types are used to
+ * create {@link TokenContext} and determine which authentication
+ * provider
+ * must be used to create a TokenContext and parse given tokens.
+ * 
+ * @author margaretha
+ *
+ */
+public enum TokenType {
+    BASIC, API, SESSION,
+    // OAuth2 access_token
+    BEARER,
+    // OAuth2 client
+    CLIENT;
+
+    public String displayName () {
+        return StringUtils.capitalize(name().toLowerCase());
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/de/ids_mannheim/korap/constant/UserGroupStatus.java b/src/main/java/de/ids_mannheim/korap/constant/UserGroupStatus.java
new file mode 100644
index 0000000..03eedcb
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/constant/UserGroupStatus.java
@@ -0,0 +1,15 @@
+package de.ids_mannheim.korap.constant;
+
+import de.ids_mannheim.korap.entity.UserGroup;
+
+/**
+ * Defines possible statuses of {@link UserGroup}s
+ * 
+ * @author margaretha
+ *
+ */
+public enum UserGroupStatus {
+    ACTIVE, DELETED,
+    // group members cannot see the group
+    HIDDEN;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/entity/Annotation.java b/src/main/java/de/ids_mannheim/korap/core/entity/Annotation.java
new file mode 100644
index 0000000..f29cac9
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/entity/Annotation.java
@@ -0,0 +1,51 @@
+package de.ids_mannheim.korap.core.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes annotation tags available in the system / used in
+ * annotating corpus data.
+ * 
+ * @author margaretha
+ *
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "annotation")
+public class Annotation {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String code;
+    private String type;
+    private String text;
+    private String description;
+    @Column(name = "de_description")
+    private String germanDescription;
+
+    public Annotation () {}
+
+    public Annotation (String code, String type, String text,
+                       String description) {
+        this.code = code;
+        this.type = type;
+        this.text = text;
+        this.description = description;
+    }
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", code= " + code + ", type= " + type
+                + ", description=" + description + ", germanDescription="
+                + germanDescription;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/entity/AnnotationKey.java b/src/main/java/de/ids_mannheim/korap/core/entity/AnnotationKey.java
new file mode 100644
index 0000000..fb3012d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/entity/AnnotationKey.java
@@ -0,0 +1,63 @@
+package de.ids_mannheim.korap.core.entity;
+
+import java.util.Set;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes the annotation key mapping to annotation_key table in the
+ * database and annotation key relations to {@link AnnotationLayer}
+ * and {@link Annotation}.
+ * 
+ * @author margaretha
+ *
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "annotation_key", uniqueConstraints = @UniqueConstraint(columnNames = {
+        "layer_id", "key_id" }))
+public class AnnotationKey {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "key_id")
+    private int keyId;
+    @Column(name = "layer_id")
+    private int layerId;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "layer_id", insertable = false, updatable = false)
+    private AnnotationLayer layer;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "key_id", insertable = false, updatable = false)
+    private Annotation key;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = "annotation_value", joinColumns = @JoinColumn(name = "key_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "value_id", referencedColumnName = "id"), uniqueConstraints = @UniqueConstraint(columnNames = {
+            "key_id", "value_id" }))
+    private Set<Annotation> values;
+
+    public AnnotationKey () {}
+
+    public AnnotationKey (int layerId, int keyId) {
+        this.layerId = layerId;
+        this.keyId = keyId;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/entity/AnnotationLayer.java b/src/main/java/de/ids_mannheim/korap/core/entity/AnnotationLayer.java
new file mode 100644
index 0000000..9beb694
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/entity/AnnotationLayer.java
@@ -0,0 +1,66 @@
+package de.ids_mannheim.korap.core.entity;
+
+import java.util.Set;
+
+import org.hibernate.annotations.Fetch;
+import org.hibernate.annotations.FetchMode;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes annotation layers as a pair of foundry and layer where
+ * foundry denotes where the annotation comes from e.g. Tree tagger
+ * parser, and layer denotes the annotation layer e.g. part of speech.
+ * 
+ * @author margaretha
+ * @see Annotation
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "annotation_layer", uniqueConstraints = @UniqueConstraint(columnNames = {
+        "foundry_id", "layer_id" }))
+public class AnnotationLayer {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "foundry_id")
+    private int foundryId;
+    @Column(name = "layer_id")
+    private int layerId;
+    @Column(name = "description")
+    private String description;
+
+    @Fetch(FetchMode.SELECT)
+    @ManyToOne // (fetch=FetchType.LAZY)
+    @JoinColumn(name = "foundry_id", insertable = false, updatable = false)
+    private Annotation foundry;
+
+    @Fetch(FetchMode.SELECT)
+    @ManyToOne // (fetch=FetchType.LAZY)
+    @JoinColumn(name = "layer_id", insertable = false, updatable = false)
+    private Annotation layer;
+
+    @OneToMany(mappedBy = "layer", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
+    private Set<AnnotationKey> keys;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", foundry=" + foundry + ", layer=" + layer
+                + ", description=" + description + ", keys= " + keys;
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/entity/Resource.java b/src/main/java/de/ids_mannheim/korap/core/entity/Resource.java
new file mode 100644
index 0000000..e002451
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/entity/Resource.java
@@ -0,0 +1,64 @@
+package de.ids_mannheim.korap.core.entity;
+
+import java.util.Set;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes resources having free licenses. Primarily for
+ * accommodating clients in providing data without login such as
+ * KorapSRU.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "resource")
+public class Resource {
+
+    @Id
+    private String id;
+
+    @Column(name = "de_title")
+    private String germanTitle;
+
+    @Column(name = "en_title")
+    private String englishTitle;
+
+    @Column(name = "en_description")
+    private String englishDescription;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = "resource_layer", joinColumns = @JoinColumn(name = "resource_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "layer_id", referencedColumnName = "id"))
+    private Set<AnnotationLayer> layers;
+
+    public Resource () {}
+
+    public Resource (String id, String germanTitle, String englishTitle,
+                     String englishDescription, Set<AnnotationLayer> layers) {
+        this.id = id;
+        this.germanTitle = germanTitle;
+        this.englishTitle = englishTitle;
+        this.englishDescription = englishDescription;
+        this.layers = layers;
+    }
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", germanTitle=" + germanTitle + ", englishTitle="
+                + englishTitle + ", description=" + englishDescription
+                + ", layers= " + layers;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java b/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java
new file mode 100644
index 0000000..e7680e2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/service/AnnotationService.java
@@ -0,0 +1,97 @@
+package de.ids_mannheim.korap.core.service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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.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;
+
+/**
+ * AnnotationService defines the logic behind
+ * {@link AnnotationController}.
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class AnnotationService {
+
+    private static final boolean DEBUG = false;
+
+    private static Logger jlog = LogManager.getLogger(AnnotationService.class);
+
+    @Autowired
+    private AnnotationDao annotationDao;
+
+    @Autowired
+    private AnnotationConverter annotationConverter;
+
+    public List<LayerDto> getLayerDtos () {
+        List<AnnotationLayer> layers = annotationDao.getAllFoundryLayerPairs();
+        if (DEBUG) {
+            jlog.debug("/layers " + layers.toString());
+        }
+        List<LayerDto> layerDto = annotationConverter.convertToLayerDto(layers);
+        return layerDto;
+    }
+
+    public List<FoundryDto> getFoundryDtos (List<String> codes, String language)
+            throws KustvaktException {
+        List<AnnotationLayer> annotationPairs = null;
+        String foundry = "", layer = "";
+        if (codes.contains("*")) {
+            annotationPairs = annotationDao.getAnnotationDescriptions(foundry,
+                    layer);
+        }
+        else {
+            String[] annotationCode;
+            annotationPairs = new ArrayList<AnnotationLayer>();
+            for (String code : codes) {
+                if (DEBUG) {
+                    jlog.debug("code " + code);
+                }
+                annotationCode = code.split("/");
+                if (annotationCode.length == 1) {
+                    foundry = annotationCode[0];
+                }
+                else if (annotationCode.length == 2) {
+                    foundry = annotationCode[0];
+                    layer = annotationCode[1];
+                }
+                else {
+                    jlog.error("Annotation code is wrong: " + annotationCode);
+                    throw new KustvaktException(StatusCodes.INVALID_ATTRIBUTE,
+                            "Bad attribute:", code);
+                }
+
+                annotationPairs.addAll(annotationDao
+                        .getAnnotationDescriptions(foundry, layer));
+            }
+        }
+
+        if (annotationPairs != null && !annotationPairs.isEmpty()) {
+            List<FoundryDto> foundryDtos = annotationConverter
+                    .convertToFoundryDto(annotationPairs, language);
+            if (DEBUG) {
+                jlog.debug("/description " + annotationPairs.toString());
+            }
+            return foundryDtos;
+        }
+        else {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "No result found", "");
+        }
+
+    }
+}
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
new file mode 100644
index 0000000..e833321
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/service/BasicService.java
@@ -0,0 +1,17 @@
+package de.ids_mannheim.korap.core.service;
+
+import java.util.List;
+
+public class BasicService {
+
+    protected String combineMultipleCorpusQuery (List<String> cqList) {
+        String combinedCorpusQuery = null;
+        if (cqList != null && cqList.size() > 0) {
+            combinedCorpusQuery = cqList.get(0);
+            for (int i = 1; i < cqList.size(); i++) {
+                combinedCorpusQuery += "&" + cqList.get(i);
+            }
+        }
+        return combinedCorpusQuery;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/service/SearchNetworkEndpoint.java b/src/main/java/de/ids_mannheim/korap/core/service/SearchNetworkEndpoint.java
new file mode 100644
index 0000000..de6038e
--- /dev/null
+++ b/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/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java b/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
new file mode 100644
index 0000000..03f186d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
@@ -0,0 +1,590 @@
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+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 com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+//import de.ids_mannheim.de.init.VCLoader;
+import de.ids_mannheim.korap.authentication.AuthenticationManager;
+import de.ids_mannheim.korap.config.KustvaktCacheable;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.query.serialize.MetaQueryBuilder;
+import de.ids_mannheim.korap.query.serialize.QuerySerializer;
+import de.ids_mannheim.korap.response.Notifications;
+import de.ids_mannheim.korap.rewrite.RewriteHandler;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.ClientsHandler;
+import de.ids_mannheim.korap.web.SearchKrill;
+import jakarta.annotation.PostConstruct;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.UriBuilder;
+
+@Service
+public class SearchService extends BasicService {
+
+    public class TotalResultCache extends KustvaktCacheable {
+
+        public TotalResultCache () {
+            super("total_results", "key:hashedKoralQuery");
+        }
+    }
+
+    private static final boolean DEBUG = false;
+
+    private static Logger jlog = LogManager.getLogger(SearchService.class);
+
+    @Autowired
+    private KustvaktConfiguration config;
+    @Autowired
+    private AuthenticationManager authenticationManager;
+
+    @Autowired
+    private RewriteHandler rewriteHandler;
+
+    @Autowired
+    private SearchKrill searchKrill;
+    @Autowired
+    private SearchNetworkEndpoint searchNetwork;
+
+    private ClientsHandler graphDBhandler;
+
+    private TotalResultCache totalResultCache;
+
+    @PostConstruct
+    private void doPostConstruct () {
+        UriBuilder builder = UriBuilder.fromUri("http://10.0.10.13").port(9997);
+        this.graphDBhandler = new ClientsHandler(builder.build());
+
+        totalResultCache = new TotalResultCache();
+    }
+
+    public String getKrillVersion () {
+        return searchKrill.getIndex().getVersion();
+
+    }
+
+    @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)
+            throws KustvaktException {
+        QuerySerializer ss = new QuerySerializer().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);
+        return query;
+    }
+
+    private User createUser (String username, HttpHeaders headers)
+            throws KustvaktException {
+        User user = authenticationManager.getUser(username);
+        authenticationManager.setAccessAndLocation(user, headers);
+        if (DEBUG) {
+            if (user != null) {
+                jlog.debug("Debug: user location=" + user.locationtoString()
+                        + ", access=" + user.accesstoString());
+            }
+        }
+        return user;
+    }
+
+    public String search (String jsonld, String username, HttpHeaders headers)
+            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);
+        // 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 (String engine, String username, HttpHeaders headers,
+            String q, String ql, String v, List<String> cqList, String fields,
+            String pipes, Integer pageIndex, Integer pageInteger, String ctx,
+            Integer pageLength, Boolean cutoff, boolean accessRewriteDisabled,
+            boolean showTokens, boolean showSnippet) throws KustvaktException {
+
+        if (pageInteger != null && pageInteger < 1) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "page must start from 1", "page");
+        }
+
+        String[] pipeArray = null;
+        if (pipes != null && !pipes.isEmpty()) {
+            pipeArray = pipes.split(",");
+        }
+
+        User user = createUser(username, headers);
+        CorpusAccess corpusAccess = user.getCorpusAccess();
+
+        // EM: TODO: check if requested fields are public metadata. Currently 
+        // it is not needed because all metadata are public.        
+        if (accessRewriteDisabled) {
+            corpusAccess = CorpusAccess.ALL;
+            user.setCorpusAccess(CorpusAccess.ALL);
+        }
+
+        QuerySerializer serializer = new QuerySerializer();
+        serializer.setQuery(q, ql, v);
+        String cq = combineMultipleCorpusQuery(cqList);
+        if (cq != null)
+            serializer.setCollection(cq);
+
+        List<String> fieldList = convertFieldsToList(fields);
+        handleNonPublicFields(fieldList, accessRewriteDisabled, serializer);
+
+        MetaQueryBuilder meta = createMetaQuery(pageIndex, pageInteger, ctx,
+                pageLength, cutoff, corpusAccess, fieldList,
+                accessRewriteDisabled, showTokens, showSnippet);
+        serializer.setMeta(meta.raw());
+
+        // There is an error in query processing
+        // - either query, corpus or meta
+        if (serializer.hasErrors()) {
+            throw new KustvaktException(serializer.toJSON());
+        }
+
+        String query = serializer.toJSON();
+
+        if (accessRewriteDisabled && showTokens) {
+            Notifications n = new Notifications();
+            n.addWarning(StatusCodes.NOT_ALLOWED,
+                    "Tokens cannot be shown without access.");
+            JsonNode warning = n.toJsonNode();
+            query = addWarning(query, warning);
+        }
+
+        query = runPipes(query, pipeArray);
+
+        query = this.rewriteHandler.processQuery(query, user);
+        if (DEBUG) {
+            jlog.debug("the serialized query " + query);
+        }
+
+        int hashedKoralQuery = createTotalResultCacheKey(query);
+        boolean hasCutOff = hasCutOff(query);
+        if (!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);
+
+        result = afterCheckTotalResultCache(hashedKoralQuery, result);
+        if (!hasCutOff) {
+            result = removeCutOff(result);
+        }
+        return result;
+
+    }
+
+    private String removeCutOff (String result) throws KustvaktException {
+        ObjectNode resultNode = (ObjectNode) JsonUtils.readTree(result);
+        ObjectNode meta = (ObjectNode) resultNode.at("/meta");
+        meta.remove("cutOff");
+        return resultNode.toString();
+    }
+
+    public int createTotalResultCacheKey (String query)
+            throws KustvaktException {
+        ObjectNode queryNode = (ObjectNode) JsonUtils.readTree(query);
+        queryNode.remove("meta");
+        return queryNode.hashCode();
+    }
+
+    private String afterCheckTotalResultCache (int hashedKoralQuery,
+            String result) throws KustvaktException {
+
+        String totalResults = (String) totalResultCache
+                .getCacheValue(hashedKoralQuery);
+        if (totalResults != null) {
+            ObjectNode queryNode = (ObjectNode) JsonUtils.readTree(result);
+            ObjectNode meta = (ObjectNode) queryNode.at("/meta");
+            if (meta.isMissingNode()) {
+                queryNode.put("totalResults", Integer.valueOf(totalResults));
+            }
+            else {
+                meta.put("totalResults", Integer.valueOf(totalResults));
+            }
+            result = queryNode.toString();
+        }
+        else {
+            JsonNode node = JsonUtils.readTree(result);
+            totalResults = node.at("/meta/totalResults").asText();
+            if (totalResults != null && !totalResults.isEmpty()
+                    && Integer.parseInt(totalResults) > 0)
+                totalResultCache.storeInCache(hashedKoralQuery, totalResults);
+        }
+        return result;
+    }
+
+    public String precheckTotalResultCache (int hashedKoralQuery, String query)
+            throws KustvaktException {
+        String totalResults = (String) totalResultCache
+                .getCacheValue(hashedKoralQuery);
+        if (totalResults != null) {
+            // add cutoff
+            ObjectNode queryNode = (ObjectNode) JsonUtils.readTree(query);
+            ObjectNode meta = (ObjectNode) queryNode.at("/meta");
+            meta.put("cutOff", "true");
+            query = queryNode.toString();
+        }
+        return query;
+    }
+
+    private boolean hasCutOff (String query) throws KustvaktException {
+        JsonNode queryNode = JsonUtils.readTree(query);
+        JsonNode cutOff = queryNode.at("/meta/cutOff");
+        if (cutOff.isMissingNode()) {
+            return false;
+        }
+        else {
+            return true;
+        }
+    }
+
+    /**
+     * Pipes are service URLs for modifying KoralQuery. A POST request
+     * with Content-Type application/json will be sent for each pipe.
+     * Kustvakt expects a KoralQuery in JSON format as the pipe
+     * response.
+     * 
+     * @param query
+     *            the original koral query
+     * @param pipeArray
+     *            the pipe service URLs
+     * @param serializer
+     *            the query serializer
+     * @return a modified koral query
+     * @throws KustvaktException
+     */
+    private String runPipes (String query, String[] pipeArray)
+            throws KustvaktException {
+        if (pipeArray != null) {
+            for (int i = 0; i < pipeArray.length; i++) {
+                String pipeURL = pipeArray[i];
+                try {
+                    URL url = new URL(pipeURL);
+                    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()) {
+                        query = entity;
+                    }
+                    else {
+                        query = handlePipeError(query, pipeURL,
+                                connection.getResponseCode() + " "
+                                        + connection.getResponseMessage());
+                    }
+                }
+                catch (Exception e) {
+                    query = handlePipeError(query, pipeURL, e.getMessage());
+                }
+            }
+        }
+        return query;
+    }
+
+    private String handlePipeError (String query, String url, String message)
+            throws KustvaktException {
+        jlog.warn(
+                "Failed running the pipe at " + url + ". Message: " + message);
+
+        Notifications n = new Notifications();
+        n.addWarning(StatusCodes.PIPE_FAILED, "Pipe failed", url, message);
+        JsonNode warning = n.toJsonNode();
+
+        query = addWarning(query, warning);
+        return query;
+    }
+
+    private String addWarning (String query, JsonNode warning)
+            throws KustvaktException {
+
+        ObjectNode node = (ObjectNode) JsonUtils.readTree(query);
+        if (node.has("warnings")) {
+            warning = warning.at("/warnings/0");
+            ArrayNode arrayNode = (ArrayNode) node.get("warnings");
+            arrayNode.add(warning);
+            node.set("warnings", arrayNode);
+        }
+        else {
+            node.setAll((ObjectNode) warning);
+        }
+        return node.toString();
+    }
+
+    private void handleNonPublicFields (List<String> fieldList,
+            boolean accessRewriteDisabled, QuerySerializer serializer) {
+        List<String> nonPublicFields = new ArrayList<>();
+        nonPublicFields.add("snippet");
+
+        List<String> ignoredFields = new ArrayList<>();
+        if (accessRewriteDisabled && !fieldList.isEmpty()) {
+            for (String field : fieldList) {
+                if (nonPublicFields.contains(field)) {
+                    ignoredFields.add(field);
+                }
+            }
+            if (!ignoredFields.isEmpty()) {
+                serializer.addWarning(StatusCodes.NON_PUBLIC_FIELD_IGNORED,
+                        "The requested non public fields are ignored",
+                        ignoredFields);
+            }
+        }
+    }
+
+    private MetaQueryBuilder createMetaQuery (Integer pageIndex,
+            Integer pageInteger, String ctx, Integer pageLength, Boolean cutoff,
+            CorpusAccess corpusAccess, List<String> fieldList,
+            boolean accessRewriteDisabled, boolean showTokens,
+            boolean showSnippet) {
+        MetaQueryBuilder meta = new MetaQueryBuilder();
+        meta.addEntry("startIndex", pageIndex);
+        meta.addEntry("startPage", pageInteger);
+        meta.setSpanContext(ctx);
+        meta.addEntry("count", pageLength);
+        // todo: what happened to cutoff?
+        meta.addEntry("cutOff", cutoff);
+        meta.addEntry("snippets", (showSnippet && !accessRewriteDisabled));
+        if (!accessRewriteDisabled) {
+            meta.addEntry("tokens", showTokens);
+        }
+
+        // meta.addMeta(pageIndex, pageInteger, pageLength, ctx,
+        // cutoff);
+        // fixme: should only apply to CQL queries per default!
+        // meta.addEntry("itemsPerResource", 1);
+
+        if (corpusAccess.equals(CorpusAccess.FREE)) {
+            meta.addEntry("timeout", 10000);
+        }
+        else {
+            meta.addEntry("timeout", 90000);
+        }
+
+        if (fieldList != null && !fieldList.isEmpty()) {
+            meta.addEntry("fields", fieldList);
+        }
+        return meta;
+    }
+
+    private List<String> convertFieldsToList (String fields) {
+        if (fields != null && !fields.isEmpty()) {
+            String[] fieldArray = fields.split(",");
+            List<String> fieldList = new ArrayList<>(fieldArray.length);
+            for (String field : fieldArray) {
+                fieldList.add(field.trim());
+            }
+            return fieldList;
+        }
+        else {
+            return new ArrayList<>();
+        }
+    }
+
+    private String searchNeo4J (String query, int pageLength,
+            MetaQueryBuilder meta, boolean raw) throws KustvaktException {
+
+        if (raw) {
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+                    "raw not supported!");
+        }
+
+        MultivaluedMap<String, String> map = new MultivaluedHashMap<String, String>();
+        map.add("q", query);
+        map.add("count", String.valueOf(pageLength));
+        map.add("lctxs", String.valueOf(meta.getSpanContext().getLeftSize()));
+        map.add("rctxs", String.valueOf(meta.getSpanContext().getRightSize()));
+        return this.graphDBhandler.getResponse(map, "distKwic");
+
+    }
+
+    private Pattern determineAvailabilityPattern (User user) {
+        Pattern p = null;
+        if (user != null) {
+            CorpusAccess corpusAccess = user.getCorpusAccess();
+            switch (corpusAccess) {
+                case PUB:
+                    p = config.getPublicLicensePattern();
+                    break;
+                case ALL:
+                    p = config.getAllLicensePattern();
+                    break;
+                default: // FREE
+                    p = config.getFreeLicensePattern();
+                    break;
+            }
+        }
+        return p;
+    }
+
+    public String retrieveMatchInfo (String corpusId, String docId,
+            String textId, String matchId, boolean info, Set<String> foundries,
+            String username, HttpHeaders headers, Set<String> layers,
+            boolean spans, boolean snippet, boolean tokens,
+            boolean sentenceExpansion, boolean highlights)
+            throws KustvaktException {
+        String matchid = searchKrill.getMatchId(corpusId, docId, textId,
+                matchId);
+
+        User user = createUser(username, headers);
+        Pattern p = determineAvailabilityPattern(user);
+
+        //        boolean match_only = foundries == null || foundries.isEmpty();
+        String results;
+        //        try {
+
+        ArrayList<String> foundryList = null;
+        ArrayList<String> layerList = null;
+
+        if (foundries != null && !foundries.isEmpty()) {
+            foundryList = new ArrayList<String>();
+            layerList = new ArrayList<String>();
+            // EM: now without user, just list all foundries and
+            // layers
+            if (foundries.contains("*")) {
+                foundryList = config.getFoundries();
+                layerList = config.getLayers();
+            }
+            else {
+                foundryList.addAll(foundries);
+                layerList.addAll(layers);
+            }
+        }
+        else {
+            sentenceExpansion = false;
+            spans = false;
+            info = false;
+            highlights = true;
+        };
+
+        results = searchKrill.getMatch(matchid, info, foundryList, layerList,
+                spans, snippet, tokens, highlights, sentenceExpansion, p);
+        //        }
+        //        catch (Exception e) {
+        //            jlog.error("Exception in the MatchInfo service encountered!", e);
+        //            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+        //                    e.getMessage());
+        //        }
+        if (DEBUG) {
+            jlog.debug("MatchInfo results: " + results);
+        }
+        return results;
+    }
+
+    public String retrieveDocMetadata (String corpusId, String docId,
+            String textId, String fields, String username, HttpHeaders headers)
+            throws KustvaktException {
+        List<String> fieldList = null;
+        if (fields != null && !fields.isEmpty()) {
+            fieldList = convertFieldsToList(fields);
+        }
+        Pattern p = null;
+        if (config.isMetadataRestricted()) {
+            User user = createUser(username, headers);
+            p = determineAvailabilityPattern(user);
+        }
+        String textSigle = searchKrill.getTextSigle(corpusId, docId, textId);
+        return searchKrill.getFields(textSigle, fieldList, p);
+    }
+
+    public String getCollocationBase (String query) throws KustvaktException {
+        return graphDBhandler.getResponse("distCollo", "q", query);
+    }
+
+    public void closeIndexReader () throws KustvaktException {
+        searchKrill.closeIndexReader();
+    }
+
+    /**
+     * Return the fingerprint of the latest index revision.
+     */
+    public String getIndexFingerprint () {
+        return searchKrill.getIndexFingerprint();
+    }
+
+    public TotalResultCache getTotalResultCache () {
+        return totalResultCache;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/service/StatisticService.java b/src/main/java/de/ids_mannheim/korap/core/service/StatisticService.java
new file mode 100644
index 0000000..6f0a3f0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/service/StatisticService.java
@@ -0,0 +1,105 @@
+package de.ids_mannheim.korap.core.service;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.response.Notifications;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.utils.KoralCollectionQueryBuilder;
+import de.ids_mannheim.korap.web.SearchKrill;
+
+@Service
+public class StatisticService extends BasicService {
+
+    @Autowired
+    private SearchKrill searchKrill;
+    @Autowired
+    private KustvaktConfiguration config;
+
+    public String retrieveStatisticsForCorpusQuery (List<String> cqList,
+            boolean isDeprecated) throws KustvaktException {
+
+        KoralCollectionQueryBuilder builder = new KoralCollectionQueryBuilder();
+        String cq = combineMultipleCorpusQuery(cqList);
+        String json = null;
+        if (cq != null && !cq.isEmpty()) {
+            builder.with(cq);
+            json = builder.toJSON();
+        }
+
+        if (json != null) {
+            checkVC(json);
+        }
+        String stats = searchKrill.getStatistics(json);
+
+        if (isDeprecated) {
+            Notifications n = new Notifications();
+            n.addWarning(StatusCodes.DEPRECATED,
+                    "Parameter corpusQuery is deprecated in favor of cq.");
+            ObjectNode warning = (ObjectNode) n.toJsonNode();
+            ObjectNode node = (ObjectNode) JsonUtils.readTree(stats);
+            node.setAll(warning);
+            stats = node.toString();
+        }
+
+        if (stats.contains("-1")) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND);
+        }
+        return stats;
+    }
+
+    private void checkVC (String json) throws KustvaktException {
+        JsonNode node = JsonUtils.readTree(json);
+        node = node.at("/collection");
+        if (node.has("ref")) {
+            String vcName = node.path("ref").asText();
+            if (vcName.contains("/")) {
+                String[] names = vcName.split("/");
+                if (names.length == 2) {
+                    vcName = names[1];
+                }
+            }
+
+            String vcInCaching = config.getVcInCaching();
+            if (vcName.equals(vcInCaching)) {
+                throw new KustvaktException(
+                        de.ids_mannheim.korap.exceptions.StatusCodes.CACHING_VC,
+                        "VC is currently busy and unaccessible due to "
+                                + "caching process",
+                        node.get("ref").asText());
+            }
+        }
+    }
+
+    public String retrieveStatisticsForKoralQuery (String koralQuery)
+            throws KustvaktException {
+        String stats = null;
+        if (koralQuery != null && !koralQuery.isEmpty()) {
+            checkVC(koralQuery);
+            stats = searchKrill.getStatistics(koralQuery);
+        }
+        else {
+            stats = searchKrill.getStatistics(null);
+        }
+
+        if (stats.contains("-1")) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND);
+        }
+        return stats;
+    }
+
+    /**
+     * Return the fingerprint of the latest index revision.
+     */
+    public String getIndexFingerprint () {
+        return searchKrill.getIndexFingerprint();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/core/web/controller/AnnotationController.java b/src/main/java/de/ids_mannheim/korap/core/web/controller/AnnotationController.java
new file mode 100644
index 0000000..1f75d3c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/web/controller/AnnotationController.java
@@ -0,0 +1,124 @@
+package de.ids_mannheim.korap.core.web.controller;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+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;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+/**
+ * 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/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
new file mode 100644
index 0000000..042f412
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
@@ -0,0 +1,486 @@
+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 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.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;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import de.ids_mannheim.korap.web.utils.SearchResourceFilters;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * 
+ * @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/src/main/java/de/ids_mannheim/korap/core/web/controller/StatisticController.java b/src/main/java/de/ids_mannheim/korap/core/web/controller/StatisticController.java
new file mode 100644
index 0000000..6f86d2c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/core/web/controller/StatisticController.java
@@ -0,0 +1,108 @@
+package de.ids_mannheim.korap.core.web.controller;
+
+import java.util.List;
+import java.util.Locale;
+
+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.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;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * 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);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/AdminDao.java b/src/main/java/de/ids_mannheim/korap/dao/AdminDao.java
new file mode 100644
index 0000000..fcf32d0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/AdminDao.java
@@ -0,0 +1,11 @@
+package de.ids_mannheim.korap.dao;
+
+import de.ids_mannheim.korap.user.User;
+
+public interface AdminDao {
+
+    void addAccount (User user);
+
+    boolean isAdmin (String userId);
+
+}
\ No newline at end of file
diff --git a/src/main/java/de/ids_mannheim/korap/dao/AdminDaoImpl.java b/src/main/java/de/ids_mannheim/korap/dao/AdminDaoImpl.java
new file mode 100644
index 0000000..172ac69
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/AdminDaoImpl.java
@@ -0,0 +1,64 @@
+package de.ids_mannheim.korap.dao;
+
+import org.springframework.stereotype.Repository;
+
+import de.ids_mannheim.korap.entity.Admin;
+import de.ids_mannheim.korap.entity.Admin_;
+import de.ids_mannheim.korap.user.User;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+/**
+ * Describes database queries and transactions regarding admin users.
+ * 
+ * @author margaretha
+ *
+ */
+//@Transactional
+@Repository
+public class AdminDaoImpl implements AdminDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    /* (non-Javadoc)
+     * @see de.ids_mannheim.korap.dao.AdminDao#addAccount(de.ids_mannheim.korap.user.User)
+     */
+    @Override
+    public void addAccount (User user) {
+        Admin admin = new Admin();
+        admin.setUserId(user.getUsername());
+        entityManager.persist(admin);
+    }
+
+    /* (non-Javadoc)
+     * @see de.ids_mannheim.korap.dao.AdminDao#isAdmin(java.lang.String)
+     */
+    @Override
+    public boolean isAdmin (String userId) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Admin> query = criteriaBuilder.createQuery(Admin.class);
+
+        Root<Admin> admin = query.from(Admin.class);
+        Predicate p = criteriaBuilder.equal(admin.get(Admin_.userId), userId);
+
+        query.select(admin);
+        query.where(p);
+
+        Query q = entityManager.createQuery(query);
+        try {
+            q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java b/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
new file mode 100644
index 0000000..4aa7cd4
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
@@ -0,0 +1,219 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.List;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.AnnotationType;
+import de.ids_mannheim.korap.core.entity.Annotation;
+import de.ids_mannheim.korap.core.entity.AnnotationKey;
+import de.ids_mannheim.korap.core.entity.AnnotationKey_;
+import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.core.entity.AnnotationLayer_;
+import de.ids_mannheim.korap.core.entity.Annotation_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+/**
+ * AnnotationDao manages queries to database regarding annotations
+ * including
+ * foundry and layer pairs.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+public class AnnotationDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    /**
+     * Retrieves all foundry-layer pairs.
+     * 
+     * @return a list of foundry-layer pairs.
+     */
+    @SuppressWarnings("unchecked")
+    public List<AnnotationLayer> getAllFoundryLayerPairs () {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AnnotationLayer> query = criteriaBuilder
+                .createQuery(AnnotationLayer.class);
+        Root<AnnotationLayer> layer = query.from(AnnotationLayer.class);
+        layer.fetch(AnnotationLayer_.foundry);
+        layer.fetch(AnnotationLayer_.layer);
+        query.select(layer);
+        Query q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    /**
+     * Retrieves foundry-layer pairs and their values for the given
+     * foundry and layer. If layer is empty, retrieves data for all
+     * layer in the given foundry. If foundry is empty, retrieves data
+     * for all foundry and layer pairs.
+     * 
+     * @param foundry
+     *            a foundry code
+     * @param layer
+     *            a layer code
+     * @return a list of foundry-layer pairs.
+     */
+    @SuppressWarnings("unchecked")
+    public List<AnnotationLayer> getAnnotationDescriptions (String foundry,
+            String layer) {
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Object> query = criteriaBuilder.createQuery();
+        Root<AnnotationLayer> annotationPair = query
+                .from(AnnotationLayer.class);
+        annotationPair.fetch(AnnotationLayer_.foundry);
+        annotationPair.fetch(AnnotationLayer_.layer);
+        annotationPair.fetch(AnnotationLayer_.keys);
+
+        // EM: Hibernate bug in join n:m (see AnnotationPair.values).
+        // There should not be any redundant AnnotationPair.
+        // The redundancy can be also avoided with
+        // fetch=FetchType.EAGER
+        // because Hibernate does 2 selects.
+        query.distinct(true);
+        query = query.select(annotationPair);
+
+        if (!foundry.isEmpty()) {
+            Predicate foundryPredicate = criteriaBuilder.equal(annotationPair
+                    .get(AnnotationLayer_.foundry).get(Annotation_.code),
+                    foundry);
+            if (layer.isEmpty() || layer.equals("*")) {
+                query.where(foundryPredicate);
+            }
+            else {
+                Predicate layerPredicate = criteriaBuilder.equal(annotationPair
+                        .get(AnnotationLayer_.layer).get(Annotation_.code),
+                        layer);
+                Predicate andPredicate = criteriaBuilder.and(foundryPredicate,
+                        layerPredicate);
+                query.where(andPredicate);
+            }
+        }
+
+        Query q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public Annotation retrieveAnnotation (String code, String type) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Annotation> query = criteriaBuilder
+                .createQuery(Annotation.class);
+
+        Root<Annotation> annotation = query.from(Annotation.class);
+        Predicate predicates = criteriaBuilder.and(
+                criteriaBuilder.equal(annotation.get(Annotation_.code), code),
+                criteriaBuilder.equal(annotation.get(Annotation_.type), type));
+        query.select(annotation).where(predicates);
+        Query q = entityManager.createQuery(query);
+        try {
+            return (Annotation) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+
+    public AnnotationLayer retrieveAnnotationLayer (String foundry,
+            String layer) {
+        Annotation ann1 = retrieveAnnotation(foundry, AnnotationType.FOUNDRY);
+        Annotation ann2 = retrieveAnnotation(layer, AnnotationType.LAYER);
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AnnotationLayer> query = criteriaBuilder
+                .createQuery(AnnotationLayer.class);
+
+        Root<AnnotationLayer> annotation = query.from(AnnotationLayer.class);
+        Predicate predicates = criteriaBuilder.and(
+                criteriaBuilder.equal(annotation.get(AnnotationLayer_.foundry),
+                        ann1),
+                criteriaBuilder.equal(annotation.get(AnnotationLayer_.layer),
+                        ann2));
+        query.select(annotation).where(predicates);
+        Query q = entityManager.createQuery(query);
+        try {
+            return (AnnotationLayer) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+
+    }
+
+    @Transactional
+    public Annotation createAnnotation (String code, String type, String text,
+            String description) {
+        Annotation ann = new Annotation(code, type, text, description);
+        entityManager.persist(ann);
+        return ann;
+    }
+
+    @Transactional
+    public AnnotationLayer createAnnotationLayer (Annotation foundry,
+            Annotation layer) throws KustvaktException {
+        ParameterChecker.checkObjectValue(foundry, "foundry");
+        ParameterChecker.checkObjectValue(layer, "layer");
+
+        AnnotationLayer annotationLayer = new AnnotationLayer();
+        annotationLayer.setFoundryId(foundry.getId());
+        annotationLayer.setLayerId(layer.getId());
+        annotationLayer.setDescription(
+                foundry.getDescription() + " " + layer.getDescription());
+        entityManager.persist(annotationLayer);
+        return annotationLayer;
+    }
+
+    @Transactional
+    public void updateAnnotationLayer (AnnotationLayer layer) {
+        entityManager.merge(layer);
+    }
+
+    @Transactional
+    public void updateAnnotationKey (AnnotationKey key) {
+        entityManager.merge(key);
+    }
+
+    @Transactional
+    public AnnotationKey createAnnotationKey (AnnotationLayer layer,
+            Annotation key) {
+        AnnotationKey annotation = new AnnotationKey(layer.getId(),
+                key.getId());
+        entityManager.persist(annotation);
+        return annotation;
+    }
+
+    public AnnotationKey retrieveAnnotationKey (AnnotationLayer layer,
+            Annotation key) {
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AnnotationKey> query = criteriaBuilder
+                .createQuery(AnnotationKey.class);
+
+        Root<AnnotationKey> annotation = query.from(AnnotationKey.class);
+        Predicate predicates = criteriaBuilder.and(
+                criteriaBuilder.equal(annotation.get(AnnotationKey_.layer),
+                        layer),
+                criteriaBuilder.equal(annotation.get(AnnotationKey_.key), key));
+        query.select(annotation).where(predicates);
+        Query q = entityManager.createQuery(query);
+        try {
+            return (AnnotationKey) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/DefaultSettingDao.java b/src/main/java/de/ids_mannheim/korap/dao/DefaultSettingDao.java
new file mode 100644
index 0000000..a6bfeee
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/DefaultSettingDao.java
@@ -0,0 +1,88 @@
+package de.ids_mannheim.korap.dao;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.entity.DefaultSetting;
+import de.ids_mannheim.korap.entity.DefaultSetting_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+
+/**
+ * DefaultSettingDao manages database queries and transactions
+ * regarding user default setting.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+public class DefaultSettingDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    /**
+     * Creates a new entry of default setting in the database. This
+     * method allows storing settings of an empty json object, i.e.
+     * {}.
+     * 
+     * @param username
+     *            username
+     * @param settings
+     *            default settings in json
+     * @throws KustvaktException
+     */
+    @Transactional
+    public void createDefaultSetting (String username, String settings)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(settings, "settings");
+        DefaultSetting us = new DefaultSetting(username, settings);
+        entityManager.persist(us);
+    }
+
+    @Transactional
+    public void updateDefaultSetting (DefaultSetting defaultSetting)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(defaultSetting, "defaultSetting");
+        entityManager.merge(defaultSetting);
+    }
+
+    @Transactional
+    public void deleteDefaultSetting (String username)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(username, "defaultSetting");
+        DefaultSetting defaultSetting = retrieveDefaultSetting(username);
+        if (defaultSetting != null) {
+            entityManager.remove(defaultSetting);
+        }
+    }
+
+    public DefaultSetting retrieveDefaultSetting (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<DefaultSetting> query = criteriaBuilder
+                .createQuery(DefaultSetting.class);
+        Root<DefaultSetting> defaultSetting = query.from(DefaultSetting.class);
+
+        query.select(defaultSetting);
+        query.where(criteriaBuilder
+                .equal(defaultSetting.get(DefaultSetting_.username), username));
+        Query q = entityManager.createQuery(query);
+        try {
+            return (DefaultSetting) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/DummyAdminDaoImpl.java b/src/main/java/de/ids_mannheim/korap/dao/DummyAdminDaoImpl.java
new file mode 100644
index 0000000..0d31885
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/DummyAdminDaoImpl.java
@@ -0,0 +1,18 @@
+package de.ids_mannheim.korap.dao;
+
+import de.ids_mannheim.korap.user.User;
+
+public class DummyAdminDaoImpl implements AdminDao {
+
+    @Override
+    public void addAccount (User user) {
+        // TODO Auto-generated method stub
+
+    }
+
+    @Override
+    public boolean isAdmin (String userId) {
+        return false;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java b/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
new file mode 100644
index 0000000..ad1e77b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
@@ -0,0 +1,71 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.List;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.entity.Privilege;
+import de.ids_mannheim.korap.entity.Privilege_;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.Role_;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+
+/**
+ * Manages database queries and transactions regarding
+ * {@link Privilege} entity or database table.
+ * 
+ * @see Privilege
+ * @see PrivilegeType
+ * @see Role
+ * 
+ * @author margaretha
+ *
+ */
+@Transactional
+@Repository
+public class PrivilegeDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public void addPrivilegesToRole (Role role,
+            List<PrivilegeType> privilegeTypes) {
+        for (PrivilegeType type : privilegeTypes) {
+            Privilege privilege = new Privilege(type, role);
+            entityManager.persist(privilege);
+        }
+    }
+
+    public void deletePrivilegeFromRole (int roleId,
+            PrivilegeType privilegeType) {
+        List<Privilege> privilegeList = retrievePrivilegeByRoleId(roleId);
+        for (Privilege p : privilegeList) {
+            if (p.getName().equals(privilegeType)) {
+                entityManager.remove(p);
+                break;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Privilege> retrievePrivilegeByRoleId (int roleId) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Privilege> query = criteriaBuilder
+                .createQuery(Privilege.class);
+
+        Root<Privilege> root = query.from(Privilege.class);
+        root.fetch(Privilege_.role);
+        query.select(root);
+        query.where(criteriaBuilder
+                .equal(root.get(Privilege_.role).get(Role_.id), roleId));
+        Query q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/QueryAccessDao.java b/src/main/java/de/ids_mannheim/korap/dao/QueryAccessDao.java
new file mode 100644
index 0000000..5448b49
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/QueryAccessDao.java
@@ -0,0 +1,254 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.List;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.QueryAccessStatus;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroup_;
+import de.ids_mannheim.korap.entity.QueryAccess;
+import de.ids_mannheim.korap.entity.QueryAccess_;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.QueryDO_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Manages database queries and transactions regarding
+ * {@link QueryAccess} entity and its corresponding database
+ * table.
+ * 
+ * @author margaretha
+ *
+ * @see QueryAccess
+ * @see Query
+ */
+@Transactional
+@Repository
+public class QueryAccessDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public QueryAccess retrieveAccessById (int accessId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(accessId, "accessId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        query.select(access);
+        query.where(builder.equal(access.get(QueryAccess_.id), accessId));
+        Query q = entityManager.createQuery(query);
+        try {
+            return (QueryAccess) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Query access is not found", String.valueOf(accessId));
+        }
+    }
+
+    // for query-access admins
+    public List<QueryAccess> retrieveActiveAccessByQuery (int queryId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(queryId, "queryId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        Join<QueryAccess, QueryDO> accessQuery = access
+                .join(QueryAccess_.query);
+
+        Predicate p = builder.and(
+                builder.equal(accessQuery.get(QueryDO_.id), queryId),
+                builder.equal(access.get(QueryAccess_.status),
+                        QueryAccessStatus.ACTIVE));
+        query.select(access);
+        query.where(p);
+        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<QueryAccess> retrieveActiveAccessByQuery (String queryCreator,
+            String queryName) throws KustvaktException {
+        ParameterChecker.checkStringValue(queryCreator, "queryCreator");
+        ParameterChecker.checkStringValue(queryName, "queryName");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        Join<QueryAccess, QueryDO> accessQuery = access
+                .join(QueryAccess_.query);
+
+        Predicate p = builder.and(
+                builder.equal(accessQuery.get(QueryDO_.name), queryName),
+                builder.equal(accessQuery.get(QueryDO_.createdBy),
+                        queryCreator),
+                builder.equal(access.get(QueryAccess_.status),
+                        QueryAccessStatus.ACTIVE));
+        query.select(access);
+        query.where(p);
+        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<QueryAccess> retrieveAllAccess () throws KustvaktException {
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        query.select(access);
+        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<QueryAccess> retrieveAllAccessByQuery (String queryCreator,
+            String queryName) throws KustvaktException {
+        ParameterChecker.checkStringValue(queryCreator, "queryCreator");
+        ParameterChecker.checkStringValue(queryName, "queryName");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        Join<QueryAccess, QueryDO> accessQuery = access
+                .join(QueryAccess_.query);
+
+        Predicate conditions = builder.and(
+                builder.equal(accessQuery.get(QueryDO_.createdBy),
+                        queryCreator),
+                builder.equal(accessQuery.get(QueryDO_.name), queryName));
+        query.select(access);
+        query.where(conditions);
+        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<QueryAccess> retrieveAllAccessByGroup (int groupId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        Join<QueryAccess, UserGroup> accessQuery = access
+                .join(QueryAccess_.userGroup);
+
+        query.select(access);
+        query.where(builder.equal(accessQuery.get(UserGroup_.id), groupId));
+        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<QueryAccess> retrieveActiveAccessByGroup (int groupId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        Join<QueryAccess, UserGroup> accessQuery = access
+                .join(QueryAccess_.userGroup);
+
+        Predicate p = builder.and(
+                builder.equal(accessQuery.get(UserGroup_.id), groupId),
+                builder.equal(access.get(QueryAccess_.status),
+                        QueryAccessStatus.ACTIVE));
+
+        query.select(access);
+        query.where(p);
+        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    /**
+     * Hidden accesses are only created for published or system query.
+     * 
+     * Warn: The actual hidden accesses are not checked.
+     * 
+     * @param queryId
+     *            queryId
+     * @return true if there is a hidden access, false otherwise
+     * @throws KustvaktException
+     */
+    public QueryAccess retrieveHiddenAccess (int queryId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(queryId, "queryId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> query = builder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> access = query.from(QueryAccess.class);
+        Join<QueryAccess, QueryDO> accessQuery = access
+                .join(QueryAccess_.query);
+
+        Predicate p = builder.and(
+                builder.equal(accessQuery.get(QueryDO_.id), queryId),
+                builder.equal(access.get(QueryAccess_.status),
+                        QueryAccessStatus.HIDDEN)
+        // ,
+        // builder.notEqual(access.get(QueryAccess_.deletedBy),
+        // "NULL")
+        );
+
+        query.select(access);
+        query.where(p);
+
+        try {
+            Query q = entityManager.createQuery(query);
+            return (QueryAccess) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+
+    public void createAccessToQuery (QueryDO query, UserGroup userGroup,
+            String createdBy, QueryAccessStatus status) {
+        QueryAccess queryAccess = new QueryAccess();
+        queryAccess.setQuery(query);
+        queryAccess.setUserGroup(userGroup);
+        queryAccess.setCreatedBy(createdBy);
+        queryAccess.setStatus(status);
+        entityManager.persist(queryAccess);
+    }
+
+    public void deleteAccess (QueryAccess access, String deletedBy) {
+        // soft delete
+
+        // hard delete
+        if (!entityManager.contains(access)) {
+            access = entityManager.merge(access);
+        }
+        entityManager.remove(access);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java b/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
new file mode 100644
index 0000000..e17582a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
@@ -0,0 +1,387 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.NonUniqueResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.QueryAccessStatus;
+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.entity.QueryAccess;
+import de.ids_mannheim.korap.entity.QueryAccess_;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.QueryDO_;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.entity.UserGroupMember_;
+import de.ids_mannheim.korap.entity.UserGroup_;
+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.ParameterChecker;
+
+/**
+ * QueryDao manages database queries and transactions
+ * regarding virtual corpus and KorAP queries.
+ * 
+ * @author margaretha
+ *
+ */
+@Transactional
+@Repository
+public class QueryDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public int createQuery (String name, ResourceType type, QueryType queryType,
+            CorpusAccess requiredAccess, String koralQuery, String definition,
+            String description, String status, boolean isCached,
+            String createdBy, String query, String queryLanguage)
+            throws KustvaktException {
+
+        QueryDO q = new QueryDO();
+        q.setName(name);
+        q.setType(type);
+        q.setQueryType(queryType);
+        q.setRequiredAccess(requiredAccess);
+        q.setKoralQuery(koralQuery);
+        q.setDefinition(definition);
+        q.setDescription(description);
+        q.setStatus(status);
+        q.setCreatedBy(createdBy);
+        q.setCached(isCached);
+        q.setQuery(query);
+        q.setQueryLanguage(queryLanguage);
+
+        entityManager.persist(q);
+        return q.getId();
+    }
+
+    public void editQuery (QueryDO queryDO, String name, ResourceType type,
+            CorpusAccess requiredAccess, String koralQuery, String definition,
+            String description, String status, boolean isCached,
+            String queryStr, String queryLanguage) throws KustvaktException {
+
+        if (name != null && !name.isEmpty()) {
+            queryDO.setName(name);
+        }
+        if (type != null) {
+            queryDO.setType(type);
+        }
+        if (requiredAccess != null) {
+            queryDO.setRequiredAccess(requiredAccess);
+        }
+        if (koralQuery != null) {
+            queryDO.setKoralQuery(koralQuery);
+        }
+        if (definition != null && !definition.isEmpty()) {
+            queryDO.setDefinition(definition);
+        }
+        if (description != null && !description.isEmpty()) {
+            queryDO.setDescription(description);
+        }
+        if (status != null && !status.isEmpty()) {
+            queryDO.setStatus(status);
+        }
+        if (queryStr != null && !queryStr.isEmpty()) {
+            queryDO.setQuery(queryStr);
+        }
+        if (queryLanguage != null && !queryLanguage.isEmpty()) {
+            queryDO.setQueryLanguage(queryLanguage);
+        }
+        queryDO.setCached(isCached);
+        entityManager.merge(queryDO);
+    }
+
+    public void deleteQuery (QueryDO query) throws KustvaktException {
+        if (!entityManager.contains(query)) {
+            query = entityManager.merge(query);
+        }
+        entityManager.remove(query);
+
+    }
+
+    /**
+     * System admin function.
+     * 
+     * Retrieves queries by creator and type. If type is not
+     * specified, retrieves queries of all types. If createdBy is not
+     * specified, retrieves queries of all users.
+     * 
+     * @param type
+     *            {@link ResourceType}
+     * @param createdBy
+     *            username of the query creator
+     * @return a list of {@link Query}
+     * @throws KustvaktException
+     */
+    @SuppressWarnings("unchecked")
+    public List<QueryDO> retrieveQueryByType (ResourceType type,
+            String createdBy, QueryType queryType) throws KustvaktException {
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> criteriaQuery = criteriaBuilder
+                .createQuery(QueryDO.class);
+        Root<QueryDO> query = criteriaQuery.from(QueryDO.class);
+
+        Predicate conditions = criteriaBuilder
+                .equal(query.get(QueryDO_.queryType), queryType);
+        if (createdBy != null && !createdBy.isEmpty()) {
+            conditions = criteriaBuilder.and(conditions, criteriaBuilder
+                    .equal(query.get(QueryDO_.createdBy), createdBy));
+            if (type != null) {
+                conditions = criteriaBuilder.and(conditions,
+                        criteriaBuilder.equal(query.get(QueryDO_.type), type));
+            }
+        }
+        else if (type != null) {
+            conditions = criteriaBuilder.and(conditions,
+                    criteriaBuilder.equal(query.get(QueryDO_.type), type));
+        }
+
+        criteriaQuery.select(query);
+        criteriaQuery.where(conditions);
+        Query q = entityManager.createQuery(criteriaQuery);
+        return q.getResultList();
+    }
+
+    public QueryDO retrieveQueryById (int id) throws KustvaktException {
+        ParameterChecker.checkIntegerValue(id, "id");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> criteriaQuery = criteriaBuilder
+                .createQuery(QueryDO.class);
+        Root<QueryDO> query = criteriaQuery.from(QueryDO.class);
+        criteriaQuery.select(query);
+        criteriaQuery.where(criteriaBuilder.equal(query.get(QueryDO_.id), id));
+
+        QueryDO qe = null;
+        try {
+            Query q = entityManager.createQuery(criteriaQuery);
+            qe = (QueryDO) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Query with id: " + id + " is not found",
+                    String.valueOf(id), e);
+        }
+        return qe;
+    }
+
+    public QueryDO retrieveQueryByName (String queryName, String createdBy)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(createdBy, "createdBy");
+        ParameterChecker.checkStringValue(queryName, "queryName");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> criteriaQuery = builder
+                .createQuery(QueryDO.class);
+
+        Root<QueryDO> query = criteriaQuery.from(QueryDO.class);
+
+        Predicate condition = builder.and(
+                builder.equal(query.get(QueryDO_.createdBy), createdBy),
+                builder.equal(query.get(QueryDO_.name), queryName));
+
+        criteriaQuery.select(query);
+        criteriaQuery.where(condition);
+
+        Query q = entityManager.createQuery(criteriaQuery);
+        QueryDO qe = null;
+        try {
+            qe = (QueryDO) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+        catch (NonUniqueResultException e) {
+            String code = createdBy + "/" + queryName;
+            throw new KustvaktException(StatusCodes.NON_UNIQUE_RESULT_FOUND,
+                    "Non unique result found for query: retrieve query by name "
+                            + code,
+                    String.valueOf(code), e);
+        }
+        return qe;
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<QueryDO> retrieveOwnerQuery (String userId, QueryType queryType)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> cq = builder.createQuery(QueryDO.class);
+
+        Root<QueryDO> query = cq.from(QueryDO.class);
+        Predicate conditions = builder.and(
+                builder.equal(query.get(QueryDO_.createdBy), userId),
+                builder.equal(query.get(QueryDO_.queryType), queryType));
+
+        cq.select(query);
+        cq.where(conditions);
+
+        Query q = entityManager.createQuery(cq);
+        return q.getResultList();
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<QueryDO> retrieveOwnerQueryByType (String userId,
+            ResourceType type) throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> cq = builder.createQuery(QueryDO.class);
+
+        Root<QueryDO> query = cq.from(QueryDO.class);
+        cq.select(query);
+
+        Predicate p = builder.and(
+                builder.equal(query.get(QueryDO_.createdBy), userId),
+                builder.equal(query.get(QueryDO_.type), type));
+        cq.where(p);
+
+        Query q = entityManager.createQuery(cq);
+        return q.getResultList();
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<QueryDO> retrieveGroupQueryByUser (String userId,
+            QueryType queryType) throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> cq = builder.createQuery(QueryDO.class);
+
+        Root<QueryDO> query = cq.from(QueryDO.class);
+        Join<QueryDO, QueryAccess> access = query.join(QueryDO_.queryAccess);
+
+        // Predicate corpusStatus = builder.and(
+        // builder.notEqual(access.get(QueryAccess_.status),
+        // VirtualCorpusAccessStatus.HIDDEN),
+        // builder.notEqual(access.get(QueryAccess_.status),
+        // VirtualCorpusAccessStatus.DELETED));
+
+        Predicate type = builder.equal(query.get(QueryDO_.queryType),
+                queryType);
+
+        Predicate accessStatus = builder.notEqual(
+                access.get(QueryAccess_.status), QueryAccessStatus.DELETED);
+
+        Predicate userGroupStatus = builder.notEqual(
+                access.get(QueryAccess_.userGroup).get(UserGroup_.status),
+                UserGroupStatus.DELETED);
+        Join<UserGroup, UserGroupMember> members = access
+                .join(QueryAccess_.userGroup).join(UserGroup_.members);
+
+        Predicate memberStatus = builder.equal(
+                members.get(UserGroupMember_.status), GroupMemberStatus.ACTIVE);
+
+        Predicate user = builder.equal(members.get(UserGroupMember_.userId),
+                userId);
+
+        cq.select(query);
+        cq.where(builder.and(type, accessStatus, userGroupStatus, memberStatus,
+                user));
+
+        Query q = entityManager.createQuery(cq);
+        return q.getResultList();
+    }
+
+    public List<QueryDO> retrieveQueryByUser (String userId,
+            QueryType queryType) throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+        ParameterChecker.checkObjectValue(queryType, "queryType");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> criteriaQuery = builder
+                .createQuery(QueryDO.class);
+
+        Root<QueryDO> query = criteriaQuery.from(QueryDO.class);
+        Predicate predicate = builder.and(
+                builder.equal(query.get(QueryDO_.queryType), queryType),
+                builder.or(builder.equal(query.get(QueryDO_.createdBy), userId),
+                        builder.equal(query.get(QueryDO_.type),
+                                ResourceType.SYSTEM)));
+
+        criteriaQuery.select(query);
+        criteriaQuery.where(predicate);
+        criteriaQuery.distinct(true);
+        Query q = entityManager.createQuery(criteriaQuery);
+
+        @SuppressWarnings("unchecked")
+        List<QueryDO> queryList = q.getResultList();
+        List<QueryDO> groupQuery = retrieveGroupQueryByUser(userId, queryType);
+        Set<QueryDO> querySet = new HashSet<QueryDO>();
+        querySet.addAll(queryList);
+        querySet.addAll(groupQuery);
+
+        List<QueryDO> merger = new ArrayList<QueryDO>(querySet.size());
+        merger.addAll(querySet);
+        Collections.sort(merger);
+        return merger;
+    }
+
+    // for admins
+    @SuppressWarnings("unchecked")
+    public List<QueryDO> retrieveQueryByGroup (int groupId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryDO> criteriaQuery = builder
+                .createQuery(QueryDO.class);
+
+        Root<QueryDO> query = criteriaQuery.from(QueryDO.class);
+        Join<QueryDO, QueryAccess> queryAccess = query
+                .join(QueryDO_.queryAccess);
+        Join<QueryAccess, UserGroup> accessGroup = queryAccess
+                .join(QueryAccess_.userGroup);
+
+        criteriaQuery.select(query);
+        criteriaQuery
+                .where(builder.equal(accessGroup.get(UserGroup_.id), groupId));
+        Query q = entityManager.createQuery(criteriaQuery);
+        return q.getResultList();
+    }
+
+    public Long countNumberOfQuery (String userId, QueryType queryType)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+        ParameterChecker.checkObjectValue(queryType, "queryType");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Long> cq = builder.createQuery(Long.class);
+
+        Root<QueryDO> query = cq.from(QueryDO.class);
+        Predicate conditions = builder.and(
+                builder.equal(query.get(QueryDO_.createdBy), userId),
+                builder.equal(query.get(QueryDO_.queryType), queryType));
+
+        cq.select(builder.count(query));
+        cq.where(conditions);
+
+        TypedQuery<Long> q = entityManager.createQuery(cq);
+        return q.getSingleResult();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java b/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
new file mode 100644
index 0000000..c121669
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
@@ -0,0 +1,82 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.core.entity.Resource;
+import de.ids_mannheim.korap.core.entity.Resource_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * ResourceDao manages SQL queries regarding resource info and layers.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+public class ResourceDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    /**
+     * Select all from the resource table
+     * 
+     * @return a list of resources
+     */
+    public List<Resource> getAllResources () {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Resource> query = criteriaBuilder
+                .createQuery(Resource.class);
+        Root<Resource> resource = query.from(Resource.class);
+        query.select(resource);
+
+        TypedQuery<Resource> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public Resource retrieveResource (String id) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Resource> query = criteriaBuilder
+                .createQuery(Resource.class);
+        Root<Resource> resource = query.from(Resource.class);
+        query.select(resource);
+        query.where(criteriaBuilder.equal(resource.get(Resource_.id), id));
+
+        Query q = entityManager.createQuery(query);
+        try {
+            return (Resource) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+
+    @Transactional
+    public void createResource (String id, String germanTitle,
+            String englishTitle, String englishDescription,
+            Set<AnnotationLayer> layers) throws KustvaktException {
+        ParameterChecker.checkStringValue(id, "id");
+        ParameterChecker.checkStringValue(englishTitle, "en_title");
+        ParameterChecker.checkStringValue(germanTitle, "de_title");
+
+        Resource r = new Resource(id, germanTitle, englishTitle,
+                englishDescription, layers);
+        entityManager.persist(r);
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java b/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
new file mode 100644
index 0000000..5481d30
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
@@ -0,0 +1,102 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.ListJoin;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.Role_;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.entity.UserGroupMember_;
+
+/**
+ * Manages database queries and transactions regarding {@link Role}
+ * entity or database table.
+ * 
+ * @author margaretha
+ * @see Role
+ * @see PrivilegeDao
+ */
+@Transactional
+@Repository
+public class RoleDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    @Autowired
+    private PrivilegeDao privilegeDao;
+
+    public void createRole (String name, List<PrivilegeType> privilegeTypes) {
+        Role r = new Role();
+        r.setName(name);
+        entityManager.persist(r);
+        privilegeDao.addPrivilegesToRole(r, privilegeTypes);
+    }
+
+    public void deleteRole (int roleId) {
+        Role r = retrieveRoleById(roleId);
+        entityManager.remove(r);
+    }
+
+    public void editRoleName (int roleId, String name) {
+        Role r = retrieveRoleById(roleId);
+        r.setName(name);
+        entityManager.persist(r);
+    }
+
+    public Role retrieveRoleById (int roleId) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
+
+        Root<Role> root = query.from(Role.class);
+        root.fetch(Role_.privileges);
+        query.select(root);
+        query.where(criteriaBuilder.equal(root.get(Role_.id), roleId));
+        Query q = entityManager.createQuery(query);
+        return (Role) q.getSingleResult();
+    }
+
+    public Role retrieveRoleByName (String roleName) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
+
+        Root<Role> root = query.from(Role.class);
+        root.fetch(Role_.privileges);
+        query.select(root);
+        query.where(criteriaBuilder.equal(root.get(Role_.name), roleName));
+        Query q = entityManager.createQuery(query);
+        return (Role) q.getSingleResult();
+    }
+
+    public Set<Role> retrieveRoleByGroupMemberId (int userId) {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
+
+        Root<Role> root = query.from(Role.class);
+        ListJoin<Role, UserGroupMember> memberRole = root
+                .join(Role_.userGroupMembers);
+
+        query.select(root);
+        query.where(criteriaBuilder.equal(memberRole.get(UserGroupMember_.id),
+                userId));
+        Query q = entityManager.createQuery(query);
+        @SuppressWarnings("unchecked")
+        List<Role> resultList = q.getResultList();
+        return new HashSet<Role>(resultList);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserDao.java
new file mode 100644
index 0000000..61c24af
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserDao.java
@@ -0,0 +1,23 @@
+package de.ids_mannheim.korap.dao;
+
+import org.springframework.stereotype.Repository;
+
+import de.ids_mannheim.korap.user.KorAPUser;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * Dummy DAO for testing using basic authentication.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+public class UserDao {
+
+    public User getAccount (String username) {
+        User user = new KorAPUser();
+        user.setUsername(username);
+        return user;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
new file mode 100644
index 0000000..8a91e9c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -0,0 +1,369 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.ListJoin;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.constant.QueryAccessStatus;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.entity.UserGroupMember_;
+import de.ids_mannheim.korap.entity.UserGroup_;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.QueryAccess;
+import de.ids_mannheim.korap.entity.QueryAccess_;
+import de.ids_mannheim.korap.entity.QueryDO_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Manages database queries and transactions regarding
+ * {@link UserGroup} entity and database table.
+ * 
+ * @author margaretha
+ * 
+ * @see UserGroup
+ *
+ */
+@Transactional
+@Repository
+public class UserGroupDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    @Autowired
+    private RoleDao roleDao;
+
+    public int createGroup (String name, String description, String createdBy,
+            UserGroupStatus status) throws KustvaktException {
+        ParameterChecker.checkStringValue(name, "name");
+        ParameterChecker.checkStringValue(createdBy, "createdBy");
+        ParameterChecker.checkObjectValue(status, "UserGroupStatus");
+
+        UserGroup group = new UserGroup();
+        group.setName(name);
+        group.setDescription(description);
+        group.setStatus(status);
+        group.setCreatedBy(createdBy);
+        entityManager.persist(group);
+
+        Set<Role> roles = new HashSet<Role>();
+        roles.add(roleDao
+                .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId()));
+        roles.add(roleDao
+                .retrieveRoleById(PredefinedRole.VC_ACCESS_ADMIN.getId()));
+
+        UserGroupMember owner = new UserGroupMember();
+        owner.setUserId(createdBy);
+        owner.setCreatedBy(createdBy);
+        owner.setStatus(GroupMemberStatus.ACTIVE);
+        owner.setGroup(group);
+        owner.setRoles(roles);
+        entityManager.persist(owner);
+
+        return group.getId();
+    }
+
+    public void deleteGroup (int groupId, String deletedBy,
+            boolean isSoftDelete) throws KustvaktException {
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+        ParameterChecker.checkStringValue(deletedBy, "deletedBy");
+
+        UserGroup group = null;
+        try {
+            group = retrieveGroupById(groupId);
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Group " + groupId + " is not found.",
+                    "groupId: " + groupId);
+        }
+
+        if (isSoftDelete) {
+            group.setStatus(UserGroupStatus.DELETED);
+            group.setDeletedBy(deletedBy);
+            entityManager.merge(group);
+        }
+        else {
+            if (!entityManager.contains(group)) {
+                group = entityManager.merge(group);
+            }
+            entityManager.remove(group);
+        }
+    }
+
+    public void updateGroup (UserGroup group) throws KustvaktException {
+        ParameterChecker.checkObjectValue(group, "user-group");
+        entityManager.merge(group);
+    }
+
+    /**
+     * Retrieves the UserGroup by the given group id. This methods
+     * does not
+     * fetch group members because only group admin is allowed to see
+     * them.
+     * Group members have to be retrieved separately.
+     * 
+     * @see UserGroupMember
+     * @param groupId
+     *            group id
+     * @return UserGroup
+     * @throws KustvaktException
+     */
+    public UserGroup retrieveGroupById (int groupId) throws KustvaktException {
+        return retrieveGroupById(groupId, false);
+    }
+
+    public UserGroup retrieveGroupById (int groupId, boolean fetchMembers)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroup> query = criteriaBuilder
+                .createQuery(UserGroup.class);
+
+        Root<UserGroup> root = query.from(UserGroup.class);
+        if (fetchMembers) {
+            root.fetch(UserGroup_.members);
+        }
+        query.select(root);
+        query.where(criteriaBuilder.equal(root.get(UserGroup_.id), groupId));
+        Query q = entityManager.createQuery(query);
+
+        try {
+            return (UserGroup) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Group with id " + groupId + " is not found",
+                    String.valueOf(groupId), e);
+        }
+    }
+
+    /**
+     * Retrieves only user-groups that are active (not hidden or
+     * deleted).
+     * 
+     * @param userId
+     *            user id
+     * @return a list of UserGroup
+     * @throws KustvaktException
+     */
+    @SuppressWarnings("unchecked")
+    public List<UserGroup> retrieveGroupByUserId (String userId)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroup> query = criteriaBuilder
+                .createQuery(UserGroup.class);
+
+        Root<UserGroup> root = query.from(UserGroup.class);
+
+        ListJoin<UserGroup, UserGroupMember> members = root
+                .join(UserGroup_.members);
+        Predicate restrictions = criteriaBuilder.and(
+                criteriaBuilder.equal(root.get(UserGroup_.status),
+                        UserGroupStatus.ACTIVE),
+                criteriaBuilder.equal(members.get(UserGroupMember_.userId),
+                        userId),
+                criteriaBuilder.notEqual(members.get(UserGroupMember_.status),
+                        GroupMemberStatus.DELETED));
+        // criteriaBuilder.equal(members.get(UserGroupMember_.status),
+        // GroupMemberStatus.ACTIVE));
+
+        query.select(root);
+        query.where(restrictions);
+        Query q = entityManager.createQuery(query);
+
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "No group for username: " + userId + " is found", userId,
+                    e);
+        }
+    }
+
+    public UserGroup retrieveGroupByName (String groupName,
+            boolean fetchMembers) throws KustvaktException {
+        ParameterChecker.checkStringValue(groupName, "groupName");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroup> query = criteriaBuilder
+                .createQuery(UserGroup.class);
+
+        Root<UserGroup> root = query.from(UserGroup.class);
+        if (fetchMembers) {
+            root.fetch(UserGroup_.members);
+        }
+        query.select(root);
+        query.where(
+                criteriaBuilder.equal(root.get(UserGroup_.name), groupName));
+        Query q = entityManager.createQuery(query);
+
+        try {
+            return (UserGroup) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Group " + groupName + " is not found", groupName, e);
+        }
+    }
+
+    public UserGroup retrieveHiddenGroupByQuery (int queryId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(queryId, "queryId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroup> criteriaQuery = criteriaBuilder
+                .createQuery(UserGroup.class);
+
+        Root<UserGroup> root = criteriaQuery.from(UserGroup.class);
+        Join<UserGroup, QueryAccess> access = root.join(UserGroup_.queryAccess);
+        Join<QueryAccess, QueryDO> query = access.join(QueryAccess_.query);
+
+        Predicate p = criteriaBuilder.and(
+                criteriaBuilder.equal(root.get(UserGroup_.status),
+                        UserGroupStatus.HIDDEN),
+                criteriaBuilder.equal(query.get(QueryDO_.id), queryId));
+
+        criteriaQuery.select(root);
+        criteriaQuery.where(p);
+        Query q = entityManager.createQuery(criteriaQuery);
+
+        try {
+            return (UserGroup) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "No hidden group for query with id " + queryId
+                            + " is found",
+                    String.valueOf(queryId), e);
+        }
+
+    }
+
+    /**
+     * This is an admin function. It retrieves all groups given the
+     * userId
+     * and status.
+     * 
+     * @param userId
+     * @param status
+     * @return a list of {@link UserGroup}s
+     * @throws KustvaktException
+     */
+    @SuppressWarnings("unchecked")
+    public List<UserGroup> retrieveGroupByStatus (String userId,
+            UserGroupStatus status) throws KustvaktException {
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroup> query = criteriaBuilder
+                .createQuery(UserGroup.class);
+
+        Root<UserGroup> root = query.from(UserGroup.class);
+
+        Predicate restrictions = null;
+
+        if (userId != null && !userId.isEmpty()) {
+
+            ListJoin<UserGroup, UserGroupMember> members = root
+                    .join(UserGroup_.members);
+            restrictions = criteriaBuilder.and(criteriaBuilder
+                    .equal(members.get(UserGroupMember_.userId), userId));
+
+            if (status != null) {
+                restrictions = criteriaBuilder.and(restrictions, criteriaBuilder
+                        .equal(root.get(UserGroup_.status), status));
+            }
+        }
+        else if (status != null) {
+            restrictions = criteriaBuilder.equal(root.get(UserGroup_.status),
+                    status);
+
+        }
+
+        query.select(root);
+        if (restrictions != null) {
+            query.where(restrictions);
+        }
+        Query q = entityManager.createQuery(query);
+
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "No group with status " + status + " is found",
+                    status.toString());
+        }
+
+    }
+
+    public void addQueryToGroup (QueryDO query, String createdBy,
+            QueryAccessStatus status, UserGroup group) {
+        QueryAccess accessGroup = new QueryAccess();
+        accessGroup.setCreatedBy(createdBy);
+        accessGroup.setStatus(status);
+        accessGroup.setUserGroup(group);
+        accessGroup.setQuery(query);;
+        entityManager.persist(accessGroup);
+    }
+
+    public void addQueryToGroup (List<QueryDO> queries, String createdBy,
+            UserGroup group, QueryAccessStatus status) {
+
+        for (QueryDO q : queries) {
+            addQueryToGroup(q, createdBy, status, group);
+        }
+    }
+
+    public void deleteQueryFromGroup (int queryId, int groupId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(queryId, "queryId");
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<QueryAccess> criteriaQuery = criteriaBuilder
+                .createQuery(QueryAccess.class);
+
+        Root<QueryAccess> root = criteriaQuery.from(QueryAccess.class);
+        Join<QueryAccess, QueryDO> queryAccess = root.join(QueryAccess_.query);
+        Join<QueryAccess, UserGroup> group = root.join(QueryAccess_.userGroup);
+
+        Predicate query = criteriaBuilder.equal(queryAccess.get(QueryDO_.id),
+                queryId);
+        Predicate userGroup = criteriaBuilder.equal(group.get(UserGroup_.id),
+                groupId);
+
+        criteriaQuery.select(root);
+        criteriaQuery.where(criteriaBuilder.and(query, userGroup));
+        Query q = entityManager.createQuery(criteriaQuery);
+        QueryAccess access = (QueryAccess) q.getSingleResult();
+        entityManager.remove(access);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
new file mode 100644
index 0000000..c399fd6
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -0,0 +1,176 @@
+package de.ids_mannheim.korap.dao;
+
+import java.util.HashSet;
+import java.util.List;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.Role_;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.entity.UserGroupMember_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Manages database queries and transactions regarding
+ * {@link UserGroupMember} entity and
+ * database table.
+ * 
+ * @author margaretha
+ * @see UserGroupMember
+ *
+ */
+@Transactional
+@Repository
+public class UserGroupMemberDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public void addMember (UserGroupMember member) throws KustvaktException {
+        ParameterChecker.checkObjectValue(member, "userGroupMember");
+        entityManager.persist(member);
+    }
+
+    public void updateMember (UserGroupMember member) throws KustvaktException {
+        ParameterChecker.checkObjectValue(member, "UserGroupMember");
+        entityManager.merge(member);
+    }
+
+    public void deleteMember (UserGroupMember member, String deletedBy,
+            boolean isSoftDelete) throws KustvaktException {
+        ParameterChecker.checkObjectValue(member, "UserGroupMember");
+        ParameterChecker.checkStringValue(deletedBy, "deletedBy");
+
+        if (!entityManager.contains(member)) {
+            member = entityManager.merge(member);
+        }
+
+        if (isSoftDelete) {
+            member.setStatus(GroupMemberStatus.DELETED);
+            member.setDeletedBy(deletedBy);
+            member.setRoles(new HashSet<>());
+            entityManager.persist(member);
+        }
+        else {
+            member.setRoles(new HashSet<>());
+            entityManager.remove(member);
+        }
+    }
+
+    public UserGroupMember retrieveMemberById (String userId, int groupId)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(userId, "userId");
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroupMember> query = criteriaBuilder
+                .createQuery(UserGroupMember.class);
+
+        Root<UserGroupMember> root = query.from(UserGroupMember.class);
+
+        Predicate predicate = criteriaBuilder.and(
+                criteriaBuilder.equal(root.get(UserGroupMember_.group),
+                        groupId),
+                criteriaBuilder.equal(root.get(UserGroupMember_.userId),
+                        userId));
+
+        query.select(root);
+        query.where(predicate);
+        Query q = entityManager.createQuery(query);
+
+        try {
+            return (UserGroupMember) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.GROUP_MEMBER_NOT_FOUND,
+                    userId + " is not found in the group", userId);
+        }
+
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<UserGroupMember> retrieveMemberByRole (int groupId, int roleId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(roleId, "roleId");
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroupMember> query = criteriaBuilder
+                .createQuery(UserGroupMember.class);
+
+        Root<UserGroupMember> root = query.from(UserGroupMember.class);
+        Join<UserGroupMember, Role> memberRole = root.join("roles");
+
+        Predicate predicate = criteriaBuilder.and(
+                criteriaBuilder.equal(root.get(UserGroupMember_.group),
+                        groupId),
+                criteriaBuilder.equal(root.get(UserGroupMember_.status),
+                        GroupMemberStatus.ACTIVE),
+                criteriaBuilder.equal(memberRole.get(Role_.id), roleId));
+
+        query.select(root);
+        query.where(predicate);
+        Query q = entityManager.createQuery(query);
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(
+                    StatusCodes.NO_RESULT_FOUND, "No member with role " + roleId
+                            + " is found in group " + groupId,
+                    String.valueOf(roleId));
+        }
+    }
+
+    public List<UserGroupMember> retrieveMemberByGroupId (int groupId)
+            throws KustvaktException {
+        return retrieveMemberByGroupId(groupId, false);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<UserGroupMember> retrieveMemberByGroupId (int groupId,
+            boolean isAdmin) throws KustvaktException {
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroupMember> query = criteriaBuilder
+                .createQuery(UserGroupMember.class);
+
+        Root<UserGroupMember> root = query.from(UserGroupMember.class);
+
+        Predicate predicate = criteriaBuilder.and(criteriaBuilder
+                .equal(root.get(UserGroupMember_.group), groupId));
+
+        if (!isAdmin) {
+            predicate = criteriaBuilder.and(predicate,
+                    criteriaBuilder.notEqual(root.get(UserGroupMember_.status),
+                            GroupMemberStatus.DELETED));
+        }
+
+        query.select(root);
+        query.where(predicate);
+        Query q = entityManager.createQuery(query);
+
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "No member in group " + groupId + " is found",
+                    String.valueOf(groupId));
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/FoundryDto.java b/src/main/java/de/ids_mannheim/korap/dto/FoundryDto.java
new file mode 100644
index 0000000..beb344c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/FoundryDto.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.dto;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Data transfer object for annotation descriptions (e.g. for
+ * Kalamar).
+ * 
+ * @author margaretha
+ * 
+ */
+@Getter
+@Setter
+@JsonInclude(Include.NON_EMPTY) // new fasterxml annotation, not used by current jersey version
+//@JsonSerialize(include=Inclusion.NON_EMPTY) // old codehouse annotation, used by jersey
+public class FoundryDto {
+
+    private String code;
+    private String description;
+    private List<Layer> layers;
+
+    @Getter
+    @Setter
+    @JsonInclude(Include.NON_EMPTY)
+    //    @JsonSerialize(include=Inclusion.NON_EMPTY) // old codehouse annotation used by jersey
+    public class Layer {
+        private String code;
+        private String description;
+        private Set<Key> keys;
+    }
+
+    @Getter
+    @Setter
+    @JsonInclude(Include.NON_EMPTY)
+    //    @JsonSerialize(include=Inclusion.NON_EMPTY) // old codehouse annotation used by jersey
+    public class Key implements Comparable<Key> {
+
+        private String code;
+        private String description;
+        private Map<String, String> values;
+
+        public Key (String code) {
+            this.code = code;
+        }
+
+        @Override
+        public int compareTo (Key k) {
+            return this.code.compareTo(k.code);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/InstalledPluginDto.java b/src/main/java/de/ids_mannheim/korap/dto/InstalledPluginDto.java
new file mode 100644
index 0000000..ac31ba0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/InstalledPluginDto.java
@@ -0,0 +1,38 @@
+package de.ids_mannheim.korap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import de.ids_mannheim.korap.entity.InstalledPlugin;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonInclude(Include.NON_EMPTY)
+public class InstalledPluginDto {
+    @JsonProperty("client_id")
+    private String clientId; // oauth2 client id
+    @JsonProperty("super_client_id")
+    private String superClientId;
+    private String name;
+    private String description;
+    private String url;
+    @JsonProperty("redirect_uri")
+    private String redirectUri;
+    @JsonProperty("installed_date")
+    private String installedDate;
+
+    public InstalledPluginDto (InstalledPlugin plugin) {
+        OAuth2Client client = plugin.getClient();
+        setClientId(client.getId());
+        setSuperClientId(plugin.getSuperClient().getId());
+        setInstalledDate(plugin.getInstalledDate().toString());
+        setName(client.getName());
+        setDescription(client.getDescription());
+        setUrl(client.getUrl());
+        setRedirectUri(client.getRedirectURI());
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/LayerDto.java b/src/main/java/de/ids_mannheim/korap/dto/LayerDto.java
new file mode 100644
index 0000000..b9884d8
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/LayerDto.java
@@ -0,0 +1,21 @@
+package de.ids_mannheim.korap.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Data transfer object for layer description (e.g. for KorapSRU).
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+public class LayerDto {
+
+    private int id;
+    private String code;
+    private String layer;
+    private String foundry;
+    private String description;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/QueryAccessDto.java b/src/main/java/de/ids_mannheim/korap/dto/QueryAccessDto.java
new file mode 100644
index 0000000..84b8b3f
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/QueryAccessDto.java
@@ -0,0 +1,30 @@
+package de.ids_mannheim.korap.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines the structure of query accesses, e.g. as JSON
+ * objects in HTTP Responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+public class QueryAccessDto {
+    private int accessId;
+    private String createdBy;
+    private int queryId;
+    private String queryName;
+    private int userGroupId;
+    private String userGroupName;
+
+    @Override
+    public String toString () {
+        return "accessId=" + accessId + ", createdBy=" + createdBy
+                + " , queryId=" + queryId + ", queryName=" + queryName
+                + ", userGroupId=" + userGroupId + ", userGroupName="
+                + userGroupName;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/QueryDto.java b/src/main/java/de/ids_mannheim/korap/dto/QueryDto.java
new file mode 100644
index 0000000..6d31fd6
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/QueryDto.java
@@ -0,0 +1,39 @@
+package de.ids_mannheim.korap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.entity.QueryDO;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines the structure of {@link QueryDO} description to be
+ * sent as JSON objects in HTTP responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@JsonInclude(Include.NON_DEFAULT)
+public class QueryDto {
+
+    private int id;
+    private String name;
+    private String type;
+    private String status;
+    private String description;
+    private String requiredAccess;
+    private String createdBy;
+
+    private int numberOfDoc;
+    private int numberOfParagraphs;
+    private int numberOfSentences;
+    private int numberOfTokens;
+
+    private String query;
+    private String queryLanguage;
+    private JsonNode koralQuery;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/ResourceDto.java b/src/main/java/de/ids_mannheim/korap/dto/ResourceDto.java
new file mode 100644
index 0000000..27952a6
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/ResourceDto.java
@@ -0,0 +1,31 @@
+package de.ids_mannheim.korap.dto;
+
+import java.util.Map;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Data transfer object for resource / corpus description (e.g. for
+ * KorapSRU).
+ * 
+ * @author margaretha
+ *
+ */
+@Setter
+@Getter
+public class ResourceDto {
+
+    private String resourceId;
+    private Map<String, String> titles;
+    private String description;
+    private String[] languages;
+    private Map<Integer, String> layers;
+
+    @Override
+    public String toString () {
+        return "resourceId= " + resourceId + ", description= " + description
+                + ", titles= " + titles + ", languages= " + languages
+                + ", layers= " + layers;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java b/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
new file mode 100644
index 0000000..273d52e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
@@ -0,0 +1,34 @@
+package de.ids_mannheim.korap.dto;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines the structure of UserGroup description, e.g.
+ * to be sent as JSON objects in HTTP response.
+ * 
+ * @author margaretha
+ *
+ */
+@Setter
+@Getter
+public class UserGroupDto {
+
+    private int id;
+    private String name;
+    private String description;
+    private String owner;
+    private UserGroupStatus status;
+
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private List<UserGroupMemberDto> members;
+
+    private GroupMemberStatus userMemberStatus;
+    private List<String> userRoles;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java b/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
new file mode 100644
index 0000000..23cfa3d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
@@ -0,0 +1,22 @@
+package de.ids_mannheim.korap.dto;
+
+import java.util.List;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines UserGroupMember description, e.g. to be sent as
+ * JSON objects in HTTP Responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Setter
+@Getter
+public class UserGroupMemberDto {
+    private String userId;
+    private GroupMemberStatus status;
+    private List<String> roles;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/AnnotationConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/AnnotationConverter.java
new file mode 100644
index 0000000..a1cc536
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/AnnotationConverter.java
@@ -0,0 +1,154 @@
+package de.ids_mannheim.korap.dto.converter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.core.entity.Annotation;
+import de.ids_mannheim.korap.core.entity.AnnotationKey;
+import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.dto.FoundryDto;
+import de.ids_mannheim.korap.dto.FoundryDto.Key;
+import de.ids_mannheim.korap.dto.FoundryDto.Layer;
+import de.ids_mannheim.korap.dto.LayerDto;
+
+/**
+ * AnnotationConverter prepares data transfer objects (DTOs) from
+ * entities. The DTOs, for instance, are serialized into JSON in the
+ * controller classes and then sent as the response entity.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class AnnotationConverter {
+
+    /**
+     * Returns layer descriptions in a list of {@link LayerDto}s.
+     * 
+     * @param pairs
+     *            a list of {@link AnnotationLayer}s
+     * @return a list of {@link LayerDto}s
+     */
+    public List<LayerDto> convertToLayerDto (List<AnnotationLayer> pairs) {
+        List<LayerDto> layerDtos = new ArrayList<LayerDto>(pairs.size());
+        LayerDto dto;
+        String foundry, layer;
+        for (AnnotationLayer p : pairs) {
+            dto = new LayerDto();
+            dto.setId(p.getId());
+            dto.setDescription(p.getDescription());
+
+            foundry = p.getFoundry().getCode();
+            dto.setFoundry(foundry);
+
+            layer = p.getLayer().getCode();
+            dto.setLayer(layer);
+            dto.setCode(foundry + "/" + layer);
+            layerDtos.add(dto);
+        }
+
+        return layerDtos;
+    }
+
+    /**
+     * Returns foundry description in {@link FoundryDto}s
+     * 
+     * @param pairs
+     *            a list of {@link AnnotationLayer}s
+     * @param language
+     * @return a list of {@link FoundryDto}s
+     */
+    public List<FoundryDto> convertToFoundryDto (List<AnnotationLayer> pairs,
+            String language) {
+        List<FoundryDto> foundryDtos = new ArrayList<FoundryDto>(pairs.size());
+        Map<String, List<AnnotationLayer>> foundryMap = createFoundryMap(pairs);
+
+        for (String foundryCode : foundryMap.keySet()) {
+            List<AnnotationLayer> foundries = foundryMap.get(foundryCode);
+            List<Layer> layers = new ArrayList<Layer>(foundries.size());
+            FoundryDto dto = null;
+
+            for (AnnotationLayer f : foundries) {
+                if (dto == null) {
+                    Annotation foundry = f.getFoundry();
+                    dto = new FoundryDto();
+                    if (language.equals("de")) {
+                        dto.setDescription(foundry.getGermanDescription());
+                    }
+                    else {
+                        dto.setDescription(foundry.getDescription());
+                    }
+                    dto.setCode(foundry.getCode());
+                }
+
+                Annotation layer = f.getLayer();
+                Set<Key> keys = new TreeSet<>();
+
+                for (AnnotationKey ak : f.getKeys()) {
+                    Annotation a = ak.getKey();
+                    Map<String, String> values = new TreeMap<>();
+
+                    Key key = dto.new Key(a.getCode());
+                    if (language.equals("de")) {
+                        key.setDescription(a.getGermanDescription());
+                        for (Annotation v : ak.getValues()) {
+                            values.put(v.getCode(), v.getGermanDescription());
+                        }
+
+                    }
+                    else {
+                        key.setDescription(a.getDescription());
+                        for (Annotation v : ak.getValues()) {
+                            values.put(v.getCode(), v.getDescription());
+                        }
+                    }
+                    key.setValues(values);
+                    keys.add(key);
+                }
+
+                Layer l = dto.new Layer();
+                l.setCode(layer.getCode());
+
+                if (language.equals("de")) {
+                    l.setDescription(layer.getGermanDescription());
+                }
+                else {
+                    l.setDescription(layer.getDescription());
+                }
+
+                l.setKeys(keys);
+                layers.add(l);
+            }
+
+            dto.setLayers(layers);
+            foundryDtos.add(dto);
+        }
+
+        return foundryDtos;
+    }
+
+    private Map<String, List<AnnotationLayer>> createFoundryMap (
+            List<AnnotationLayer> pairs) {
+        Map<String, List<AnnotationLayer>> foundries = new HashMap<String, List<AnnotationLayer>>();
+        for (AnnotationLayer p : pairs) {
+            String foundryCode = p.getFoundry().getCode();
+            if (foundries.containsKey(foundryCode)) {
+                foundries.get(foundryCode).add(p);
+            }
+            else {
+                List<AnnotationLayer> foundryList = new ArrayList<AnnotationLayer>();
+                foundryList.add(p);
+                foundries.put(foundryCode, foundryList);
+            }
+        }
+
+        return foundries;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/QueryAccessConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/QueryAccessConverter.java
new file mode 100644
index 0000000..5dfad63
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/QueryAccessConverter.java
@@ -0,0 +1,42 @@
+package de.ids_mannheim.korap.dto.converter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.dto.QueryAccessDto;
+import de.ids_mannheim.korap.entity.QueryAccess;
+
+/**
+ * QueryAccessConverter prepares data transfer objects (DTOs)
+ * from {@link QueryAccess} entities. DTO structure defines
+ * controllers output, namely the structure of JSON objects in HTTP
+ * responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class QueryAccessConverter {
+
+    public List<QueryAccessDto> createQueryAccessDto (
+            List<QueryAccess> accessList) {
+        List<QueryAccessDto> dtos = new ArrayList<>(accessList.size());
+        for (QueryAccess access : accessList) {
+            QueryAccessDto dto = new QueryAccessDto();
+            dto.setAccessId(access.getId());
+            dto.setCreatedBy(access.getCreatedBy());
+
+            dto.setQueryId(access.getQuery().getId());
+            dto.setQueryName(access.getQuery().getName());
+
+            dto.setUserGroupId(access.getUserGroup().getId());
+            dto.setUserGroupName(access.getUserGroup().getName());
+
+            dtos.add(dto);
+        }
+        return dtos;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/QueryConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/QueryConverter.java
new file mode 100644
index 0000000..3d469a9
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/QueryConverter.java
@@ -0,0 +1,50 @@
+package de.ids_mannheim.korap.dto.converter;
+
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.dto.QueryDto;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * QueryConverter prepares data transfer objects (DTOs) from
+ * {@link QueryDO} entities. DTO structure defines controllers
+ * output, namely the structure of JSON objects in HTTP responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class QueryConverter {
+
+    public QueryDto createQueryDto (QueryDO query, String statistics)
+            throws KustvaktException {
+
+        QueryDto dto = new QueryDto();
+        dto.setId(query.getId());
+        dto.setName(query.getName());
+        dto.setCreatedBy(query.getCreatedBy());
+        dto.setRequiredAccess(query.getRequiredAccess().name());
+        dto.setStatus(query.getStatus());
+        dto.setDescription(query.getDescription());
+        dto.setType(query.getType().displayName());
+
+        dto.setQuery(query.getQuery());
+        dto.setQueryLanguage(query.getQueryLanguage());
+
+        // JsonNode kq = JsonUtils.readTree(query.getKoralQuery());
+        // dto.setKoralQuery(kq);
+
+        if (statistics != null) {
+            JsonNode node = JsonUtils.readTree(statistics);
+            dto.setNumberOfDoc(node.at("/documents").asInt());
+            dto.setNumberOfParagraphs(node.at("/paragraphs").asInt());
+            dto.setNumberOfSentences(node.at("/sentences").asInt());
+            dto.setNumberOfTokens(node.at("/tokens").asInt());
+        }
+        return dto;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/ResourceConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/ResourceConverter.java
new file mode 100644
index 0000000..d04576c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/ResourceConverter.java
@@ -0,0 +1,57 @@
+package de.ids_mannheim.korap.dto.converter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.core.entity.AnnotationLayer;
+import de.ids_mannheim.korap.core.entity.Resource;
+import de.ids_mannheim.korap.dto.ResourceDto;
+
+/**
+ * ResourceConverter prepares data transfer objects (DTOs) from
+ * {@link Resource} entities. DTO structure defines controllers
+ * output, namely the structure of JSON objects in HTTP responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class ResourceConverter {
+
+    public List<ResourceDto> convertToResourcesDto (List<Resource> resources) {
+        List<ResourceDto> resourceDtoList = new ArrayList<ResourceDto>(
+                resources.size());
+        ResourceDto dto;
+        Map<String, String> titles;
+        HashMap<Integer, String> layers;
+        for (Resource r : resources) {
+            dto = new ResourceDto();
+            dto.setDescription(r.getEnglishDescription());
+            dto.setResourceId(r.getId());
+            dto.setLanguages(new String[] { "deu" });
+
+            titles = new HashMap<String, String>();
+            titles.put("en", r.getEnglishTitle());
+            titles.put("de", r.getGermanTitle());
+            dto.setTitles(titles);
+
+            layers = new HashMap<Integer, String>();
+            String foundry, layer, code;
+            for (AnnotationLayer annotationPair : r.getLayers()) {
+                foundry = annotationPair.getFoundry().getCode();
+                layer = annotationPair.getLayer().getCode();
+                code = foundry + "/" + layer;
+                layers.put(annotationPair.getId(), code);
+            }
+            dto.setLayers(layers);
+
+            resourceDtoList.add(dto);
+        }
+
+        return resourceDtoList;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
new file mode 100644
index 0000000..706cc21
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.dto.converter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.dto.UserGroupDto;
+import de.ids_mannheim.korap.dto.UserGroupMemberDto;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+
+/**
+ * UserGroupConverter manages conversion of {@link UserGroup} objects
+ * to their data access objects (DTO), e.g. UserGroupDto. DTO
+ * structure defines controllers output, namely the structure of JSON
+ * objects in HTTP responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class UserGroupConverter {
+
+    public UserGroupDto createUserGroupDto (UserGroup group,
+            List<UserGroupMember> members, GroupMemberStatus userMemberStatus,
+            Set<Role> roleSet) {
+
+        UserGroupDto dto = new UserGroupDto();
+        dto.setId(group.getId());
+        dto.setName(group.getName());
+        dto.setDescription(group.getDescription());
+        dto.setStatus(group.getStatus());
+        dto.setOwner(group.getCreatedBy());
+        dto.setUserMemberStatus(userMemberStatus);
+
+        if (roleSet != null) {
+            dto.setUserRoles(convertRoleSetToStringList(roleSet));
+        }
+
+        if (members != null) {
+            ArrayList<UserGroupMemberDto> memberDtos = new ArrayList<>(
+                    members.size());
+            for (UserGroupMember member : members) {
+
+                UserGroupMemberDto memberDto = new UserGroupMemberDto();
+                memberDto.setUserId(member.getUserId());
+                memberDto.setStatus(member.getStatus());
+                memberDto.setRoles(
+                        convertRoleSetToStringList(member.getRoles()));
+                memberDtos.add(memberDto);
+            }
+            dto.setMembers(memberDtos);
+        }
+        else {
+            dto.setMembers(new ArrayList<UserGroupMemberDto>());
+        }
+
+        return dto;
+    }
+
+    private List<String> convertRoleSetToStringList (Set<Role> roleSet) {
+        List<String> roles = new ArrayList<>(roleSet.size());
+        for (Role r : roleSet) {
+            roles.add(r.getName());
+        }
+        Collections.sort(roles);
+        return roles;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/encryption/DefaultEncryption.java b/src/main/java/de/ids_mannheim/korap/encryption/DefaultEncryption.java
new file mode 100644
index 0000000..6792ff3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/encryption/DefaultEncryption.java
@@ -0,0 +1,66 @@
+package de.ids_mannheim.korap.encryption;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+
+import de.ids_mannheim.korap.config.Configurable;
+import de.ids_mannheim.korap.config.ContextHolder;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+
+/**
+ * @author hanl
+ * @date 05/06/2015
+ */
+@Configurable(ContextHolder.KUSTVAKT_ENCRYPTION)
+public class DefaultEncryption implements EncryptionIface {
+
+    private SecureRandom randomizer;
+
+    public DefaultEncryption () {
+        randomizer = new SecureRandom();
+    }
+
+    @Override
+    public String secureHash (String input, String salt)
+            throws KustvaktException {
+        return null;
+    }
+
+    @Override
+    public String secureHash (String input) {
+        return null;
+    }
+
+    @Override
+    public boolean checkHash (String plain, String hash, String salt) {
+        return false;
+    }
+
+    @Override
+    public boolean checkHash (String plain, String hash) {
+        return false;
+    }
+
+    @Override
+    public String createToken (boolean hash, Object ... obj) {
+        return createToken();
+
+    }
+
+    @Override
+    public String createToken () {
+        return new BigInteger(100, randomizer).toString(20);
+    }
+
+    @Override
+    public String createRandomNumber (Object ... obj) {
+        return createToken();
+    }
+
+    @Override
+    public String encodeBase () {
+        return null;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java b/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
new file mode 100644
index 0000000..610f69c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
@@ -0,0 +1,281 @@
+package de.ids_mannheim.korap.encryption;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import org.apache.commons.codec.EncoderException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.mindrot.jbcrypt.BCrypt;
+
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+
+public class KustvaktEncryption implements EncryptionIface {
+
+    private static final String ALGORITHM = "SHA-256";
+    private static Logger jlog = LogManager.getLogger(KustvaktEncryption.class);
+
+    private final FullConfiguration config;
+
+    public KustvaktEncryption (FullConfiguration config) {
+        jlog.info("initializing KorAPEncryption implementation");
+        this.config = config;
+    }
+
+    public static boolean matchTokenByteCode (Object param) {
+        if (!(param instanceof String))
+            return false;
+        String token = (String) param;
+        byte[] bytes = token.getBytes();
+        return 64 == bytes.length;
+    }
+
+    private String encodeBase (byte[] bytes) throws EncoderException {
+        return Base64.encodeBase64String(bytes);
+    }
+
+    @Override
+    public String encodeBase () {
+        try {
+            return encodeBase(this.createSecureRandom(24));
+        }
+        catch (EncoderException e) {
+            return "";
+        }
+    }
+
+    public String secureHash (String input) {
+        return secureHash(input, "");
+    }
+
+    @Override
+    public String secureHash (String input, String salt) {
+        String hashString = "";
+        switch (config.getSecureHashAlgorithm()) {
+            case ESAPICYPHER:
+                break;
+            case SIMPLE:
+                try {
+                    MessageDigest md = MessageDigest.getInstance("SHA-256");
+                    md.update(input.getBytes("UTF-8"));
+                    byte[] digest = md.digest();
+
+                    for (byte b : digest)
+                        hashString += String.format("%02x", b);
+                }
+                catch (UnsupportedEncodingException
+                        | NoSuchAlgorithmException e) {
+                    e.printStackTrace();
+                }
+                break;
+            case BCRYPT:
+                hashString = bcryptHash(input, salt);
+                break;
+            default:
+                jlog.warn("Invalid value: " + config.getSecureHashAlgorithm());
+                break;
+        }
+        return hashString;
+    }
+
+    public String hash (String input) {
+        String hashString = "";
+        MessageDigest md;
+        try {
+            md = MessageDigest.getInstance(ALGORITHM);
+            md.update(input.getBytes("UTF-8"));
+        }
+        catch (NoSuchAlgorithmException e) {
+            return "";
+        }
+        catch (UnsupportedEncodingException e) {
+            return "";
+        }
+
+        byte[] digest = md.digest();
+
+        for (byte b : digest) {
+            hashString += String.format("%02x", b);
+        }
+        return hashString;
+    }
+
+    /**
+     * // some sort of algorithm to create token and isSystem
+     * regularly the integrity
+     * // of the token
+     * public String createAuthToken() {
+     * final byte[] rNumber = SecureRGenerator
+     * .getNextSecureRandom(SecureRGenerator.TOKEN_RANDOM_SIZE);
+     * String hash;
+     * try {
+     * hash = produceSimpleHash(SecureRGenerator.toHex(rNumber));
+     * } catch (NoSuchAlgorithmException |
+     * UnsupportedEncodingException e) {
+     * return "";
+     * }
+     * return hash;
+     * }
+     */
+
+    private byte[] createSecureRandom (int size) {
+        return SecureRGenerator.getNextSecureRandom(size);
+    }
+
+    /**
+     * does this need to be equal for every iteration?!
+     * 
+     * @param hash
+     * @param obj
+     * @return
+     */
+    @Override
+    public String createToken (boolean hash, Object ... obj) {
+        StringBuffer b = new StringBuffer();
+        try {
+            for (Object o : obj) {
+                b.append(" | ");
+                b.append(o);
+            }
+            if (hash)
+                return encodeBase(hash(b.toString().trim()).getBytes());
+            else
+                return encodeBase(b.toString().trim().getBytes());
+        }
+        catch (EncoderException e) {
+            return "";
+        }
+
+    }
+
+    @Override
+    public String createToken () {
+        return RandomStringUtils.randomAlphanumeric(64);
+
+        // EM: code from MH
+        //        String encoded;
+        //        String v = RandomStringUtils.randomAlphanumeric(64);
+        //        encoded = hash(v);
+        //        jlog.trace("creating new token {}", encoded);
+        //        return encoded;
+    }
+
+    @Override
+    public String createRandomNumber (Object ... obj) {
+        final byte[] rNumber = SecureRGenerator
+                .getNextSecureRandom(SecureRGenerator.ID_RANDOM_SIZE);
+        if (obj.length == 0) {
+            obj = new Object[1];
+            obj[0] = rNumber;
+        }
+        return createToken(false, obj);
+    }
+
+    @Override
+    public boolean checkHash (String plain, String hash, String salt) {
+        String pw = "";
+        switch (config.getSecureHashAlgorithm()) {
+            case ESAPICYPHER:
+                pw = secureHash(plain, salt);
+                break;
+            case BCRYPT:
+                try {
+                    return BCrypt.checkpw(plain, hash);
+                }
+                catch (IllegalArgumentException e) {
+                    return false;
+                }
+            case SIMPLE:
+                pw = hash(plain);
+                break;
+        }
+        return pw.equals(hash);
+    }
+
+    @Override
+    public boolean checkHash (String plain, String hash) {
+        switch (config.getSecureHashAlgorithm()) {
+            case ESAPICYPHER:
+                return secureHash(plain).equals(hash);
+            case BCRYPT:
+                try {
+                    return BCrypt.checkpw(plain, hash);
+                }
+                catch (IllegalArgumentException e) {
+                    return false;
+                }
+            case SIMPLE:
+                return hash(plain).equals(hash);
+        }
+        return false;
+    }
+
+    private String bcryptHash (String text, String salt) {
+        if (salt == null || salt.isEmpty())
+            salt = BCrypt.gensalt(config.getLoadFactor());
+        return BCrypt.hashpw(text, salt);
+    }
+
+    @Override
+    public String toString () {
+        return this.getClass().getCanonicalName();
+    }
+
+    public static class SecureRGenerator {
+        private static final String SHA1_PRNG = "SHA1PRNG";
+        protected static final int DEFAULT_RANDOM_SIZE = 128;
+        protected static final int TOKEN_RANDOM_SIZE = 128;
+        protected static final int ID_RANDOM_SIZE = 128;
+        protected static final int CORPUS_RANDOM_SIZE = 64;
+        private static final char[] HEX_DIGIT = { '0', '1', '2', '3', '4', '5',
+                '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'z', 'x', 'h',
+                'q', 'w' };
+        private static final SecureRandom sRandom__;
+
+        static {
+            try {
+                sRandom__ = SecureRandom.getInstance("SHA1PRNG");
+            }
+            catch (NoSuchAlgorithmException e) {
+                throw new Error(e);
+            }
+        }
+
+        public static byte[] getNextSecureRandom (int bits) {
+            if (bits % 8 != 0) {
+                throw new IllegalArgumentException(
+                        "Size is not divisible by 8!");
+            }
+
+            byte[] bytes = new byte[bits / 8];
+
+            sRandom__.nextBytes(bytes);
+
+            return bytes;
+        }
+
+        public static String toHex (byte[] bytes) {
+            if (bytes == null) {
+                return null;
+            }
+
+            StringBuilder buffer = new StringBuilder(bytes.length * 2);
+            for (byte thisByte : bytes) {
+                buffer.append(byteToHex(thisByte));
+            }
+
+            return buffer.toString();
+        }
+
+        private static String byteToHex (byte b) {
+            char[] array = { HEX_DIGIT[(b >> 4 & 0xF)], HEX_DIGIT[(b & 0xF)] };
+            return new String(array);
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/encryption/RandomCodeGenerator.java b/src/main/java/de/ids_mannheim/korap/encryption/RandomCodeGenerator.java
new file mode 100644
index 0000000..e6ff6e0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/encryption/RandomCodeGenerator.java
@@ -0,0 +1,104 @@
+package de.ids_mannheim.korap.encryption;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import jakarta.annotation.PostConstruct;
+
+/**
+ * Generates a random string that can be used for tokens, client id,
+ * client secret, etc.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class RandomCodeGenerator {
+
+    public static String alphanumeric = "34679bdfhmnprtFGHJLMNPRT";
+    public static List<Character> charList = alphanumeric.chars()
+            .mapToObj(c -> (char) c).collect(Collectors.toList());
+
+    public final static List<String> LIMITED_CHARACTERS = Arrays
+            .asList(new String[] { "3", "4", "6", "7", "9", "b", "d", "f", "h",
+                    "m", "n", "p", "r", "t", "F", "G", "H", "J", "L", "M", "N",
+                    "P", "R", "T" });
+
+    private Logger log = LogManager.getLogger(RandomCodeGenerator.class);
+
+    @Autowired
+    public KustvaktConfiguration config;
+
+    public static SecureRandom secureRandom;
+
+    @PostConstruct
+    public void init () throws NoSuchAlgorithmException {
+        String algorithm = config.getSecureRandomAlgorithm();
+        if (!algorithm.isEmpty()) {
+            secureRandom = SecureRandom.getInstance(algorithm);
+        }
+        else {
+            secureRandom = new SecureRandom();
+        }
+        log.info("Secure random algorithm: " + secureRandom.getAlgorithm());
+    }
+
+    public String createRandomCode (KustvaktConfiguration c)
+            throws KustvaktException, NoSuchAlgorithmException {
+        config = c;
+        init();
+        return createRandomCode();
+    }
+
+    public String createRandomCode () throws KustvaktException {
+        UUID randomUUID = UUID.randomUUID();
+        byte[] uuidBytes = randomUUID.toString().getBytes();
+        byte[] secureBytes = new byte[3];
+        secureRandom.nextBytes(secureBytes);
+
+        byte[] bytes = ArrayUtils.addAll(uuidBytes, secureBytes);
+
+        try {
+            MessageDigest md = MessageDigest
+                    .getInstance(config.getMessageDigestAlgorithm());
+            md.update(bytes);
+            byte[] digest = md.digest();
+            String code = Base64.encodeBase64URLSafeString(digest);
+            md.reset();
+            return code;
+        }
+        catch (NoSuchAlgorithmException e) {
+            throw new KustvaktException(StatusCodes.INVALID_ALGORITHM,
+                    config.getMessageDigestAlgorithm()
+                            + "is not a valid MessageDigest algorithm");
+        }
+    }
+
+    public String filterRandomCode (String code) {
+        StringBuffer s = new StringBuffer();
+        for (char c : code.toCharArray()) {
+            if (!charList.contains(c)) {
+                int n = ThreadLocalRandom.current().nextInt(0, charList.size());
+                c = charList.get(n);
+            }
+            s.append(c);
+        }
+        return s.toString();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/Admin.java b/src/main/java/de/ids_mannheim/korap/entity/Admin.java
new file mode 100644
index 0000000..b0cbed9
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/Admin.java
@@ -0,0 +1,35 @@
+package de.ids_mannheim.korap.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes admin users
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "admin")
+public class Admin {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "user_id")
+    private String userId;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", userId=" + userId;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/DefaultSetting.java b/src/main/java/de/ids_mannheim/korap/entity/DefaultSetting.java
new file mode 100644
index 0000000..d09e2b0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/DefaultSetting.java
@@ -0,0 +1,56 @@
+package de.ids_mannheim.korap.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+/**
+ * Describes default_setting table. Each user may define one set of
+ * default settings. The elements of default settings are
+ * not strictly defined and thus are described as JSON strings.
+ * 
+ * Some elements that are often used may be adopted as columns.
+ * 
+ * Examples of the default settings' elements are foundry, layer and
+ * number of results per page.
+ * 
+ * @author margaretha
+ *
+ */
+@Entity
+@Table(name = "default_setting")
+public class DefaultSetting {
+
+    @Id
+    private String username;
+    private String settings; // json string
+
+    public DefaultSetting () {}
+
+    public DefaultSetting (String username, String settings) {
+        this.username = username;
+        this.settings = settings;
+    }
+
+    @Override
+    public String toString () {
+        return "name= " + getUsername() + ", settings= " + getSettings();
+    }
+
+    public String getUsername () {
+        return username;
+    }
+
+    public void setUsername (String username) {
+        this.username = username;
+    }
+
+    public String getSettings () {
+        return settings;
+    }
+
+    public void setSettings (String settings) {
+        this.settings = settings;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/InstalledPlugin.java b/src/main/java/de/ids_mannheim/korap/entity/InstalledPlugin.java
new file mode 100644
index 0000000..12e19ce
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/InstalledPlugin.java
@@ -0,0 +1,46 @@
+package de.ids_mannheim.korap.entity;
+
+import java.time.ZonedDateTime;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@Entity
+@Table(name = "installed_plugin")
+public class InstalledPlugin implements Comparable<InstalledPlugin> {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "installed_by")
+    private String installedBy;
+    @Column(name = "installed_date")
+    private ZonedDateTime installedDate;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "client_id")
+    private OAuth2Client client;
+
+    // where a plugin is installed
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "super_client_id")
+    private OAuth2Client superClient;
+
+    @Override
+    public int compareTo (InstalledPlugin o) {
+        return this.client.compareTo(o.client);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/Privilege.java b/src/main/java/de/ids_mannheim/korap/entity/Privilege.java
new file mode 100644
index 0000000..0828595
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/Privilege.java
@@ -0,0 +1,49 @@
+package de.ids_mannheim.korap.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes privilege table listing users and their roles.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "privilege")
+public class Privilege {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Enumerated(EnumType.STRING)
+    private PrivilegeType name;
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "role_id", referencedColumnName = "id")
+    private Role role;
+
+    public Privilege () {}
+
+    public Privilege (PrivilegeType name, Role role) {
+        this.name = name;
+        this.role = role;
+    }
+
+    public String toString () {
+        return "id=" + id + ", name=" + name + ", role=" + role;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/QueryAccess.java b/src/main/java/de/ids_mannheim/korap/entity/QueryAccess.java
new file mode 100644
index 0000000..42c7968
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/QueryAccess.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.constant.QueryAccessStatus;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes the relationship between virtual corpora and user groups,
+ * i.e. which groups may access which virtual corpora, and the history
+ * of group-access management.
+ * 
+ * @author margaretha
+ * @see QueryDO
+ * @see UserGroup
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "query_access")
+public class QueryAccess {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "created_by")
+    private String createdBy;
+    @Column(name = "approved_by")
+    private String approvedBy;
+    @Column(name = "deleted_by")
+    private String deletedBy;
+
+    @Enumerated(EnumType.STRING)
+    private QueryAccessStatus status;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "query_id", referencedColumnName = "id")
+    private QueryDO query;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "user_group_id", referencedColumnName = "id")
+    private UserGroup userGroup;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", query= " + query + ", userGroup= " + userGroup;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java b/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java
new file mode 100644
index 0000000..77d8102
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java
@@ -0,0 +1,104 @@
+package de.ids_mannheim.korap.entity;
+
+import java.util.List;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes the query table and its relation to {@link QueryAccess}.
+ * 
+ * Any user may create a query and share it to a user group.
+ * However, if the user is not a user-group admin, the query
+ * will not be shared until a user-group admin accept his/her request.
+ * 
+ * @author margaretha
+ *
+ * @see QueryAccess
+ * @see UserGroup
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "query")
+public class QueryDO implements Comparable<QueryDO> {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String name;
+    @Enumerated(EnumType.STRING)
+    private ResourceType type;
+    private String status;
+    private String description;
+    @Enumerated(EnumType.STRING)
+    @Column(name = "required_access")
+    private CorpusAccess requiredAccess;
+    @Column(name = "koral_query")
+    private String koralQuery;
+    private String definition;
+    @Column(name = "created_by")
+    private String createdBy;
+    @Column(name = "is_cached")
+    private boolean isCached;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "query_type")
+    private QueryType queryType;
+    private String query;
+    @Column(name = "query_language")
+    private String queryLanguage;
+
+    @OneToMany(mappedBy = "query", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
+    private List<QueryAccess> queryAccess;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", name= " + name + ", type= " + type + ", status= "
+                + status + ", description=" + description + ", requiredAccess="
+                + requiredAccess + ", koralQuery= " + koralQuery
+                + ", definition= " + definition + ", createdBy= " + createdBy;
+    }
+
+    @Override
+    public int hashCode () {
+        int prime = 37;
+        int result = 1;
+        result = prime * result + id;
+        result = prime * result + name.hashCode();
+        result = prime * result + createdBy.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals (Object obj) {
+        QueryDO query = (QueryDO) obj;
+        return (this.id == query.getId()) ? true : false;
+    }
+
+    @Override
+    public int compareTo (QueryDO o) {
+        if (this.getId() > o.getId()) {
+            return 1;
+        }
+        else if (this.getId() < o.getId()) {
+            return -1;
+        }
+        return 0;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/Role.java b/src/main/java/de/ids_mannheim/korap/entity/Role.java
new file mode 100644
index 0000000..f096c80
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/Role.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.entity;
+
+import java.util.List;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes user roles for example in managing a group or
+ * virtual corpora of a group.
+ * 
+ * @author margaretha
+ * @see Privilege
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "role")
+public class Role implements Comparable<Role> {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(unique = true)
+    private String name;
+
+    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
+    private List<UserGroupMember> userGroupMembers;
+
+    @OneToMany(mappedBy = "role", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
+    private List<Privilege> privileges;
+
+    public String toString () {
+        return "id=" + id + ", name=" + name;
+    }
+
+    @Override
+    public int compareTo (Role o) {
+        if (this.getId() > o.getId()) {
+            return 1;
+        }
+        else if (this.getId() < o.getId()) {
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean equals (Object obj) {
+        Role r = (Role) obj;
+        if (this.id == r.getId() && this.name.equals(r.getName())) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode () {
+        int hash = 7;
+        hash = 31 * hash + (int) id;
+        hash = 31 * hash + (name == null ? 0 : name.hashCode());
+        return hash;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java b/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java
new file mode 100644
index 0000000..9a7db7d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.entity;
+
+import java.util.List;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes user group table and its relations to UserGroupMember and
+ * {@link QueryAccess}.
+ * 
+ * Any user may create a user group and send invitations to group
+ * member by username. Any group member may reject the invitation
+ * or unsubscribe from the group.
+ * 
+ * @author margaretha
+ * @see UserGroupMember
+ * @see QueryAccess
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "user_group")
+public class UserGroup implements Comparable<UserGroup> {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    // unique
+    private String name;
+    private String description;
+    @Column(name = "created_by")
+    private String createdBy;
+    @Column(name = "deleted_by")
+    private String deletedBy;
+
+    @Enumerated(EnumType.STRING)
+    private UserGroupStatus status;
+
+    @OneToMany(mappedBy = "group", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
+    private List<UserGroupMember> members;
+
+    @OneToMany(mappedBy = "userGroup", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
+    private List<QueryAccess> queryAccess;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", name= " + name + ", createdBy= " + createdBy;
+    }
+
+    @Override
+    public int compareTo (UserGroup o) {
+        if (this.getId() > o.getId()) {
+            return 1;
+        }
+        else if (this.getId() < o.getId()) {
+            return -1;
+        }
+        return 0;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java b/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
new file mode 100644
index 0000000..e1cc3e5
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
@@ -0,0 +1,80 @@
+package de.ids_mannheim.korap.entity;
+
+import java.time.ZonedDateTime;
+import java.util.Set;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes members of user groups. Only member of predefined role
+ * group admin can see the rest of members.
+ * 
+ * @author margaretha
+ * @see UserGroup
+ * @see Role
+ * @see PredefinedRole
+ */
+@Setter
+@Getter
+@Entity
+@Table(name = "user_group_member", indexes = {
+        @Index(unique = true, columnList = "user_id, group_id") })
+public class UserGroupMember {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "user_id")
+    private String userId;
+    @Column(name = "created_by")
+    private String createdBy;
+    @Column(name = "deleted_by")
+    private String deletedBy;
+
+    // auto update in the database
+    @Column(name = "status_date")
+    private ZonedDateTime statusDate;
+
+    @Enumerated(EnumType.STRING)
+    private GroupMemberStatus status;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "group_id")
+    private UserGroup group;
+
+    /**
+     * Information about roles is deemed to be always necessary to
+     * describe a member.
+     * 
+     */
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = "group_member_role", joinColumns = @JoinColumn(name = "group_member_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), uniqueConstraints = @UniqueConstraint(columnNames = {
+            "group_member_id", "role_id" }))
+    private Set<Role> roles;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", group= " + group + ", userId= " + userId
+                + ", createdBy= " + createdBy + ", deletedBy= " + deletedBy
+                + ", roles=" + roles;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/exceptions/DatabaseException.java b/src/main/java/de/ids_mannheim/korap/exceptions/DatabaseException.java
new file mode 100644
index 0000000..f0c9bcf
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/exceptions/DatabaseException.java
@@ -0,0 +1,38 @@
+package de.ids_mannheim.korap.exceptions;
+
+import java.util.Arrays;
+
+/**
+ * @author hanl
+ * @date 08/04/2015
+ */
+public class DatabaseException extends KustvaktException {
+
+    private DatabaseException (Object userid, Integer status, String message,
+                               String args, Exception e) {
+        super(String.valueOf(userid), status, message, args, e);
+    }
+
+    public DatabaseException (Object userid, String target, Integer status,
+                              String message, String ... args) {
+        this(null, userid, target, status, message);
+    }
+
+    public DatabaseException (Exception e, Object userid, String target,
+                              Integer status, String message, String ... args) {
+        this(userid, status, message, Arrays.asList(args).toString(), e);
+    }
+
+    public DatabaseException (KustvaktException e, Integer status,
+                              String ... args) {
+        this(e.getUserid(), e.getStatusCode(), e.getMessage(), e.getEntity(),
+                e);
+    }
+
+    @Override
+    public String string () {
+        return "DBExcpt{" + "status=" + getStatusCode() + ", message="
+                + getMessage() + ", args=" + getEntity() + ", userid="
+                + this.getUserid() + '}';
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/exceptions/EmptyResultException.java b/src/main/java/de/ids_mannheim/korap/exceptions/EmptyResultException.java
new file mode 100644
index 0000000..d687d39
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/exceptions/EmptyResultException.java
@@ -0,0 +1,19 @@
+package de.ids_mannheim.korap.exceptions;
+
+/**
+ * @author hanl
+ * @date 25/03/2014
+ */
+@Deprecated
+// even useful anymore?
+public class EmptyResultException extends KustvaktException {
+
+    public EmptyResultException (String message, String entity) {
+        super(StatusCodes.NO_RESULT_FOUND, message, entity);
+    }
+
+    public EmptyResultException (String entity) {
+        super(StatusCodes.NO_RESULT_FOUND, "No entity found for id", entity);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/exceptions/KustvaktException.java b/src/main/java/de/ids_mannheim/korap/exceptions/KustvaktException.java
new file mode 100644
index 0000000..0311878
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/exceptions/KustvaktException.java
@@ -0,0 +1,126 @@
+package de.ids_mannheim.korap.exceptions;
+
+import java.net.URI;
+import java.util.Arrays;
+
+import com.nimbusds.oauth2.sdk.ErrorObject;
+
+//import de.ids_mannheim.korap.constant.TokenType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author hanl
+ * @date 11/12/2013
+ */
+@Setter
+@Getter
+public class KustvaktException extends Exception {
+
+    private static final long serialVersionUID = -1955783565699446322L;
+    private String userid;
+    private Integer statusCode;
+    private int responseStatus;
+
+    private String entity;
+    private String notification;
+    private boolean isNotification;
+    //    private TokenType authType;
+    private URI redirectUri;
+    private ErrorObject oauth2Error;
+
+    public KustvaktException (int status) {
+        this.statusCode = status;
+    }
+
+    public KustvaktException (int status, String ... args) {
+        super(args[0]);
+        this.statusCode = status;
+        String[] subarray = Arrays.copyOfRange(args, 1, args.length);
+        this.entity = Arrays.asList(subarray).toString();
+    }
+
+    public KustvaktException (int status, String notification,
+                              boolean isNotification) {
+        this.statusCode = status;
+        this.notification = notification;
+        this.isNotification = isNotification;
+    }
+
+    public boolean hasNotification () {
+        return isNotification;
+    }
+
+    public KustvaktException (Object userid, int status) {
+        this(status);
+        this.userid = String.valueOf(userid);
+    }
+
+    // add throwable to parameters
+    public KustvaktException (Object userid, int status, String message,
+                              String entity) {
+        this(userid, status, message, entity, null);
+    }
+
+    public KustvaktException (Object userid, int status, String message,
+                              String entity, Exception e) {
+        super(message, e);
+        this.statusCode = status;
+        this.entity = entity;
+        this.userid = String.valueOf(userid);
+    }
+
+    public KustvaktException (Object userid, int status, String entity) {
+        super(StatusCodes.getMessage(status));
+        this.statusCode = status;
+        this.entity = entity;
+        this.userid = String.valueOf(userid);
+    }
+
+    public KustvaktException (int status, String message, String entity) {
+        super(message);
+        this.statusCode = status;
+        this.entity = entity;
+    }
+
+    public KustvaktException (int status, String message, String entity,
+                              Throwable e) {
+        super(message, e);
+        this.statusCode = status;
+        this.entity = entity;
+    }
+
+    public KustvaktException (int status, String message, Throwable e) {
+        super(message, e);
+        this.statusCode = status;
+    }
+
+    public KustvaktException (int status, String message,
+                              ErrorObject oauth2Error) {
+        super(message);
+        this.statusCode = status;
+        this.oauth2Error = oauth2Error;
+        this.entity = oauth2Error.toString();
+    }
+
+    public KustvaktException (Throwable cause, int status) {
+        super(cause);
+        this.statusCode = status;
+    }
+
+    public KustvaktException (String message, Throwable cause, int status) {
+        super(message, cause);
+        this.statusCode = status;
+    }
+
+    public KustvaktException (String notification) {
+        this.notification = notification;
+        isNotification = true;
+    }
+
+    public String string () {
+        return "Excpt{" + "status=" + getStatusCode() + ", message="
+                + getMessage() + ", args=" + getEntity() + ", userid=" + userid
+                + '}';
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java b/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
new file mode 100644
index 0000000..94778c6
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
@@ -0,0 +1,213 @@
+package de.ids_mannheim.korap.exceptions;
+
+import java.util.Properties;
+
+import de.ids_mannheim.korap.config.ConfigLoader;
+
+/**
+ * @author hanl, margaretha
+ * @date 07/09/2014
+ */
+public class StatusCodes {
+
+    /**
+     * 100 status codes for standard system errors
+     */
+    public static final int GENERAL_ERROR = 100;
+    public static final int NO_RESULT_FOUND = 101;
+    public static final int UNSUPPORTED_AUTHENTICATION_SCHEME = 102;
+    public static final int UNSUPPORTED_OPERATION = 103;
+    public static final int ILLEGAL_ARGUMENT = 104;
+    public static final int MISSING_PARAMETER = 105;
+    public static final int CONNECTION_ERROR = 106;
+    public static final int INVALID_ARGUMENT = 107;
+    public static final int NOT_SUPPORTED = 108;
+    public static final int NOT_ALLOWED = 109;
+    public static final int HTTPS_REQUIRED = 110;
+    public static final int INVALID_ALGORITHM = 111;
+    public static final int UNSUPPORTED_API_VERSION = 112;
+    public static final int NON_UNIQUE_RESULT_FOUND = 113;
+    public static final int NO_RESOURCE_FOUND = 114;
+    public static final int DEPRECATED = 115;
+    public static final int CACHING_VC = 116;
+    public static final int NETWORK_ENDPOINT_NOT_AVAILABLE = 117;
+    public static final int SEARCH_NETWORK_ENDPOINT_FAILED = 118;
+
+    /**
+     * 200 status codes general JSON serialization error
+     */
+
+    public static final int SERIALIZATION_FAILED = 200;
+    public static final int DESERIALIZATION_FAILED = 201;
+    public static final int MISSING_ATTRIBUTE = 202;
+    public static final int INVALID_ATTRIBUTE = 203;
+    public static final int UNSUPPORTED_VALUE = 204;
+
+    /**
+     * 300 status codes for query language and serialization
+     * see Koral
+     * (de.ids_mannheim.korap.query.serialize.util.StatusCodes)
+     */
+
+    /**
+     * 400 status codes for rewrite functions
+     */
+
+    public static final int REWRITE_ERROR_DEFAULT = 400;
+    public static final int NON_PUBLIC_FIELD_IGNORED = 401;
+    public static final int PIPE_FAILED = 402;
+
+    //    public static final int UNSUPPORTED_RESOURCE = 402;
+    //    public static final int REWRITE_FAILED = 403;
+    //public static final int UNSUPPORTED_FOUNDRY = 403;
+    //public static final int UNSUPPORTED_CORPUS = 404;
+    //public static final int UNSUPPORTED_LAYER = 405;
+    // make a distinction between no and invalid vc?
+    //public static final int UNSUPPORTED_COLLECTION = 406;
+    //public static final int CORPUS_REWRITE = 407;
+    //public static final int FOUNDRY_REWRITE = 408;
+    //public static final int FOUNDRY_INJECTION = 409;
+    //    public static final int MISSING_RESOURCE = 405;
+    //    public static final int NO_POLICY_TARGET = 406;
+    //    public static final int NO_POLICY_CONDITION = 407;
+    //    public static final int NO_POLICY_PERMISSION = 408;
+    //    public static final int NO_POLICIES = 409;
+
+    /**
+     * 500 status codes for access control related components (also
+     * policy rewrite)
+     */
+    // todo: extend according to policy rewrite possible!
+    // policy errors
+
+    // database codes
+    public static final int DB_GET_FAILED = 500;
+    public static final int DB_INSERT_FAILED = 501;
+    public static final int DB_DELETE_FAILED = 502;
+    public static final int DB_UPDATE_FAILED = 503;
+
+    public static final int DB_GET_SUCCESSFUL = 504;
+    public static final int DB_INSERT_SUCCESSFUL = 505;
+    public static final int DB_DELETE_SUCCESSFUL = 506;
+    public static final int DB_UPDATE_SUCCESSFUL = 507;
+    public static final int DB_ENTRY_EXISTS = 508;
+
+    //    public static final int ARGUMENT_VALIDATION_FAILURE = 700;
+    // public static final int ARGUMENT_VALIDATION_FAILURE = 701;
+
+    // service status codes
+    public static final int CREATE_ACCOUNT_SUCCESSFUL = 700;
+    public static final int CREATE_ACCOUNT_FAILED = 701;
+    public static final int DELETE_ACCOUNT_SUCCESSFUL = 702;
+    public static final int DELETE_ACCOUNT_FAILED = 703;
+    public static final int UPDATE_ACCOUNT_SUCCESSFUL = 704;
+    public static final int UPDATE_ACCOUNT_FAILED = 705;
+
+    public static final int GET_ACCOUNT_SUCCESSFUL = 706;
+    public static final int GET_ACCOUNT_FAILED = 707;
+
+    public static final int STATUS_OK = 1000;
+    public static final int NOTHING_CHANGED = 1001;
+    public static final int INVALID_REQUEST = 1002;
+
+    //    public static final int ACCESS_DENIED = 1003;
+
+    // User group and member 
+    public static final int GROUP_MEMBER_EXISTS = 1601;
+    public static final int GROUP_MEMBER_INACTIVE = 1602;
+    public static final int GROUP_MEMBER_DELETED = 1603;
+    public static final int GROUP_MEMBER_NOT_FOUND = 1604;
+    public static final int INVITATION_EXPIRED = 1605;
+    public static final int GROUP_DELETED = 1606;
+
+    /**
+     * 1800 Oauth2
+     */
+
+    public static final int OAUTH2_SYSTEM_ERROR = 1800;
+
+    public static final int CLIENT_REGISTRATION_FAILED = 1801;
+    public static final int CLIENT_DEREGISTRATION_FAILED = 1802;
+    public static final int CLIENT_AUTHENTICATION_FAILED = 1803;
+    public static final int CLIENT_AUTHORIZATION_FAILED = 1804;
+    public static final int CLIENT_NOT_FOUND = 1805;
+    public static final int INVALID_REDIRECT_URI = 1806;
+    public static final int MISSING_REDIRECT_URI = 1807;
+    public static final int INVALID_SCOPE = 1808;
+    public static final int INVALID_AUTHORIZATION = 1809;
+    public static final int INVALID_REFRESH_TOKEN = 1810;
+
+    public static final int UNSUPPORTED_GRANT_TYPE = 1811;
+    public static final int UNSUPPORTED_AUTHENTICATION_METHOD = 1812;
+    public static final int UNSUPPORTED_RESPONSE_TYPE = 1813;
+
+    public static final int USER_REAUTHENTICATION_REQUIRED = 1822;
+
+    public static final int INVALID_REFRESH_TOKEN_EXPIRY = 1830;
+
+    /**
+     * 1850 Plugins
+     */
+
+    public static final int PLUGIN_NOT_PERMITTED = 1850;
+    public static final int PLUGIN_HAS_BEEN_INSTALLED = 1851;
+
+    /**
+     * 1900 User account and logins
+     */
+
+    public static final int LOGIN_SUCCESSFUL = 1900;
+    public static final int ALREADY_LOGGED_IN = 1901;
+
+    public static final int LOGOUT_SUCCESSFUL = 1902;
+    public static final int LOGOUT_FAILED = 1903;
+
+    public static final int ACCOUNT_CONFIRMATION_FAILED = 1904;
+    public static final int PASSWORD_RESET_FAILED = 1905;
+
+    /**
+     * 2000 status and error codes concerning authentication
+     * 
+     * Response with WWW-Authenticate header will be created
+     * for all KustvaktExceptions with status codes 2001 or greater
+     * 
+     * MH: service level messages and callbacks
+     */
+
+    @Deprecated
+    public static final int INCORRECT_ADMIN_TOKEN = 2000;
+
+    public static final int AUTHENTICATION_FAILED = 2001;
+    public static final int LOGIN_FAILED = 2002;
+    public static final int EXPIRED = 2003;
+    public static final int BAD_CREDENTIALS = 2004;
+    public static final int ACCOUNT_NOT_CONFIRMED = 2005;
+    public static final int ACCOUNT_DEACTIVATED = 2006;
+
+    //    public static final int CLIENT_AUTHORIZATION_FAILED = 2013;
+    public static final int AUTHORIZATION_FAILED = 2010;
+    public static final int INVALID_ACCESS_TOKEN = 2011;
+
+    // 2020 - 2029 reserviert für LDAP-Fehlercodes - 21.04.17/FB
+    public static final int LDAP_BASE_ERRCODE = 2020;
+
+    /**/
+    private static StatusCodes codes;
+
+    private final Properties props;
+
+    private StatusCodes () {
+        this.props = ConfigLoader.loadProperties("codes.info");
+    }
+
+    public static final String getMessage (int code) {
+        return getCodes().props.getProperty(String.valueOf(code));
+    }
+
+    public static StatusCodes getCodes () {
+        if (codes == null)
+            codes = new StatusCodes();
+        return codes;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/exceptions/WrappedException.java b/src/main/java/de/ids_mannheim/korap/exceptions/WrappedException.java
new file mode 100644
index 0000000..2d12165
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/exceptions/WrappedException.java
@@ -0,0 +1,30 @@
+package de.ids_mannheim.korap.exceptions;
+
+import java.util.Arrays;
+
+/**
+ * @author hanl
+ * @date 08/04/2015
+ */
+// should be a http exception that responds to a service point
+// is the extension of the notauthorized exception!
+public class WrappedException extends KustvaktException {
+
+    private static final long serialVersionUID = 1L;
+
+    private WrappedException (Object userid, Integer status, String message,
+                              String args, Exception rootCause) {
+        super(String.valueOf(userid), status, message, args, rootCause);
+    }
+
+    public WrappedException (Object userid, Integer status, String ... args) {
+        this(userid, status, "", Arrays.asList(args).toString(), null);
+    }
+
+    public WrappedException (KustvaktException e, Integer status,
+                             String ... args) {
+        this(e.getUserid(), e.getStatusCode(), e.getMessage(), e.getEntity(),
+                e);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/handlers/BatchBuilder.java b/src/main/java/de/ids_mannheim/korap/handlers/BatchBuilder.java
new file mode 100644
index 0000000..c362773
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/handlers/BatchBuilder.java
@@ -0,0 +1,72 @@
+package de.ids_mannheim.korap.handlers;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author hanl
+ * @date 24/03/2014
+ */
+public class BatchBuilder {
+
+    private static final int SINGLE_BATCH = 1;
+    private static final int SMALL_BATCH = 4;
+    private static final int SMALL_MEDIUM_BATCH = 6;
+    private static final int MEDIUM_BATCH = 8;
+    private static final int LARGE_BATCH = 12;
+    private Logger log = LogManager.getLogger(BatchBuilder.class);
+
+    private JdbcOperations operations;
+
+    public BatchBuilder (JdbcOperations operations) {
+        this.operations = operations;
+    }
+
+    public <T> List<T> selectFromIDs (String query, Collection ids,
+            RowMapper<T> mapper) {
+        List l = new ArrayList(ids);
+        int size = ids.size();
+        List<T> values = new ArrayList<>();
+        while (size > 0) {
+            int batchSize = SINGLE_BATCH;
+            if (size >= LARGE_BATCH)
+                batchSize = LARGE_BATCH;
+            else if (size >= MEDIUM_BATCH)
+                batchSize = MEDIUM_BATCH;
+            else if (size >= SMALL_MEDIUM_BATCH)
+                batchSize = SMALL_MEDIUM_BATCH;
+            else if (size >= SMALL_BATCH)
+                batchSize = SMALL_BATCH;
+            size -= batchSize;
+            StringBuilder inClause = new StringBuilder();
+            for (int i = 0; i < batchSize; i++) {
+                inClause.append('?');
+                inClause.append(',');
+            }
+            inClause.deleteCharAt(inClause.length() - 1);
+            String sql = query + " (" + inClause.toString() + ");";
+            Object[] args = new Object[batchSize];
+            List d = new ArrayList();
+            for (int idx = 0; idx < batchSize; idx++) {
+                args[idx] = l.get(idx);
+                d.add(idx, args[idx]);
+            }
+            l.removeAll(d);
+            try {
+                values.addAll(this.operations.query(sql, args, mapper));
+            }
+            catch (DataAccessException e) {
+                log.error("Exception during database retrieval", e);
+            }
+
+        }
+        return values;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/handlers/RowMapperFactory.java b/src/main/java/de/ids_mannheim/korap/handlers/RowMapperFactory.java
new file mode 100644
index 0000000..47ad065
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/handlers/RowMapperFactory.java
@@ -0,0 +1,79 @@
+package de.ids_mannheim.korap.handlers;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Map;
+
+import org.springframework.jdbc.core.RowMapper;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.URIParam;
+import de.ids_mannheim.korap.user.KorAPUser;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * @author hanl
+ * @date 14/01/2014
+ */
+public class RowMapperFactory {
+
+    public static class UserMapMapper implements RowMapper<Map<?, ?>> {
+
+        @Override
+        public Map<?, ?> mapRow (ResultSet rs, int rowNum) throws SQLException {
+            User user = new UserMapper().mapRow(rs, rowNum);
+            return user.toMap();
+        }
+    }
+
+    public static class UserMapper implements RowMapper<User> {
+
+        @Override
+        public User mapRow (ResultSet rs, int rowNum) throws SQLException {
+            User user;
+            switch (rs.getInt("type")) {
+                case 0:
+                    user = getKorAP(rs);
+                    break;
+                //                case 1:
+                //                    user = getShib(rs);
+                //                    break;
+                default:
+                    user = User.UserFactory.getDemoUser();
+                    user.setId(rs.getInt("id"));
+                    user.setAccountCreation(
+                            rs.getTimestamp(Attributes.ACCOUNT_CREATION)
+                                    .getTime());
+                    return user;
+            }
+            return user;
+        }
+
+        private KorAPUser getKorAP (ResultSet rs) throws SQLException {
+            KorAPUser user = User.UserFactory
+                    .getUser(rs.getString(Attributes.USERNAME));
+            user.setPassword(rs.getString(Attributes.PASSWORD));
+            user.setId(rs.getInt(Attributes.ID));
+            user.setAccountLocked(rs.getBoolean(Attributes.ACCOUNTLOCK));
+            user.setAccountCreation(rs.getLong(Attributes.ACCOUNT_CREATION));
+            user.setAccountLink(rs.getString(Attributes.ACCOUNTLINK));
+            long l = rs.getLong(Attributes.URI_EXPIRATION);
+
+            URIParam param = new URIParam(rs.getString(Attributes.URI_FRAGMENT),
+                    l == 0 ? -1 : new Timestamp(l).getTime());
+            user.addField(param);
+            return user;
+        }
+
+        //        private ShibbolethUser getShib (ResultSet rs) throws SQLException {
+        //            ShibbolethUser user = User.UserFactory.getShibInstance(
+        //                    rs.getString(Attributes.USERNAME),
+        //                    rs.getString(Attributes.MAIL), rs.getString(Attributes.CN));
+        //            user.setId(rs.getInt(Attributes.ID));
+        //            return user;
+        //        }
+
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/init/Initializator.java b/src/main/java/de/ids_mannheim/korap/init/Initializator.java
new file mode 100644
index 0000000..e908737
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/init/Initializator.java
@@ -0,0 +1,68 @@
+package de.ids_mannheim.korap.init;
+
+import java.io.IOException;
+import java.util.EnumSet;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.annotation.FreeResourceParser;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.NamedVCLoader;
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.dao.AccessScopeDao;
+import de.ids_mannheim.korap.oauth2.service.OAuth2InitClientService;
+import de.ids_mannheim.korap.util.QueryException;
+
+/**
+ * Initializes values in the database from kustvakt configuration and
+ * performs named VC caching.
+ * 
+ * @author margaretha
+ *
+ */
+public class Initializator {
+
+    @Autowired
+    private AccessScopeDao accessScopeDao;
+    @Autowired
+    private NamedVCLoader vcLoader;
+    @Autowired
+    private FreeResourceParser resourceParser;
+    @Autowired
+    private FullConfiguration config;
+    @Autowired
+    private OAuth2InitClientService clientService;
+
+    public Initializator () {}
+
+    public void init () throws IOException, QueryException, KustvaktException {
+        setInitialAccessScope();
+        resourceParser.run();
+
+        if (config.createInitialSuperClient()) {
+            clientService.createInitialSuperClient(
+                    OAuth2InitClientService.OUTPUT_FILENAME);
+        }
+
+        Thread t = new Thread(vcLoader);
+        t.start();
+    }
+
+    public void initTest () throws IOException, KustvaktException {
+        setInitialAccessScope();
+        if (config.createInitialSuperClient()) {
+            clientService.createInitialTestSuperClient();
+        }
+    }
+
+    public void initResourceTest () throws IOException, KustvaktException {
+        setInitialAccessScope();
+        resourceParser.run();
+    }
+
+    private void setInitialAccessScope () {
+        EnumSet<OAuth2Scope> scopes = EnumSet.allOf(OAuth2Scope.class);
+        accessScopeDao.storeAccessScopes(scopes);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/interfaces/EncryptionIface.java b/src/main/java/de/ids_mannheim/korap/interfaces/EncryptionIface.java
new file mode 100644
index 0000000..fe0c722
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/interfaces/EncryptionIface.java
@@ -0,0 +1,65 @@
+package de.ids_mannheim.korap.interfaces;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+
+public interface EncryptionIface {
+
+    public enum Encryption {
+        @Deprecated
+        SIMPLE, ESAPICYPHER, BCRYPT
+    }
+
+    /**
+     * One-way hashing of String input. Used to canonicalize
+     * 
+     * @param input
+     * @param salt
+     * @return
+     */
+    public String secureHash (String input, String salt)
+            throws KustvaktException;
+
+    public String secureHash (String input);
+
+    /**
+     * @param plain
+     * @param hash
+     * @param salt
+     * @return
+     */
+    public boolean checkHash (String plain, String hash, String salt);
+
+    public boolean checkHash (String plain, String hash);
+
+    /**
+     * create random String to be used as authentication token
+     * 
+     * @return
+     */
+    public String createToken (boolean hash, Object ... obj);
+
+    public String createToken ();
+
+    /**
+     * create a random Integer to be used as ID for databases
+     * 
+     * @return
+     */
+    public String createRandomNumber (Object ... obj);
+
+    public String encodeBase ();
+
+    // @Deprecated
+    //public Map<String, Object> validateMap (Map<String, Object> map)
+    //        throws KustvaktException;
+
+    //@Deprecated
+    //public String validateEntry (String input, String type)
+    //        throws KustvaktException;
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/interfaces/EntityHandlerIface.java b/src/main/java/de/ids_mannheim/korap/interfaces/EntityHandlerIface.java
new file mode 100644
index 0000000..5a3bcfe
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/interfaces/EntityHandlerIface.java
@@ -0,0 +1,30 @@
+package de.ids_mannheim.korap.interfaces;
+
+import de.ids_mannheim.korap.exceptions.EmptyResultException;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * User: hanl
+ * Date: 8/19/13
+ * Time: 11:04 AM
+ */
+public interface EntityHandlerIface {
+
+    User getAccount (String username)
+            throws EmptyResultException, KustvaktException;
+
+    int updateAccount (User user) throws KustvaktException;
+
+    int createAccount (User user) throws KustvaktException;
+
+    int deleteAccount (Integer userid) throws KustvaktException;
+
+    int truncate () throws KustvaktException;
+
+    int resetPassphrase (String username, String uriToken, String passphrase)
+            throws KustvaktException;
+
+    int activateAccount (String username, String uriToken)
+            throws KustvaktException;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/interfaces/KustvaktTypeInterface.java b/src/main/java/de/ids_mannheim/korap/interfaces/KustvaktTypeInterface.java
new file mode 100644
index 0000000..2f66b95
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/interfaces/KustvaktTypeInterface.java
@@ -0,0 +1,10 @@
+package de.ids_mannheim.korap.interfaces;
+
+/**
+ * @author hanl
+ * @date 11/02/2016
+ */
+public interface KustvaktTypeInterface<T> {
+
+    Class<T> type ();
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2ClientType.java b/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2ClientType.java
new file mode 100644
index 0000000..b682883
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2ClientType.java
@@ -0,0 +1,32 @@
+package de.ids_mannheim.korap.oauth2.constant;
+
+/**
+ * Defines possible OAuth2 client types.
+ * 
+ * Quoted from RFC 6749:
+ * <ul>
+ * 
+ * <li> <b>Confidential clients</b> are clients capable of maintaining
+ * the confidentiality of their
+ * credentials (e.g., client implemented on a secure server with
+ * restricted access to the client credentials), or capable of secure
+ * client authentication using other means.
+ * </li>
+ * 
+ * <li>
+ * <b>Public clients</b> are Clients incapable of maintaining the
+ * confidentiality of their credentials (e.g., clients executing on
+ * the device used by the resource owner, such as an installed
+ * native application or a web browser-based application), and
+ * incapable of secure client authentication via any other means.
+ * Mobile and Javascript apps are considered public clients.
+ * </li>
+ * </ul>
+ * 
+ * @author margaretha
+ *
+ */
+public enum OAuth2ClientType {
+
+    CONFIDENTIAL, PUBLIC;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Error.java b/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Error.java
new file mode 100644
index 0000000..aeb1cc8
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Error.java
@@ -0,0 +1,115 @@
+package de.ids_mannheim.korap.oauth2.constant;
+
+/**
+ * Lists possible OAuth2 errors as described in RFC 6749 and 6750.
+ * 
+ * @author margaretha
+ *
+ */
+public class OAuth2Error {
+
+    public static final String ERROR = "error";
+    public static final String DESCRIPTION = "error_description";
+    public static final String URI = "error_uri";
+
+    /**
+     * The request is missing a required parameter, includes an
+     * invalid parameter value, includes a parameter more than
+     * once, or is otherwise malformed.
+     */
+    public static final String INVALID_REQUEST = "invalid_request";
+
+    /**
+     * Client authentication failed (e.g., unknown client, no
+     * client authentication included, or unsupported
+     * authentication method). The authorization server MAY
+     * return an HTTP 401 (Unauthorized) status code to indicate
+     * which HTTP authentication schemes are supported. If the
+     * client attempted to authenticate via the "Authorization"
+     * request header field, the authorization server MUST
+     * respond with an HTTP 401 (Unauthorized) status code and
+     * include the "WWW-Authenticate" response header field
+     * matching the authentication scheme used by the client.
+     */
+    public static final String INVALID_CLIENT = "invalid_client";
+
+    /**
+     * The provided authorization grant (e.g., authorization
+     * code, resource owner credentials) or refresh token is
+     * invalid, expired, revoked, does not match the redirection
+     * URI used in the authorization request, or was issued to
+     * another client.
+     * 
+     */
+    public static final String INVALID_GRANT = "invalid_grant";
+
+    /**
+     * The client is not authorized to request an authorization
+     * code using this method.
+     * 
+     */
+    public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
+
+    /**
+     * The authorization grant type is not supported by the
+     * authorization server.
+     */
+    public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
+
+    /**
+     * The requested scope is invalid, unknown, or malformed.
+     * 
+     */
+    public static final String INVALID_SCOPE = "invalid_scope";
+
+    /**
+     * The resource owner or authorization server denied the
+     * request.
+     * 
+     */
+    public static final String ACCESS_DENIED = "access_denied";
+
+    /**
+     * The authorization server does not support obtaining an
+     * authorization code using this method.
+     */
+    public static final String UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
+
+    /**
+     * The authorization server encountered an unexpected
+     * condition that prevented it from fulfilling the request.
+     * (This error code is needed because a 500 Internal Server
+     * Error HTTP status code cannot be returned to the client
+     * via an HTTP redirect.)
+     * 
+     */
+    public static final String SERVER_ERROR = "server_error";
+
+    /**
+     * The authorization server is currently unable to handle
+     * the request due to a temporary overloading or maintenance
+     * of the server. (This error code is needed because a 503
+     * Service Unavailable HTTP status code cannot be returned
+     * to the client via an HTTP redirect.)
+     */
+    public static final String TEMPORARILY_UNAVAILABLE = "temporarily_unavailable";
+
+    /**
+     * The request requires higher privileges than provided by the
+     * access token.
+     */
+    public static final String INSUFFICIENT_SCOPE = "insufficient_scope";
+
+    /**
+     * The access token provided is revoked, malformed, or
+     * invalid for other reasons.
+     */
+    public static final String INVALID_TOKEN = "invalid_token";
+
+    /**
+     * The access token provided is expired. This error is a
+     * specialization of invalid_token error and not part of
+     * the RFCs.
+     */
+    public static final String EXPIRED_TOKEN = "expired_token";
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
new file mode 100644
index 0000000..77a578d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
@@ -0,0 +1,55 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+
+/**
+ * AccessScopeDao manages database queries and transactions regarding
+ * access scopes.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+@Transactional
+public class AccessScopeDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    @SuppressWarnings("unchecked")
+    public List<AccessScope> retrieveAccessScopes () {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessScope> query = builder
+                .createQuery(AccessScope.class);
+        Root<AccessScope> root = query.from(AccessScope.class);
+        query.select(root);
+        Query q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public void storeAccessScopes (Set<OAuth2Scope> scopes) {
+        List<AccessScope> existingScopes = retrieveAccessScopes();
+        AccessScope newScope;
+        for (OAuth2Scope scope : scopes) {
+            newScope = new AccessScope(scope);
+            if (!existingScopes.contains(newScope)) {
+                entityManager.persist(newScope);
+            }
+        }
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
new file mode 100644
index 0000000..35b2b75
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
@@ -0,0 +1,245 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.KustvaktCacheable;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken_;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Manages database queries and transactions regarding access tokens.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+@Transactional
+public class AccessTokenDao extends KustvaktCacheable {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+    @Autowired
+    private FullConfiguration config;
+    @Autowired
+    private OAuth2ClientDao clientDao;
+
+    public AccessTokenDao () {
+        super("access_token", "key:access_token");
+    }
+
+    public void storeAccessToken (String token, RefreshToken refreshToken,
+            Set<AccessScope> scopes, String userId, String clientId,
+            ZonedDateTime authenticationTime) throws KustvaktException {
+        ParameterChecker.checkStringValue(token, "access_token");
+        // ParameterChecker.checkObjectValue(refreshToken, "refresh
+        // token");
+        ParameterChecker.checkObjectValue(scopes, "scopes");
+        // ParameterChecker.checkStringValue(userId, "username");
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        ParameterChecker.checkObjectValue(authenticationTime,
+                "authentication time");
+
+        ZonedDateTime now = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+
+        ZonedDateTime expiry;
+        AccessToken accessToken = new AccessToken();
+
+        if (refreshToken != null) {
+            accessToken.setRefreshToken(refreshToken);
+            expiry = now.plusSeconds(config.getAccessTokenExpiry());
+        }
+        else {
+            expiry = now.plusSeconds(config.getAccessTokenLongExpiry());
+        }
+
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+
+        accessToken.setCreatedDate(now);
+        accessToken.setExpiryDate(expiry);
+        accessToken.setToken(token);
+        accessToken.setScopes(scopes);
+        accessToken.setUserId(userId);
+        accessToken.setClient(client);
+        accessToken.setUserAuthenticationTime(authenticationTime);
+        entityManager.persist(accessToken);
+    }
+
+    public void storeAccessToken (AccessToken accessToken)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(accessToken, "access token");
+        entityManager.persist(accessToken);
+    }
+
+    public AccessToken updateAccessToken (AccessToken accessToken)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(accessToken, "access_token");
+        AccessToken cachedToken = (AccessToken) this
+                .getCacheValue(accessToken.getToken());
+        if (cachedToken != null) {
+            this.removeCacheEntry(accessToken.getToken());
+        }
+
+        accessToken = entityManager.merge(accessToken);
+        return accessToken;
+    }
+
+    public AccessToken retrieveAccessToken (String accessToken)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(accessToken, "access_token");
+        AccessToken token = (AccessToken) this.getCacheValue(accessToken);
+        if (token != null) {
+            return token;
+        }
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query = builder
+                .createQuery(AccessToken.class);
+        Root<AccessToken> root = query.from(AccessToken.class);
+        query.select(root);
+        query.where(builder.equal(root.get(AccessToken_.token), accessToken));
+        Query q = entityManager.createQuery(query);
+        try {
+            token = (AccessToken) q.getSingleResult();
+            this.storeInCache(accessToken, token);
+            return token;
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+                    "Access token is invalid", OAuth2Error.INVALID_TOKEN);
+        }
+    }
+
+    public AccessToken retrieveAccessToken (String accessToken, String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(accessToken, "access_token");
+        ParameterChecker.checkStringValue(username, "username");
+        AccessToken token = (AccessToken) this.getCacheValue(accessToken);
+        if (token != null) {
+            return token;
+        }
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query = builder
+                .createQuery(AccessToken.class);
+        Root<AccessToken> root = query.from(AccessToken.class);
+
+        Predicate condition = builder.and(
+                builder.equal(root.get(AccessToken_.userId), username),
+                builder.equal(root.get(AccessToken_.token), accessToken));
+
+        query.select(root);
+        query.where(condition);
+        Query q = entityManager.createQuery(query);
+        try {
+            token = (AccessToken) q.getSingleResult();
+            this.storeInCache(accessToken, token);
+            return token;
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+
+    public List<AccessToken> retrieveAccessTokenByClientId (String clientId,
+            String username) throws KustvaktException {
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query = builder
+                .createQuery(AccessToken.class);
+        Root<AccessToken> root = query.from(AccessToken.class);
+
+        Predicate condition = builder.equal(root.get(AccessToken_.client),
+                client);
+        if (username != null && !username.isEmpty()) {
+            condition = builder.and(condition,
+                    builder.equal(root.get(AccessToken_.userId), username));
+        }
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<AccessToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<AccessToken> retrieveAccessTokenByUser (String username,
+            String clientId) throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query = builder
+                .createQuery(AccessToken.class);
+
+        Root<AccessToken> root = query.from(AccessToken.class);
+        root.fetch(AccessToken_.client);
+        Predicate condition = builder.and(
+                builder.equal(root.get(AccessToken_.userId), username),
+                builder.equal(root.get(AccessToken_.isRevoked), false),
+                builder.greaterThan(
+                        root.<ZonedDateTime> get(AccessToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+        if (clientId != null && !clientId.isEmpty()) {
+            OAuth2Client client = clientDao.retrieveClientById(clientId);
+            condition = builder.and(condition,
+                    builder.equal(root.get(AccessToken_.client), client));
+        }
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<AccessToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public void deleteInvalidAccessTokens () {
+        List<AccessToken> invalidAccessTokens = retrieveInvalidAccessTokens();
+        invalidAccessTokens.forEach(token -> entityManager.remove(token));
+    }
+
+    public List<AccessToken> retrieveInvalidAccessTokens () {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query = builder
+                .createQuery(AccessToken.class);
+
+        Root<AccessToken> root = query.from(AccessToken.class);
+        Predicate condition = builder.or(
+                builder.equal(root.get(AccessToken_.isRevoked), true),
+                builder.lessThan(
+                        root.<ZonedDateTime> get(AccessToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<AccessToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
new file mode 100644
index 0000000..b145f44
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
@@ -0,0 +1,33 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Set;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.Authorization;
+
+/**
+ * AuthorizationDao is an interface describing methods for managing
+ * authorizations.
+ * 
+ * @author margaretha
+ *
+ */
+public interface AuthorizationDao {
+
+    public Authorization storeAuthorizationCode (String clientId, String userId,
+            String code, Set<AccessScope> scopes, String redirectURI,
+            ZonedDateTime authenticationTime, String nonce)
+            throws KustvaktException;
+
+    public Authorization retrieveAuthorizationCode (String code)
+            throws KustvaktException;
+
+    public Authorization updateAuthorization (Authorization authorization)
+            throws KustvaktException;
+
+    public List<Authorization> retrieveAuthorizationsByClientId (
+            String clientId);
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDaoImpl.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDaoImpl.java
new file mode 100644
index 0000000..ed7f218
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDaoImpl.java
@@ -0,0 +1,126 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.oauth2.entity.Authorization_;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Implementations of {@link AuthorizationDao} managing database
+ * queries and transactions regarding OAuth2 authorizations.
+ * 
+ * @author margaretha
+ *
+ */
+@Transactional
+@Repository
+public class AuthorizationDaoImpl implements AuthorizationDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+    @Autowired
+    private FullConfiguration config;
+
+    public Authorization storeAuthorizationCode (String clientId, String userId,
+            String code, Set<AccessScope> scopes, String redirectURI,
+            ZonedDateTime authenticationTime, String nonce)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        ParameterChecker.checkStringValue(userId, "user_id");
+        ParameterChecker.checkStringValue(code, "authorization_code");
+        ParameterChecker.checkCollection(scopes, "scopes");
+        ParameterChecker.checkObjectValue(authenticationTime,
+                "user_authentication_time");
+
+        Authorization authorization = new Authorization();
+        authorization.setCode(code);
+        authorization.setClientId(clientId);
+        authorization.setUserId(userId);
+        authorization.setScopes(scopes);
+        authorization.setRedirectURI(redirectURI);
+        authorization.setUserAuthenticationTime(authenticationTime);
+        authorization.setNonce(nonce);
+
+        ZonedDateTime now = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+        authorization.setCreatedDate(now);
+        authorization.setExpiryDate(
+                now.plusSeconds(config.getAuthorizationCodeExpiry()));
+
+        entityManager.persist(authorization);
+        // what if unique fails
+        return authorization;
+    }
+
+    public Authorization retrieveAuthorizationCode (String code)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(code, "code");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Authorization> query = builder
+                .createQuery(Authorization.class);
+        Root<Authorization> root = query.from(Authorization.class);
+
+        Predicate restrictions = builder.equal(root.get(Authorization_.code),
+                code);
+
+        query.select(root);
+        query.where(restrictions);
+        Query q = entityManager.createQuery(query);
+        try {
+            return (Authorization) q.getSingleResult();
+        }
+        catch (Exception e) {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Invalid authorization: " + e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST);
+        }
+    }
+
+    public Authorization updateAuthorization (Authorization authorization)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(authorization, "authorization");
+        authorization = entityManager.merge(authorization);
+        return authorization;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public List<Authorization> retrieveAuthorizationsByClientId (
+            String clientId) {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Authorization> query = builder
+                .createQuery(Authorization.class);
+        Root<Authorization> root = query.from(Authorization.class);
+
+        Predicate restrictions = builder
+                .equal(root.get(Authorization_.clientId), clientId);
+
+        query.select(root);
+        query.where(restrictions);
+        Query q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/CachedAuthorizationDaoImpl.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/CachedAuthorizationDaoImpl.java
new file mode 100644
index 0000000..20453fd
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/CachedAuthorizationDaoImpl.java
@@ -0,0 +1,110 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.KustvaktCacheable;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import net.sf.ehcache.Element;
+
+/**
+ * Implementations of {@link AuthorizationDao} using a cache instead
+ * of a database.
+ * 
+ * @author margaretha
+ *
+ */
+public class CachedAuthorizationDaoImpl extends KustvaktCacheable
+        implements AuthorizationDao {
+
+    @Autowired
+    private FullConfiguration config;
+
+    public CachedAuthorizationDaoImpl () {
+        super("authorization", "key:authorization");
+    }
+
+    @Override
+    public Authorization storeAuthorizationCode (String clientId, String userId,
+            String code, Set<AccessScope> scopes, String redirectURI,
+            ZonedDateTime authenticationTime, String nonce)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        ParameterChecker.checkStringValue(userId, "user_id");
+        ParameterChecker.checkStringValue(code, "authorization_code");
+        ParameterChecker.checkCollection(scopes, "scopes");
+        ParameterChecker.checkObjectValue(authenticationTime,
+                "user_authentication_time");
+
+        Authorization authorization = new Authorization();
+        authorization.setCode(code);
+        authorization.setClientId(clientId);
+        authorization.setUserId(userId);
+        authorization.setScopes(scopes);
+        authorization.setRedirectURI(redirectURI);
+        authorization.setUserAuthenticationTime(authenticationTime);
+        authorization.setNonce(nonce);
+
+        ZonedDateTime now = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+        authorization.setCreatedDate(now);
+        authorization.setExpiryDate(
+                now.plusSeconds(config.getAuthorizationCodeExpiry()));
+
+        this.storeInCache(code, authorization);
+        return authorization;
+    }
+
+    @Override
+    public Authorization retrieveAuthorizationCode (String code)
+            throws KustvaktException {
+
+        Object auth = this.getCacheValue(code);
+        if (auth != null) {
+            return (Authorization) auth;
+        }
+        else {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Authorization is invalid.", OAuth2Error.INVALID_REQUEST);
+        }
+    }
+
+    @Override
+    public Authorization updateAuthorization (Authorization authorization)
+            throws KustvaktException {
+
+        this.storeInCache(authorization.getCode(), authorization);
+        Authorization auth = (Authorization) this
+                .getCacheValue(authorization.getCode());
+        return auth;
+    }
+
+    @Override
+    public List<Authorization> retrieveAuthorizationsByClientId (
+            String clientId) {
+        List<Authorization> authList = new ArrayList<>();
+
+        Map<Object, Element> map = getAllCacheElements();
+        for (Object key : map.keySet()) {
+            Authorization auth = (Authorization) map.get(key).getObjectValue();
+            if (auth.getClientId().equals(clientId)) {
+                authList.add(auth);
+            }
+        }
+        return authList;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/InstalledPluginDao.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/InstalledPluginDao.java
new file mode 100644
index 0000000..46bc06b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/InstalledPluginDao.java
@@ -0,0 +1,112 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.entity.InstalledPlugin;
+import de.ids_mannheim.korap.entity.InstalledPlugin_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+@Repository
+@Transactional
+public class InstalledPluginDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public InstalledPlugin storeUserPlugin (OAuth2Client superClient,
+            OAuth2Client client, String installedBy) throws KustvaktException {
+        ParameterChecker.checkStringValue(installedBy, "installed_by");
+
+        InstalledPlugin p = new InstalledPlugin();
+        p.setInstalledBy(installedBy);
+        p.setInstalledDate(
+                ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE)));
+        p.setClient(client);
+        p.setSuperClient(superClient);
+        entityManager.persist(p);
+        return p;
+    }
+
+    public InstalledPlugin retrieveInstalledPlugin (String superClientId,
+            String clientId, String installedBy) throws KustvaktException {
+        ParameterChecker.checkStringValue(superClientId, "super_client_id");
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        ParameterChecker.checkStringValue(installedBy, "installedBy");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<InstalledPlugin> query = builder
+                .createQuery(InstalledPlugin.class);
+
+        Root<InstalledPlugin> root = query.from(InstalledPlugin.class);
+        query.select(root);
+        query.where(builder.and(
+                builder.equal(root.get(InstalledPlugin_.INSTALLED_BY),
+                        installedBy),
+                builder.equal(
+                        root.get(InstalledPlugin_.client).get(OAuth2Client_.id),
+                        clientId),
+                builder.equal(root.get(InstalledPlugin_.superClient)
+                        .get(OAuth2Client_.id), superClientId)));
+
+        Query q = entityManager.createQuery(query);
+        try {
+            return (InstalledPlugin) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND);
+        }
+    }
+
+    public List<InstalledPlugin> retrieveInstalledPlugins (String superClientId,
+            String installedBy) throws KustvaktException {
+        ParameterChecker.checkStringValue(superClientId, "super_client_id");
+        ParameterChecker.checkStringValue(installedBy, "installedBy");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<InstalledPlugin> query = builder
+                .createQuery(InstalledPlugin.class);
+
+        Root<InstalledPlugin> root = query.from(InstalledPlugin.class);
+        query.select(root);
+        query.where(builder.and(
+                builder.equal(root.get(InstalledPlugin_.INSTALLED_BY),
+                        installedBy),
+                builder.equal(root.get(InstalledPlugin_.superClient)
+                        .get(OAuth2Client_.id), superClientId)));
+
+        TypedQuery<InstalledPlugin> q = entityManager.createQuery(query);
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND);
+        }
+    }
+
+    public void uninstallPlugin (String superClientId, String clientId,
+            String username) throws KustvaktException {
+        InstalledPlugin plugin = retrieveInstalledPlugin(superClientId,
+                clientId, username);
+        entityManager.remove(plugin);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
new file mode 100644
index 0000000..0e118d5
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
@@ -0,0 +1,238 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken_;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Manages database queries and transactions regarding OAuth2 clients.
+ * 
+ * @author margaretha
+ *
+ */
+@Transactional
+@Repository
+public class OAuth2ClientDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+    @Autowired
+    private FullConfiguration config;
+
+    public void registerClient (String id, String secretHashcode, String name,
+            OAuth2ClientType type, String url, String redirectURI,
+            String registeredBy, String description, int refreshTokenExpiry,
+            JsonNode source) throws KustvaktException {
+        ParameterChecker.checkStringValue(id, "client_id");
+        ParameterChecker.checkStringValue(name, "client_name");
+        ParameterChecker.checkObjectValue(type, "client_type");
+        ParameterChecker.checkStringValue(description, "client_description");
+        // ParameterChecker.checkStringValue(url, "client url");
+        // ParameterChecker.checkStringValue(redirectURI, "client
+        // redirect uri");
+        ParameterChecker.checkStringValue(registeredBy, "registered_by");
+
+        OAuth2Client client = new OAuth2Client();
+        client.setId(id);
+        client.setName(name);
+        client.setSecret(secretHashcode);
+        client.setType(type);
+        client.setUrl(url);
+        client.setRedirectURI(redirectURI);
+        client.setRegisteredBy(registeredBy);
+        client.setRegistrationDate(ZonedDateTime.now());
+        client.setDescription(description);
+        if (source != null && !source.isNull()) {
+            if (type.equals(OAuth2ClientType.CONFIDENTIAL)) {
+                client.setSource(source.toString());
+            }
+            else {
+                throw new KustvaktException(StatusCodes.NOT_SUPPORTED,
+                        "Only confidential plugins are supported.");
+            }
+        }
+        else {
+            client.setPermitted(true);
+        }
+        if (refreshTokenExpiry <= 0) {
+            if (type.equals(OAuth2ClientType.CONFIDENTIAL)) {
+                refreshTokenExpiry = config.getRefreshTokenLongExpiry();
+            }
+        }
+        else if (type.equals(OAuth2ClientType.PUBLIC)) {
+            throw new KustvaktException(
+                    StatusCodes.INVALID_REFRESH_TOKEN_EXPIRY,
+                    "Custom refresh token expiry is only applicable for confidential clients");
+        }
+        else if (refreshTokenExpiry > 31536000) {
+            throw new KustvaktException(
+                    StatusCodes.INVALID_REFRESH_TOKEN_EXPIRY,
+                    "Maximum refresh token expiry is 31536000 seconds (1 year)");
+        }
+
+        client.setRefreshTokenExpiry(refreshTokenExpiry);
+        entityManager.persist(client);
+    }
+
+    public OAuth2Client retrieveClientById (String clientId)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(clientId, "client_id");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query = builder
+                .createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> root = query.from(OAuth2Client.class);
+        query.select(root);
+        query.where(builder.equal(root.get(OAuth2Client_.id), clientId));
+
+        Query q = entityManager.createQuery(query);
+        try {
+            return (OAuth2Client) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.CLIENT_NOT_FOUND,
+                    "Unknown client: " + clientId, "invalid_client");
+        }
+        catch (Exception e) {
+            throw new KustvaktException(StatusCodes.CLIENT_NOT_FOUND,
+                    e.getMessage(), "invalid_client");
+        }
+    }
+
+    public void deregisterClient (OAuth2Client client)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(client, "client");
+        if (!entityManager.contains(client)) {
+            client = entityManager.merge(client);
+        }
+        entityManager.remove(client);
+    }
+
+    public void updateClient (OAuth2Client client) throws KustvaktException {
+        ParameterChecker.checkObjectValue(client, "client");
+        client = entityManager.merge(client);
+    }
+
+    public List<OAuth2Client> retrieveUserAuthorizedClients (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query = builder
+                .createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        Join<OAuth2Client, RefreshToken> refreshToken = client
+                .join(OAuth2Client_.refreshTokens);
+        Predicate condition = builder.and(
+                builder.equal(refreshToken.get(RefreshToken_.userId), username),
+                builder.equal(refreshToken.get(RefreshToken_.isRevoked), false),
+                builder.greaterThan(
+                        refreshToken
+                                .<ZonedDateTime> get(RefreshToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+        query.select(client);
+        query.where(condition);
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<OAuth2Client> retrieveClientsByAccessTokens (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query = builder
+                .createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        Join<OAuth2Client, AccessToken> accessToken = client
+                .join(OAuth2Client_.accessTokens);
+        Predicate condition = builder.and(
+                builder.equal(accessToken.get(AccessToken_.userId), username),
+                builder.equal(accessToken.get(AccessToken_.isRevoked), false),
+                builder.greaterThan(
+                        accessToken
+                                .<ZonedDateTime> get(AccessToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+        query.select(client);
+        query.where(condition);
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<OAuth2Client> retrieveUserRegisteredClients (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query = builder
+                .createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        query.select(client);
+        query.where(builder.equal(client.get(OAuth2Client_.registeredBy),
+                username));
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<OAuth2Client> retrievePlugins (boolean isPermittedOnly)
+            throws KustvaktException {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query = builder
+                .createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        Predicate restrictions = builder
+                .isNotNull(client.get(OAuth2Client_.SOURCE));
+        if (isPermittedOnly) {
+            restrictions = builder.and(restrictions,
+                    builder.isTrue(client.get(OAuth2Client_.IS_PERMITTED)));
+        }
+
+        query.select(client);
+        query.where(restrictions);
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java b/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
new file mode 100644
index 0000000..19da4ff
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
@@ -0,0 +1,195 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+/**
+ * Manages database queries and transactions regarding refresh tokens.
+ * 
+ * @author margaretha
+ *
+ */
+@Repository
+@Transactional
+public class RefreshTokenDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+    @Autowired
+    private OAuth2ClientDao clientDao;
+
+    public RefreshToken storeRefreshToken (String refreshToken, String userId,
+            ZonedDateTime userAuthenticationTime, OAuth2Client client,
+            Set<AccessScope> scopes) throws KustvaktException {
+        ParameterChecker.checkStringValue(refreshToken, "refresh_token");
+        // ParameterChecker.checkStringValue(userId, "username");
+        ParameterChecker.checkObjectValue(client, "client");
+        ParameterChecker.checkObjectValue(scopes, "scopes");
+
+        ZonedDateTime now = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+
+        RefreshToken token = new RefreshToken();
+        token.setToken(refreshToken);
+        token.setUserId(userId);
+        token.setUserAuthenticationTime(userAuthenticationTime);
+        token.setClient(client);
+        token.setCreatedDate(now);
+        token.setExpiryDate(now.plusSeconds(client.getRefreshTokenExpiry()));
+        token.setScopes(scopes);
+
+        entityManager.persist(token);
+        return token;
+    }
+
+    public RefreshToken updateRefreshToken (RefreshToken token)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(token, "refresh_token");
+
+        token = entityManager.merge(token);
+        return token;
+    }
+
+    public RefreshToken retrieveRefreshToken (String token)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(token, "refresh_token");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query = builder
+                .createQuery(RefreshToken.class);
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+        root.fetch(RefreshToken_.client);
+
+        query.select(root);
+        query.where(builder.equal(root.get(RefreshToken_.token), token));
+        Query q = entityManager.createQuery(query);
+        return (RefreshToken) q.getSingleResult();
+    }
+
+    public RefreshToken retrieveRefreshToken (String token, String username)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(token, "refresh_token");
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query = builder
+                .createQuery(RefreshToken.class);
+
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+        Predicate condition = builder.and(
+                builder.equal(root.get(RefreshToken_.userId), username),
+                builder.equal(root.get(RefreshToken_.token), token));
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
+        try {
+            return q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+
+    public List<RefreshToken> retrieveRefreshTokenByClientId (String clientId,
+            String username) throws KustvaktException {
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query = builder
+                .createQuery(RefreshToken.class);
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+
+        Predicate condition = builder.equal(root.get(RefreshToken_.client),
+                client);
+        if (username != null && !username.isEmpty()) {
+            condition = builder.and(condition,
+                    builder.equal(root.get(RefreshToken_.userId), username));
+        }
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<RefreshToken> retrieveRefreshTokenByUser (String username,
+            String clientId) throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query = builder
+                .createQuery(RefreshToken.class);
+
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+        root.fetch(RefreshToken_.client);
+        Predicate condition = builder.and(
+                builder.equal(root.get(RefreshToken_.userId), username),
+                builder.equal(root.get(RefreshToken_.isRevoked), false),
+                builder.greaterThan(
+                        root.<ZonedDateTime> get(RefreshToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+        if (clientId != null && !clientId.isEmpty()) {
+            OAuth2Client client = clientDao.retrieveClientById(clientId);
+            condition = builder.and(condition,
+                    builder.equal(root.get(RefreshToken_.client), client));
+        }
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public void deleteInvalidRefreshTokens () {
+        List<RefreshToken> invalidRefreshTokens = retrieveInvalidRefreshTokens();
+        invalidRefreshTokens.forEach(token -> entityManager.remove(token));
+    }
+
+    public List<RefreshToken> retrieveInvalidRefreshTokens () {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query = builder
+                .createQuery(RefreshToken.class);
+
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+        Predicate condition = builder.or(
+                builder.equal(root.get(RefreshToken_.isRevoked), true),
+                builder.lessThan(
+                        root.<ZonedDateTime> get(RefreshToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientDto.java b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientDto.java
new file mode 100644
index 0000000..fccffb8
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientDto.java
@@ -0,0 +1,41 @@
+package de.ids_mannheim.korap.oauth2.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+/**
+ * Describes the client_id and the client_secret of a client after
+ * client registration or reset secret.
+ * 
+ * @author margaretha
+ *
+ */
+@JsonInclude(Include.NON_EMPTY)
+public class OAuth2ClientDto {
+
+    private String client_id;
+    private String client_secret;
+
+    public OAuth2ClientDto () {}
+
+    public OAuth2ClientDto (String id, String secret) {
+        this.setClient_id(id);
+        this.setClient_secret(secret);
+    }
+
+    public String getClient_id () {
+        return client_id;
+    }
+
+    public void setClient_id (String client_id) {
+        this.client_id = client_id;
+    }
+
+    public String getClient_secret () {
+        return client_secret;
+    }
+
+    public void setClient_secret (String client_secret) {
+        this.client_secret = client_secret;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
new file mode 100644
index 0000000..d50d79b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
@@ -0,0 +1,169 @@
+package de.ids_mannheim.korap.oauth2.dto;
+
+import java.time.ZonedDateTime;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * Describes information about an OAuth2 client.
+ * 
+ * @author margaretha
+ *
+ */
+@JsonInclude(Include.NON_EMPTY)
+public class OAuth2ClientInfoDto {
+    @JsonProperty("super")
+    private boolean isSuper;
+
+    @JsonProperty("client_id")
+    private String clientId;
+    @JsonProperty("client_name")
+    private String clientName;
+    @JsonProperty("client_type")
+    private OAuth2ClientType clientType;
+    @JsonProperty("client_description")
+    private String description;
+    @JsonProperty("client_url")
+    private String url;
+    @JsonProperty("client_redirect_uri")
+    private String redirect_uri;
+    @JsonProperty("registration_date")
+    private String registrationDate;
+    @JsonProperty("registered_by")
+    private String registeredBy;
+    @JsonProperty("refresh_token_expiry")
+    private int refreshTokenExpiry; // in seconds
+
+    @JsonProperty("permitted")
+    private boolean isPermitted;
+    private JsonNode source;
+
+    public OAuth2ClientInfoDto (OAuth2Client client) throws KustvaktException {
+        this.setClientId(client.getId());
+        this.setClientName(client.getName());
+        this.setDescription(client.getDescription());
+        this.setClientType(client.getType());
+        this.setUrl(client.getUrl());
+        this.setClientType(client.getType());
+        this.setRedirect_uri(client.getRedirectURI());
+        this.setSuper(client.isSuper());
+        this.setPermitted(client.isPermitted());
+        this.setRegisteredBy(client.getRegisteredBy());
+
+        String source = client.getSource();
+        if (source != null && !source.isEmpty()) {
+            this.source = JsonUtils.readTree(source);
+        }
+        if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+            this.setRefreshTokenExpiry(client.getRefreshTokenExpiry());
+        }
+        ZonedDateTime registrationDate = client.getRegistrationDate();
+        if (registrationDate != null) {
+            this.setRegistrationDate(registrationDate.toString());
+        }
+    }
+
+    public boolean isSuper () {
+        return isSuper;
+    }
+
+    public void setSuper (boolean isSuper) {
+        this.isSuper = isSuper;
+    }
+
+    public String getRegisteredBy () {
+        return registeredBy;
+    }
+
+    public void setRegisteredBy (String registeredBy) {
+        this.registeredBy = registeredBy;
+    }
+
+    public String getClientId () {
+        return clientId;
+    }
+
+    public void setClientId (String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientName () {
+        return clientName;
+    }
+
+    public void setClientName (String clientName) {
+        this.clientName = clientName;
+    }
+
+    public OAuth2ClientType getClientType () {
+        return clientType;
+    }
+
+    public void setClientType (OAuth2ClientType clientType) {
+        this.clientType = clientType;
+    }
+
+    public String getDescription () {
+        return description;
+    }
+
+    public void setDescription (String description) {
+        this.description = description;
+    }
+
+    public String getUrl () {
+        return url;
+    }
+
+    public void setUrl (String url) {
+        this.url = url;
+    }
+
+    public String getRedirect_uri () {
+        return redirect_uri;
+    }
+
+    public void setRedirect_uri (String redirect_uri) {
+        this.redirect_uri = redirect_uri;
+    }
+
+    public String getRegistrationDate () {
+        return registrationDate;
+    }
+
+    public void setRegistrationDate (String registrationDate) {
+        this.registrationDate = registrationDate;
+    }
+
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
+
+    public boolean isPermitted () {
+        return isPermitted;
+    }
+
+    public void setPermitted (boolean isPermitted) {
+        this.isPermitted = isPermitted;
+    }
+
+    public JsonNode getSource () {
+        return source;
+    }
+
+    public void setSource (JsonNode source) {
+        this.source = source;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2TokenDto.java b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2TokenDto.java
new file mode 100644
index 0000000..684acd5
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2TokenDto.java
@@ -0,0 +1,105 @@
+package de.ids_mannheim.korap.oauth2.dto;
+
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Describes OAuth2 refresh tokens
+ * 
+ * @author margaretha
+ *
+ */
+public class OAuth2TokenDto {
+
+    private String token;
+    @JsonProperty("created_date")
+    private String createdDate;
+    @JsonProperty("expires_in")
+    private long expiresIn;
+    @JsonProperty("user_authentication_time")
+    private String userAuthenticationTime;
+    private Set<String> scope;
+
+    @JsonProperty("client_id")
+    private String clientId;
+    @JsonProperty("client_name")
+    private String clientName;
+    @JsonProperty("client_description")
+    private String clientDescription;
+    @JsonProperty("client_url")
+    private String clientUrl;
+
+    public String getToken () {
+        return token;
+    }
+
+    public void setToken (String token) {
+        this.token = token;
+    }
+
+    public String getClientId () {
+        return clientId;
+    }
+
+    public void setClientId (String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientName () {
+        return clientName;
+    }
+
+    public void setClientName (String clientName) {
+        this.clientName = clientName;
+    }
+
+    public String getClientDescription () {
+        return clientDescription;
+    }
+
+    public void setClientDescription (String clientDescription) {
+        this.clientDescription = clientDescription;
+    }
+
+    public String getClientUrl () {
+        return clientUrl;
+    }
+
+    public void setClientUrl (String clientUrl) {
+        this.clientUrl = clientUrl;
+    }
+
+    public String getCreatedDate () {
+        return createdDate;
+    }
+
+    public void setCreatedDate (String createdDate) {
+        this.createdDate = createdDate;
+    }
+
+    public long getExpiresIn () {
+        return expiresIn;
+    }
+
+    public void setExpiresIn (long expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+
+    public String getUserAuthenticationTime () {
+        return userAuthenticationTime;
+    }
+
+    public void setUserAuthenticationTime (String userAuthenticationTime) {
+        this.userAuthenticationTime = userAuthenticationTime;
+    }
+
+    public Set<String> getScope () {
+        return scope;
+    }
+
+    public void setScope (Set<String> scope) {
+        this.scope = scope;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
new file mode 100644
index 0000000..f499fe0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
@@ -0,0 +1,143 @@
+package de.ids_mannheim.korap.oauth2.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * Please use {@link OAuth2ClientInfoDto} instead.
+ * 
+ * This class is not used anymore to describe OAuth2 clients of a
+ * user.
+ * 
+ * @author margaretha
+ *
+ */
+@Deprecated
+@JsonInclude(Include.NON_DEFAULT)
+public class OAuth2UserClientDto {
+    @JsonProperty("client_id")
+    private String clientId;
+    @JsonProperty("client_name")
+    private String clientName;
+    @JsonProperty("client_type")
+    private OAuth2ClientType clientType;
+    @JsonProperty("client_description")
+    private String description;
+    @JsonProperty("client_url")
+    private String url;
+    @JsonProperty("client_redirect_uri")
+    private String redirect_uri;
+    @JsonProperty("registration_date")
+    private String registrationDate;
+    @JsonProperty("refresh_token_expiry")
+    private int refreshTokenExpiry;
+
+    private boolean permitted;
+    private JsonNode source;
+
+    public OAuth2UserClientDto (OAuth2Client client) throws KustvaktException {
+        this.setClientId(client.getId());
+        this.setClientName(client.getName());
+        this.setDescription(client.getDescription());
+        this.setClientType(client.getType());
+        this.setUrl(client.getUrl());
+        this.setClientType(client.getType());
+        this.setRedirect_uri(client.getRedirectURI());
+        this.setRegistrationDate(client.getRegistrationDate().toString());
+        this.setPermitted(client.isPermitted());
+        if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+            this.setRefreshTokenExpiry(client.getRefreshTokenExpiry());
+        }
+        String source = client.getSource();
+        if (source != null) {
+            this.setSource(JsonUtils.readTree(client.getSource()));
+        }
+    }
+
+    public String getClientName () {
+        return clientName;
+    }
+
+    public void setClientName (String clientName) {
+        this.clientName = clientName;
+    }
+
+    public String getClientId () {
+        return clientId;
+    }
+
+    public void setClientId (String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getDescription () {
+        return description;
+    }
+
+    public void setDescription (String description) {
+        this.description = description;
+    }
+
+    public String getUrl () {
+        return url;
+    }
+
+    public void setUrl (String url) {
+        this.url = url;
+    }
+
+    public OAuth2ClientType getClientType () {
+        return clientType;
+    }
+
+    public void setClientType (OAuth2ClientType clientType) {
+        this.clientType = clientType;
+    }
+
+    public String getRedirect_uri () {
+        return redirect_uri;
+    }
+
+    public void setRedirect_uri (String redirect_uri) {
+        this.redirect_uri = redirect_uri;
+    }
+
+    public boolean isPermitted () {
+        return permitted;
+    }
+
+    public void setPermitted (boolean permitted) {
+        this.permitted = permitted;
+    }
+
+    public JsonNode getSource () {
+        return source;
+    }
+
+    public void setSource (JsonNode source) {
+        this.source = source;
+    }
+
+    public String getRegistrationDate () {
+        return registrationDate;
+    }
+
+    public void setRegistrationDate (String registrationDate) {
+        this.registrationDate = registrationDate;
+    }
+
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java b/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
new file mode 100644
index 0000000..85f0fcf
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
@@ -0,0 +1,71 @@
+package de.ids_mannheim.korap.oauth2.entity;
+
+import java.io.Serializable;
+import java.util.List;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines the oauth2_access_scope database table mapping and access
+ * scope relations to Authorization, AccessToken and RefreshToken.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "oauth2_access_scope")
+public class AccessScope implements Serializable {
+
+    private static final long serialVersionUID = -7356877266702636705L;
+
+    @Id
+    @Enumerated(EnumType.STRING)
+    private OAuth2Scope id;
+
+    public AccessScope () {}
+
+    public AccessScope (OAuth2Scope scope) {
+        this.id = scope;
+    }
+
+    @ManyToMany(mappedBy = "scopes", fetch = FetchType.LAZY)
+    private List<Authorization> authorizations;
+
+    @ManyToMany(mappedBy = "scopes", fetch = FetchType.LAZY)
+    private List<AccessToken> accessTokens;
+
+    @ManyToMany(mappedBy = "scopes", fetch = FetchType.LAZY)
+    private List<RefreshToken> refreshTokens;
+
+    @Override
+    public String toString () {
+        return id.toString();
+    }
+
+    @Override
+    public boolean equals (Object obj) {
+        AccessScope scope = (AccessScope) obj;
+        if (scope.getId().equals(this.id)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode () {
+        return this.getId().hashCode();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java b/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
new file mode 100644
index 0000000..9e0fd8d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
@@ -0,0 +1,71 @@
+package de.ids_mannheim.korap.oauth2.entity;
+
+import java.io.Serializable;
+import java.time.ZonedDateTime;
+import java.util.Set;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines oauth2_access_token database table mapping and access token
+ * relations to AccessScope and RefreshToken.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "oauth2_access_token")
+public class AccessToken implements Serializable {
+
+    private static final long serialVersionUID = 8452701765986475302L;
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String token;
+    @Column(name = "created_date", updatable = false)
+    private ZonedDateTime createdDate;
+    @Column(name = "expiry_date", updatable = false)
+    private ZonedDateTime expiryDate;
+    @Column(name = "user_id")
+    private String userId;
+    //    @Column(name = "client_id")
+    //    private String clientId;
+    @Column(name = "is_revoked")
+    private boolean isRevoked;
+    @Column(name = "user_auth_time", updatable = false)
+    private ZonedDateTime userAuthenticationTime;
+
+    // @OneToOne(fetch=FetchType.LAZY, cascade=CascadeType.REMOVE)
+    // @JoinColumn(name="authorization_id")
+    // private Authorization authorization;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = "oauth2_access_token_scope", joinColumns = @JoinColumn(name = "token_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "scope_id", referencedColumnName = "id"), uniqueConstraints = @UniqueConstraint(columnNames = {
+            "token_id", "scope_id" }))
+    private Set<AccessScope> scopes;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "refresh_token")
+    private RefreshToken refreshToken;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "client")
+    private OAuth2Client client;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java b/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java
new file mode 100644
index 0000000..acaf081
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java
@@ -0,0 +1,68 @@
+package de.ids_mannheim.korap.oauth2.entity;
+
+import java.time.ZonedDateTime;
+import java.util.Set;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Describes oauth2_authorization database table mapping and
+ * authorization relations to AccessScope.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "oauth2_authorization")
+public class Authorization {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String code;
+    @Column(name = "client_id")
+    private String clientId;
+    @Column(name = "user_id")
+    private String userId;
+    @Column(name = "redirect_uri")
+    private String redirectURI;
+    @Column(name = "created_date", updatable = false)
+    private ZonedDateTime createdDate;
+    @Column(name = "expiry_date")
+    private ZonedDateTime expiryDate;
+    @Column(name = "is_revoked")
+    private boolean isRevoked;
+    @Column(name = "total_attempts")
+    private int totalAttempts;
+    @Column(name = "user_auth_time", updatable = false)
+    private ZonedDateTime userAuthenticationTime;
+    @Column(updatable = false)
+    private String nonce;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = "oauth2_authorization_scope", joinColumns = @JoinColumn(name = "authorization_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "scope_id", referencedColumnName = "id"), uniqueConstraints = @UniqueConstraint(columnNames = {
+            "authorization_id", "scope_id" }))
+    private Set<AccessScope> scopes;
+
+    @Override
+    public String toString () {
+        return "code: " + code + ", " + "clientId: " + clientId + ", "
+                + "userId: " + userId + ", " + "createdDate: " + createdDate
+                + ", " + "isRevoked: " + isRevoked + ", " + "scopes: " + scopes
+                + ", " + "totalAttempts: " + totalAttempts;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java b/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
new file mode 100644
index 0000000..5f29815
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
@@ -0,0 +1,181 @@
+package de.ids_mannheim.korap.oauth2.entity;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+
+import de.ids_mannheim.korap.entity.InstalledPlugin;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+
+/**
+ * Describe oauth2_client database table mapping.
+ * 
+ * @author margaretha
+ *
+ */
+@Entity
+@Table(name = "oauth2_client")
+public class OAuth2Client implements Comparable<OAuth2Client> {
+
+    @Id
+    private String id;
+    private String name;
+    // Secret hashcode is stored instead of plain secret
+    private String secret;
+    @Enumerated(EnumType.STRING)
+    private OAuth2ClientType type;
+    @Column(name = "super")
+    private boolean isSuper;
+    @Column(name = "redirect_uri")
+    private String redirectURI;
+    @Column(name = "registered_by", updatable = false)
+    private String registeredBy;
+    @Column(name = "registration_date", updatable = false)
+    private ZonedDateTime registrationDate;
+
+    // How long a refresh token for this client should be valid
+    // in seconds. Maximum 31536000 seconds equivalent to 1 year     
+    @Column(name = "refresh_token_expiry")
+    private int refreshTokenExpiry;
+
+    private String description;
+    private String url;
+
+    private String source;
+    @Column(name = "is_permitted")
+    private boolean isPermitted;
+
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
+    private List<RefreshToken> refreshTokens;
+
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
+    private List<AccessToken> accessTokens;
+
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
+    private List<InstalledPlugin> installedPlugins;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", name=" + name + ", secret=" + secret + ", type="
+                + type + ", isSuper=" + isSuper() + ", redirectURI="
+                + redirectURI + ", registeredBy=" + registeredBy
+                + ", description=" + description;
+    }
+
+    @Override
+    public int compareTo (OAuth2Client o) {
+        return this.getName().compareTo(o.getName());
+    }
+
+    public boolean isSuper () {
+        return isSuper;
+    }
+
+    public void setSuper (boolean isSuper) {
+        this.isSuper = isSuper;
+    }
+
+    public String getId () {
+        return id;
+    }
+
+    public void setId (String id) {
+        this.id = id;
+    }
+
+    public String getName () {
+        return name;
+    }
+
+    public void setName (String name) {
+        this.name = name;
+    }
+
+    public String getSecret () {
+        return secret;
+    }
+
+    public void setSecret (String secret) {
+        this.secret = secret;
+    }
+
+    public OAuth2ClientType getType () {
+        return type;
+    }
+
+    public void setType (OAuth2ClientType type) {
+        this.type = type;
+    }
+
+    public String getRedirectURI () {
+        return redirectURI;
+    }
+
+    public void setRedirectURI (String redirectURI) {
+        this.redirectURI = redirectURI;
+    }
+
+    public String getRegisteredBy () {
+        return registeredBy;
+    }
+
+    public void setRegisteredBy (String registeredBy) {
+        this.registeredBy = registeredBy;
+    }
+
+    public ZonedDateTime getRegistrationDate () {
+        return registrationDate;
+    }
+
+    public void setRegistrationDate (ZonedDateTime registrationDate) {
+        this.registrationDate = registrationDate;
+    }
+
+    public String getDescription () {
+        return description;
+    }
+
+    public void setDescription (String description) {
+        this.description = description;
+    }
+
+    public String getUrl () {
+        return url;
+    }
+
+    public void setUrl (String url) {
+        this.url = url;
+    }
+
+    public String getSource () {
+        return source;
+    }
+
+    public void setSource (String source) {
+        this.source = source;
+    }
+
+    public boolean isPermitted () {
+        return isPermitted;
+    }
+
+    public void setPermitted (boolean isPermitted) {
+        this.isPermitted = isPermitted;
+    }
+
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java b/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java
new file mode 100644
index 0000000..db70d6a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/entity/RefreshToken.java
@@ -0,0 +1,134 @@
+package de.ids_mannheim.korap.oauth2.entity;
+
+import java.time.ZonedDateTime;
+import java.util.Set;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+
+/**
+ * Describes oauth2_refresh_token database table mapping and refresh
+ * token relations to access scopes, access tokens, and oauth2
+ * clients.
+ * 
+ * @author margaretha
+ *
+ */
+@Entity
+@Table(name = "oauth2_refresh_token")
+public class RefreshToken {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    private String token;
+    @Column(name = "created_date", updatable = false)
+    private ZonedDateTime createdDate;
+    @Column(name = "expiry_date", updatable = false)
+    private ZonedDateTime expiryDate;
+    @Column(name = "user_id")
+    private String userId;
+    // @Column(name = "client_id")
+    // private String clientId;
+    @Column(name = "user_auth_time", updatable = false)
+    private ZonedDateTime userAuthenticationTime;
+    @Column(name = "is_revoked")
+    private boolean isRevoked;
+
+    @OneToMany(fetch = FetchType.EAGER, mappedBy = "refreshToken")
+    private Set<AccessToken> accessTokens;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "client")
+    private OAuth2Client client;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = "oauth2_refresh_token_scope", joinColumns = @JoinColumn(name = "token_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "scope_id", referencedColumnName = "id"), uniqueConstraints = @UniqueConstraint(columnNames = {
+            "token_id", "scope_id" }))
+    private Set<AccessScope> scopes;
+
+    public String getToken () {
+        return token;
+    }
+
+    public void setToken (String token) {
+        this.token = token;
+    }
+
+    public ZonedDateTime getCreatedDate () {
+        return createdDate;
+    }
+
+    public void setCreatedDate (ZonedDateTime createdDate) {
+        this.createdDate = createdDate;
+    }
+
+    public ZonedDateTime getExpiryDate () {
+        return expiryDate;
+    }
+
+    public void setExpiryDate (ZonedDateTime expiryDate) {
+        this.expiryDate = expiryDate;
+    }
+
+    public String getUserId () {
+        return userId;
+    }
+
+    public void setUserId (String userId) {
+        this.userId = userId;
+    }
+
+    public ZonedDateTime getUserAuthenticationTime () {
+        return userAuthenticationTime;
+    }
+
+    public void setUserAuthenticationTime (
+            ZonedDateTime userAuthenticationTime) {
+        this.userAuthenticationTime = userAuthenticationTime;
+    }
+
+    public boolean isRevoked () {
+        return isRevoked;
+    }
+
+    public void setRevoked (boolean isRevoked) {
+        this.isRevoked = isRevoked;
+    }
+
+    public Set<AccessToken> getAccessTokens () {
+        return accessTokens;
+    }
+
+    public void setAccessTokens (Set<AccessToken> accessTokens) {
+        this.accessTokens = accessTokens;
+    }
+
+    public Set<AccessScope> getScopes () {
+        return scopes;
+    }
+
+    public void setScopes (Set<AccessScope> scopes) {
+        this.scopes = scopes;
+    }
+
+    public OAuth2Client getClient () {
+        return client;
+    }
+
+    public void setClient (OAuth2Client client) {
+        this.client = client;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/DummyOAuth2ScopeServiceImpl.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/DummyOAuth2ScopeServiceImpl.java
new file mode 100644
index 0000000..38e039a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/DummyOAuth2ScopeServiceImpl.java
@@ -0,0 +1,16 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+
+public class DummyOAuth2ScopeServiceImpl implements OAuth2ScopeService {
+
+    @Override
+    public void verifyScope (TokenContext context, OAuth2Scope requiredScope)
+            throws KustvaktException {
+        // TODO Auto-generated method stub
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AdminService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AdminService.java
new file mode 100644
index 0000000..60edf29
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AdminService.java
@@ -0,0 +1,50 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
+import de.ids_mannheim.korap.oauth2.dao.OAuth2ClientDao;
+import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+
+@Service
+public class OAuth2AdminService {
+
+    @Autowired
+    private OAuth2ClientService clientService;
+
+    @Autowired
+    private AccessTokenDao tokenDao;
+    @Autowired
+    private RefreshTokenDao refreshDao;
+    @Autowired
+    private OAuth2ClientDao clientDao;
+
+    public void cleanTokens () {
+        tokenDao.deleteInvalidAccessTokens();
+        refreshDao.deleteInvalidRefreshTokens();
+        tokenDao.clearCache();
+    }
+
+    public void updatePrivilege (String clientId, boolean isSuper)
+            throws KustvaktException {
+
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        if (isSuper) {
+            if (!client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+                throw new KustvaktException(StatusCodes.NOT_ALLOWED,
+                        "Only confidential clients are allowed to be super clients.");
+            }
+        }
+        else {
+            clientService.revokeAllAuthorizationsByClientId(clientId);
+        }
+
+        client.setSuper(isSuper);
+        clientDao.updateClient(client);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
new file mode 100644
index 0000000..36b027f
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
@@ -0,0 +1,377 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Set;
+
+import org.apache.commons.validator.routines.UrlValidator;
+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 com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationErrorResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.AuthorizationSuccessResponse;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.id.State;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.dao.AuthorizationDao;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+
+/**
+ * Describes business logic behind OAuth2 authorization requests.
+ * 
+ * @author margaretha
+ *
+ */
+@Service(value = "authorizationService")
+public class OAuth2AuthorizationService {
+
+    public static Logger jlog = LogManager
+            .getLogger(OAuth2AuthorizationService.class);
+
+    public static boolean DEBUG = false;
+
+    @Autowired
+    private RandomCodeGenerator codeGenerator;
+    @Autowired
+    protected OAuth2ClientService clientService;
+    @Autowired
+    protected OAuth2ScopeServiceImpl scopeService;
+    @Autowired
+    private AuthorizationDao authorizationDao;
+    @Autowired
+    private UrlValidator redirectURIValidator;
+
+    @Autowired
+    protected FullConfiguration config;
+
+    public State createAuthorizationState (String state) {
+        State authState = null;
+        if (state != null && !state.isEmpty())
+            authState = new State(state);
+        return authState;
+    }
+
+    public AuthorizationErrorResponse createAuthorizationError (
+            KustvaktException e, String state) {
+        State authState = createAuthorizationState(state);
+        ErrorObject error = e.getOauth2Error();
+        error = error.setDescription(e.getMessage());
+        AuthorizationErrorResponse errorResponse = new AuthorizationErrorResponse(
+                e.getRedirectUri(), error, authState, null);
+        return errorResponse;
+    }
+
+    public URI requestAuthorizationCode (URI requestURI, String clientId,
+            String redirectUri, String scope, String state, String username,
+            ZonedDateTime authenticationTime) throws KustvaktException {
+
+        URI redirectURI = null;
+        String code;
+        try {
+            OAuth2Client client = clientService.authenticateClientId(clientId);
+            redirectURI = verifyRedirectUri(client, redirectUri);
+            //checkResponseType(authzRequest.getResponseType(), redirectURI);
+            code = codeGenerator.createRandomCode();
+            URI responseURI = createAuthorizationResponse(requestURI,
+                    redirectURI, code, state);
+
+            createAuthorization(username, clientId, redirectUri, scope,
+                    code.toString(), authenticationTime, null);
+            return responseURI;
+
+        }
+        catch (KustvaktException e) {
+            e.setRedirectUri(redirectURI);
+            throw e;
+        }
+    }
+
+    private URI createAuthorizationResponse (URI requestURI, URI redirectURI,
+            String code, String state) throws KustvaktException {
+        AuthorizationRequest authRequest = null;
+        try {
+            authRequest = AuthorizationRequest.parse(requestURI);
+
+            if (authRequest.getResponseType()
+                    .equals(new ResponseType(ResponseType.Value.CODE))) {
+
+                State authState = createAuthorizationState(state);
+                AuthorizationSuccessResponse response = new AuthorizationSuccessResponse(
+                        redirectURI, new AuthorizationCode(code), null,
+                        authState, null);
+                return response.toURI();
+            }
+            else {
+                KustvaktException ke = new KustvaktException(
+                        StatusCodes.UNSUPPORTED_RESPONSE_TYPE,
+                        "Unsupported response type. Only code is supported.",
+                        OAuth2Error.UNSUPPORTED_RESPONSE_TYPE);
+                throw ke;
+            }
+        }
+        catch (ParseException e) {
+            KustvaktException ke = new KustvaktException(
+                    StatusCodes.INVALID_REQUEST, e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST_URI);
+            throw ke;
+        }
+
+    }
+
+    @Deprecated
+    public String createAuthorization (String username, String clientId,
+            String redirectUri, Set<String> scopeSet, String code,
+            ZonedDateTime authenticationTime, String nonce)
+            throws KustvaktException {
+
+        if (scopeSet == null || scopeSet.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "scope is required", OAuth2Error.INVALID_SCOPE);
+        }
+        Set<AccessScope> scopes = scopeService.convertToAccessScope(scopeSet);
+
+        authorizationDao.storeAuthorizationCode(clientId, username, code,
+                scopes, redirectUri, authenticationTime, nonce);
+        return String.join(" ", scopeSet);
+    }
+
+    /**
+     * Authorization code request does not require client
+     * authentication, but only checks if the client id exists.
+     * 
+     * @param username
+     * @param clientId
+     * @param redirectUri
+     * @param scope
+     * @param code
+     * @param authenticationTime
+     *            user authentication time
+     * @param nonce
+     * @throws KustvaktException
+     */
+    public void createAuthorization (String username, String clientId,
+            String redirectUri, String scope, String code,
+            ZonedDateTime authenticationTime, String nonce)
+            throws KustvaktException {
+
+        if (scope == null || scope.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "scope is required", OAuth2Error.INVALID_SCOPE);
+        }
+        Set<AccessScope> accessScopes = scopeService
+                .convertToAccessScope(scope);
+
+        authorizationDao.storeAuthorizationCode(clientId, username, code,
+                accessScopes, redirectUri, authenticationTime, nonce);
+    }
+
+    @Deprecated
+    protected void checkResponseType (String responseType)
+            throws KustvaktException {
+        if (responseType == null || responseType.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "response_type is missing.", OAuth2Error.INVALID_REQUEST);
+        }
+        else if (!responseType.equals("code")) {
+            throw new KustvaktException(StatusCodes.NOT_SUPPORTED,
+                    "unsupported response_type: " + responseType,
+                    OAuth2Error.INVALID_REQUEST);
+        }
+    }
+
+    /**
+     * If the request contains a redirect_uri parameter, the server
+     * must confirm it is a valid redirect URI.
+     * 
+     * If there is no redirect_uri parameter in the request, and only
+     * one URI was registered, the server uses the redirect URL that
+     * was previously registered.
+     * 
+     * If no redirect URL has been registered, this is an error.
+     * 
+     * @param client
+     *            an OAuth2Client
+     * @param redirectUri
+     *            the redirect_uri value
+     * @return a client's redirect URI
+     * @throws KustvaktException
+     */
+    public URI verifyRedirectUri (OAuth2Client client, String redirectUri)
+            throws KustvaktException {
+
+        String registeredUri = client.getRedirectURI();
+
+        if (redirectUri != null && !redirectUri.isEmpty()) {
+            // check if the redirect URI the same as that in DB
+            if (!redirectURIValidator.isValid(redirectUri)
+                    || (registeredUri != null && !registeredUri.isEmpty()
+                            && !redirectUri.equals(registeredUri))) {
+                throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
+                        "Invalid redirect URI", OAuth2Error.INVALID_REQUEST);
+            }
+        }
+        // redirect_uri is not required in client registration
+        else if (registeredUri != null && !registeredUri.isEmpty()) {
+            redirectUri = registeredUri;
+        }
+        else {
+            throw new KustvaktException(StatusCodes.MISSING_REDIRECT_URI,
+                    "Missing parameter: redirect URI",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+        URI redirectURI;
+        try {
+            redirectURI = new URI(redirectUri);
+        }
+        catch (URISyntaxException e) {
+            throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
+                    "Invalid redirect URI", OAuth2Error.INVALID_REQUEST);
+        }
+
+        return redirectURI;
+    }
+
+    public KustvaktException checkRedirectUri (KustvaktException e,
+            String clientId, String redirectUri) {
+        int statusCode = e.getStatusCode();
+        if (clientId != null && !clientId.isEmpty()
+                && statusCode != StatusCodes.CLIENT_NOT_FOUND
+                && statusCode != StatusCodes.AUTHORIZATION_FAILED
+                && statusCode != StatusCodes.INVALID_REDIRECT_URI) {
+            String registeredUri = null;
+            try {
+                OAuth2Client client = clientService.retrieveClient(clientId);
+                registeredUri = client.getRedirectURI();
+            }
+            catch (KustvaktException e1) {}
+
+            if (redirectUri != null && !redirectUri.isEmpty()) {
+                if (registeredUri != null && !registeredUri.isEmpty()
+                        && !redirectUri.equals(registeredUri)) {
+                    return new KustvaktException(
+                            StatusCodes.INVALID_REDIRECT_URI,
+                            "Invalid redirect URI",
+                            OAuth2Error.INVALID_REQUEST);
+                }
+                else {
+                    try {
+                        e.setRedirectUri(new URI(redirectUri));
+                    }
+                    catch (URISyntaxException e1) {
+                        return new KustvaktException(
+                                StatusCodes.INVALID_REDIRECT_URI,
+                                "Invalid redirect URI",
+                                OAuth2Error.INVALID_REQUEST);
+                    }
+                    e.setResponseStatus(HttpStatus.SC_TEMPORARY_REDIRECT);
+                }
+            }
+            else if (registeredUri != null && !registeredUri.isEmpty()) {
+                try {
+                    e.setRedirectUri(new URI(registeredUri));
+                }
+                catch (URISyntaxException e1) {
+                    return new KustvaktException(
+                            StatusCodes.INVALID_REDIRECT_URI,
+                            "Invalid redirect URI",
+                            OAuth2Error.INVALID_REQUEST);
+                }
+                e.setResponseStatus(HttpStatus.SC_TEMPORARY_REDIRECT);
+            }
+            else {
+                return new KustvaktException(StatusCodes.MISSING_REDIRECT_URI,
+                        "Missing parameter: redirect URI",
+                        OAuth2Error.INVALID_REQUEST);
+            }
+        }
+
+        return e;
+    }
+
+    public Authorization retrieveAuthorization (String code)
+            throws KustvaktException {
+        return authorizationDao.retrieveAuthorizationCode(code);
+    }
+
+    public Authorization verifyAuthorization (Authorization authorization,
+            String clientId, String redirectURI) throws KustvaktException {
+
+        // EM: can Kustvakt be specific about the invalid grant error
+        // description?
+        if (!authorization.getClientId().equals(clientId)) {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Invalid authorization", OAuth2Error.INVALID_GRANT);
+        }
+        if (authorization.isRevoked()) {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Invalid authorization", OAuth2Error.INVALID_GRANT);
+        }
+
+        if (isExpired(authorization.getExpiryDate())) {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Authorization expired", OAuth2Error.INVALID_GRANT);
+        }
+
+        String authorizedUri = authorization.getRedirectURI();
+        if (authorizedUri != null && !authorizedUri.isEmpty()) {
+            if (redirectURI == null || redirectURI.isEmpty()) {
+                throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
+                        "Missing redirect URI", OAuth2Error.INVALID_GRANT);
+            }
+            if (!authorizedUri.equals(redirectURI)) {
+                throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
+                        "Invalid redirect URI", OAuth2Error.INVALID_GRANT);
+            }
+        }
+        else if (redirectURI != null && !redirectURI.isEmpty()) {
+            throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
+                    "Invalid redirect URI", OAuth2Error.INVALID_GRANT);
+        }
+
+        authorization.setRevoked(true);
+        authorization = authorizationDao.updateAuthorization(authorization);
+
+        return authorization;
+    }
+
+    public void addTotalAttempts (Authorization authorization)
+            throws KustvaktException {
+        int totalAttempts = authorization.getTotalAttempts() + 1;
+        if (totalAttempts == config.getMaxAuthenticationAttempts()) {
+            authorization.setRevoked(true);
+        }
+        authorization.setTotalAttempts(totalAttempts);
+        authorizationDao.updateAuthorization(authorization);
+    }
+
+    private boolean isExpired (ZonedDateTime expiryDate) {
+        ZonedDateTime now = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+        if (DEBUG) {
+            jlog.debug("createdDate: " + expiryDate);
+            jlog.debug("expiration: " + expiryDate + ", now: " + now);
+        }
+        if (expiryDate.isAfter(now)) {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
new file mode 100644
index 0000000..3781458
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
@@ -0,0 +1,479 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.validator.routines.UrlValidator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.dto.InstalledPluginDto;
+import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
+import de.ids_mannheim.korap.entity.InstalledPlugin;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
+import de.ids_mannheim.korap.oauth2.dao.AuthorizationDao;
+import de.ids_mannheim.korap.oauth2.dao.InstalledPluginDao;
+import de.ids_mannheim.korap.oauth2.dao.OAuth2ClientDao;
+import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
+import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+
+/**
+ * Defines business logic related to OAuth2 client including
+ * client registration and client authentication.
+ * 
+ * According to RFC 6749, an authorization server MUST:
+ * <ul>
+ * <li>
+ * require client authentication for confidential clients or for any
+ * client that was issued client credentials (or with other
+ * authentication
+ * requirements),
+ * </li>
+ * 
+ * <li>authenticate the client if client authentication is included
+ * </li>
+ * </ul>
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class OAuth2ClientService {
+
+    //    public static final UrlValidator redirectURIValidator =
+    //            new UrlValidator(new String[] { "http", "https" },
+    //                    UrlValidator.NO_FRAGMENTS + UrlValidator.ALLOW_LOCAL_URLS);
+
+    @Autowired
+    private OAuth2TokenService tokenService;
+    @Autowired
+    private InstalledPluginDao pluginDao;
+    @Autowired
+    private OAuth2ClientDao clientDao;
+    @Autowired
+    private AccessTokenDao tokenDao;
+    @Autowired
+    private RefreshTokenDao refreshDao;
+    @Autowired
+    private AuthorizationDao authorizationDao;
+    @Autowired
+    private AdminDao adminDao;
+    @Autowired
+    private UrlValidator redirectURIValidator;
+    @Autowired
+    private UrlValidator urlValidator;
+    @Autowired
+    private EncryptionIface encryption;
+    @Autowired
+    private RandomCodeGenerator codeGenerator;
+    @Autowired
+    private FullConfiguration config;
+
+    public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
+            String registeredBy) throws KustvaktException {
+        try {
+            ParameterChecker.checkNameValue(clientJson.getName(),
+                    "client_name");
+            ParameterChecker.checkObjectValue(clientJson.getType(),
+                    "client_type");
+            ParameterChecker.checkStringValue(clientJson.getName(),
+                    "client_description");
+        }
+        catch (KustvaktException e) {
+            throw new KustvaktException(e.getStatusCode(), e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        String url = clientJson.getUrl();
+        if (url != null && !url.isEmpty()) {
+            if (!urlValidator.isValid(url)) {
+                throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                        "Invalid URL", OAuth2Error.INVALID_REQUEST);
+            }
+        }
+
+        String redirectURI = clientJson.getRedirectURI();
+        if (redirectURI != null && !redirectURI.isEmpty()
+                && !redirectURIValidator.isValid(redirectURI)) {
+            throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
+                    "Invalid redirect URI", OAuth2Error.INVALID_REQUEST);
+        }
+
+        // boolean isNative = isNativeClient(url, redirectURI);
+
+        String secret = null;
+        String secretHashcode = null;
+        if (clientJson.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+            // RFC 6749:
+            // The authorization server MUST NOT issue client
+            // passwords or other client credentials to native
+            // application (clients installed and executed on the
+            // device used by the resource owner e.g. desktop
+            // application, native mobile application) or
+            // user-agent-based application clients for client
+            // authentication. The authorization server MAY issue a
+            // client password or other credentials for a specific
+            // installation of a native application client on a
+            // specific device.
+
+            secret = codeGenerator.createRandomCode();
+            secretHashcode = encryption.secureHash(secret);
+        }
+
+        String id = codeGenerator.createRandomCode();
+        id = codeGenerator.filterRandomCode(id);
+
+        try {
+            clientDao.registerClient(id, secretHashcode, clientJson.getName(),
+                    clientJson.getType(), url, redirectURI, registeredBy,
+                    clientJson.getDescription(),
+                    clientJson.getRefreshTokenExpiry(), clientJson.getSource());
+        }
+        catch (KustvaktException e) {
+            throw new KustvaktException(e.getStatusCode(), e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST);
+        }
+        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.CLIENT_REGISTRATION_FAILED,
+                    cause.getMessage(), OAuth2Error.INVALID_REQUEST);
+        }
+
+        return new OAuth2ClientDto(id, secret);
+    }
+
+    @Deprecated
+    private boolean isNativeClient (String url, String redirectURI)
+            throws KustvaktException {
+        if (url == null || url.isEmpty() || redirectURI == null
+                || redirectURI.isEmpty()) {
+            return false;
+        }
+
+        String nativeHost = config.getNativeClientHost();
+        String urlHost = null;
+        try {
+            urlHost = new URL(url).getHost();
+        }
+        catch (MalformedURLException e) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "Invalid url :" + e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        if (!urlHost.equals(nativeHost)) {
+            return false;
+        }
+
+        String uriHost = null;
+        try {
+            uriHost = new URI(redirectURI).getHost();
+        }
+        catch (URISyntaxException e) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "Invalid redirectURI: " + e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST);
+        }
+        if (!uriHost.equals(nativeHost)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public void deregisterClient (String clientId, String username)
+            throws KustvaktException {
+
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+
+        if (adminDao.isAdmin(username)
+                || client.getRegisteredBy().equals(username)) {
+
+            revokeAllAuthorizationsByClientId(clientId);
+            clientDao.deregisterClient(client);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public void revokeAllAuthorizationsByClientId (String clientId)
+            throws KustvaktException {
+
+        // revoke all related authorization codes
+        List<Authorization> authList = authorizationDao
+                .retrieveAuthorizationsByClientId(clientId);
+        for (Authorization authorization : authList) {
+            authorization.setRevoked(true);
+            authorizationDao.updateAuthorization(authorization);
+        }
+
+        // revoke all related access tokens
+        List<AccessToken> tokens = tokenDao
+                .retrieveAccessTokenByClientId(clientId, null);
+        for (AccessToken token : tokens) {
+            token.setRevoked(true);
+            tokenDao.updateAccessToken(token);
+        }
+
+        List<RefreshToken> refreshTokens = refreshDao
+                .retrieveRefreshTokenByClientId(clientId, null);
+        for (RefreshToken token : refreshTokens) {
+            token.setRevoked(true);
+            refreshDao.updateRefreshToken(token);
+        }
+    }
+
+    public OAuth2ClientDto resetSecret (String clientId, String username)
+            throws KustvaktException {
+
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        if (!client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+            throw new KustvaktException(StatusCodes.NOT_ALLOWED,
+                    "Operation is not allowed for public clients",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+        if (adminDao.isAdmin(username)
+                || client.getRegisteredBy().equals(username)) {
+
+            String secret = codeGenerator.createRandomCode();
+            String secretHashcode = encryption.secureHash(secret);
+
+            client.setSecret(secretHashcode);
+            clientDao.updateClient(client);
+            return new OAuth2ClientDto(clientId, secret);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public OAuth2Client authenticateClient (String clientId,
+            String clientSecret) throws KustvaktException {
+        return authenticateClient(clientId, clientSecret, false);
+    }
+
+    public OAuth2Client authenticateClient (String clientId,
+            String clientSecret, boolean isSuper) throws KustvaktException {
+        String errorClient = "client";
+        if (isSuper) {
+            errorClient = "super_client";
+        }
+
+        if (clientId == null || clientId.isEmpty()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+                    "Missing parameter: " + errorClient + "_id",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        authenticateClient(client, clientSecret, errorClient);
+        return client;
+    }
+
+    public void authenticateClient (OAuth2Client client, String clientSecret,
+            String errorClient) throws KustvaktException {
+        if (clientSecret == null || clientSecret.isEmpty()) {
+            if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+                throw new KustvaktException(
+                        StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+                        "Missing parameter: " + errorClient + "_secret",
+                        OAuth2Error.INVALID_REQUEST);
+            }
+        }
+        else if (client.getSecret() == null || client.getSecret().isEmpty()) {
+            if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+                throw new KustvaktException(
+                        StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+                        errorClient + "_secret was not registered",
+                        OAuth2Error.INVALID_CLIENT);
+            }
+        }
+        else if (!encryption.checkHash(clientSecret, client.getSecret())) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+                    "Invalid " + errorClient + " credentials",
+                    OAuth2Error.INVALID_CLIENT);
+        }
+    }
+
+    public OAuth2Client authenticateClientId (String clientId)
+            throws KustvaktException {
+        if (clientId == null || clientId.isEmpty()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+                    "Missing parameter: client_id",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        return clientDao.retrieveClientById(clientId);
+    }
+
+    public OAuth2ClientInfoDto retrieveClientInfo (String clientId)
+            throws KustvaktException {
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        //        if (adminDao.isAdmin(username)
+        //                || username.equals(client.getRegisteredBy())) {
+        return new OAuth2ClientInfoDto(client);
+        //        }
+        //        else {
+        //            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+        //                    "Unauthorized operation for user: " + username, username);
+        //        }
+    }
+
+    public OAuth2Client retrieveClient (String clientId)
+            throws KustvaktException {
+        return clientDao.retrieveClientById(clientId);
+    }
+
+    public List<OAuth2ClientInfoDto> listUserAuthorizedClients (String username)
+            throws KustvaktException {
+        List<OAuth2Client> userClients = clientDao
+                .retrieveUserAuthorizedClients(username);
+        userClients.addAll(clientDao.retrieveClientsByAccessTokens(username));
+
+        List<String> clientIds = new ArrayList<>();
+        List<OAuth2Client> uniqueClients = new ArrayList<>();
+        for (OAuth2Client c : userClients) {
+            String id = c.getId();
+            if (!clientIds.contains(id)) {
+                clientIds.add(id);
+                uniqueClients.add(c);
+            }
+        }
+
+        Collections.sort(uniqueClients);
+        return createClientDtos(uniqueClients);
+    }
+
+    public List<OAuth2ClientInfoDto> listUserRegisteredClients (String username)
+            throws KustvaktException {
+        List<OAuth2Client> userClients = clientDao
+                .retrieveUserRegisteredClients(username);
+        Collections.sort(userClients);
+        return createClientDtos(userClients);
+    }
+
+    public List<OAuth2ClientInfoDto> listPlugins (boolean isPermitted)
+            throws KustvaktException {
+
+        List<OAuth2Client> plugins = clientDao.retrievePlugins(isPermitted);
+        Collections.sort(plugins);
+        return createClientDtos(plugins);
+    }
+
+    public List<InstalledPluginDto> listInstalledPlugins (String superClientId,
+            String username) throws KustvaktException {
+
+        List<InstalledPlugin> plugins = pluginDao
+                .retrieveInstalledPlugins(superClientId, username);
+        Collections.sort(plugins); // by client name
+
+        List<InstalledPluginDto> list = new ArrayList<InstalledPluginDto>(
+                plugins.size());
+        for (InstalledPlugin p : plugins) {
+            list.add(new InstalledPluginDto(p));
+        }
+
+        return list;
+    }
+
+    public InstalledPluginDto installPlugin (String superClientId,
+            String clientId, String installedBy) throws KustvaktException {
+
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        if (!client.isPermitted()) {
+            throw new KustvaktException(StatusCodes.PLUGIN_NOT_PERMITTED,
+                    "Plugin is not permitted", clientId);
+        }
+
+        if (isPluginInstalled(superClientId, clientId, installedBy)) {
+            throw new KustvaktException(StatusCodes.PLUGIN_HAS_BEEN_INSTALLED,
+                    "Plugin has been installed", clientId);
+        }
+
+        OAuth2Client superClient = clientDao.retrieveClientById(superClientId);
+        InstalledPlugin plugin = pluginDao.storeUserPlugin(superClient, client,
+                installedBy);
+
+        InstalledPluginDto dto = new InstalledPluginDto(plugin);
+        return dto;
+    }
+
+    public void uninstallPlugin (String superClientId, String clientId,
+            String username) throws KustvaktException {
+        pluginDao.uninstallPlugin(superClientId, clientId, username);
+        tokenService.revokeAllClientTokensForUser(clientId, username);
+    }
+
+    private boolean isPluginInstalled (String superClientId, String clientId,
+            String installedBy) {
+        try {
+            pluginDao.retrieveInstalledPlugin(superClientId, clientId,
+                    installedBy);
+        }
+        catch (KustvaktException e) {
+            return false;
+        }
+        return true;
+    }
+
+    private List<OAuth2ClientInfoDto> createClientDtos (
+            List<OAuth2Client> userClients) throws KustvaktException {
+        List<OAuth2ClientInfoDto> dtoList = new ArrayList<>(userClients.size());
+        for (OAuth2Client uc : userClients) {
+            if (uc.isSuper())
+                continue;
+            OAuth2ClientInfoDto dto = new OAuth2ClientInfoDto(uc);
+            dtoList.add(dto);
+        }
+        return dtoList;
+    }
+
+    public boolean isPublicClient (OAuth2Client oAuth2Client) {
+        return oAuth2Client.getType().equals(OAuth2ClientType.PUBLIC);
+    }
+
+    public void verifySuperClient (String clientId, String clientSecret)
+            throws KustvaktException {
+        OAuth2Client client = authenticateClient(clientId, clientSecret, true);
+        if (!client.isSuper()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Only super client is allowed to use this service",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2InitClientService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2InitClientService.java
new file mode 100644
index 0000000..25b07dd
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2InitClientService.java
@@ -0,0 +1,110 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+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 com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.oauth2.dao.OAuth2ClientDao;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+
+@Service
+public class OAuth2InitClientService {
+
+    private static Logger log = LogManager
+            .getLogger(OAuth2InitClientService.class);
+    public static String OAUTH2_CLIENT_JSON_INPUT_FILE = "initial_super_client.json";
+    public static String OUTPUT_FILENAME = "super_client_info";
+    public static String TEST_OUTPUT_FILENAME = "test_super_client_info";
+
+    @Autowired
+    private OAuth2ClientService clientService;
+    @Autowired
+    private OAuth2ClientDao clientDao;
+    @Autowired
+    private EncryptionIface encryption;
+
+    public void createInitialSuperClient (String outputFilename)
+            throws IOException, KustvaktException {
+
+        File dir = new File(KustvaktConfiguration.DATA_FOLDER);
+        if (!dir.exists()) {
+            dir.mkdir();
+        }
+
+        String path = KustvaktConfiguration.DATA_FOLDER + "/" + outputFilename;
+        File f = new File(path);
+
+        if (!f.exists()) {
+            OAuth2ClientJson json = readOAuth2ClientJsonFile();
+            OAuth2ClientDto clientDto = clientService.registerClient(json,
+                    "system");
+            String clientId = clientDto.getClient_id();
+            OAuth2Client client = clientService.retrieveClient(clientId);
+            client.setSuper(true);
+            clientDao.updateClient(client);
+            JsonUtils.writeFile(path, clientDto);
+
+            log.info(
+                    "Initial super client has been successfully registered. See "
+                            + path);
+        }
+        else {
+            JsonNode node = JsonUtils.readFile(path, JsonNode.class);
+            String existingClientId = node.at("/client_id").asText();
+            String clientSecret = node.at("/client_secret").asText();
+            String secretHashcode = encryption.secureHash(clientSecret);
+
+            try {
+                clientService.retrieveClient(existingClientId);
+                log.info("Super client info file exists. Initial super client "
+                        + "registration is cancelled.");
+            }
+            catch (Exception e) {
+                log.info("Super client info file exists but the client "
+                        + "doesn't exist in the database.");
+                OAuth2ClientJson json = readOAuth2ClientJsonFile();
+                OAuth2ClientDto clientDto = clientService.registerClient(json,
+                        "system");
+                String clientId = clientDto.getClient_id();
+                OAuth2Client client = clientService.retrieveClient(clientId);
+                client.setSuper(true);
+                client.setId(existingClientId);
+                client.setSecret(secretHashcode);
+                clientDao.updateClient(client);
+            }
+        }
+    }
+
+    private OAuth2ClientJson readOAuth2ClientJsonFile ()
+            throws IOException, KustvaktException {
+        File f = new File(OAUTH2_CLIENT_JSON_INPUT_FILE);
+        if (f.exists()) {
+            return JsonUtils.readFile(OAUTH2_CLIENT_JSON_INPUT_FILE,
+                    OAuth2ClientJson.class);
+        }
+        else {
+            InputStream is = getClass().getClassLoader().getResourceAsStream(
+                    "json/" + OAUTH2_CLIENT_JSON_INPUT_FILE);
+            return JsonUtils.read(is, OAuth2ClientJson.class);
+        }
+
+    }
+
+    public void createInitialTestSuperClient ()
+            throws IOException, KustvaktException {
+        createInitialSuperClient(TEST_OUTPUT_FILENAME);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
new file mode 100644
index 0000000..e51b83a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
@@ -0,0 +1,26 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.security.context.TokenContext;
+
+/**
+ * @author margaretha
+ *
+ */
+public interface OAuth2ScopeService {
+
+    /**
+     * Verifies whether the given token context contains the required
+     * scope
+     * 
+     * @param context
+     *            a token context containing authorized scopes
+     * @param requiredScope
+     *            the required scope
+     * @throws KustvaktException
+     */
+    void verifyScope (TokenContext context, OAuth2Scope requiredScope)
+            throws KustvaktException;
+
+}
\ No newline at end of file
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeServiceImpl.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeServiceImpl.java
new file mode 100644
index 0000000..acf236b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeServiceImpl.java
@@ -0,0 +1,184 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.dao.AdminDao;
+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.entity.AccessScope;
+import de.ids_mannheim.korap.security.context.TokenContext;
+
+/**
+ * Defines business logic related to OAuth2 scopes.
+ * 
+ * @author margaretha
+ *
+ */
+public class OAuth2ScopeServiceImpl implements OAuth2ScopeService {
+
+    @Autowired
+    private AccessScopeDao accessScopeDao;
+
+    @Autowired
+    private AdminDao adminDao;
+
+    /**
+     * Converts a set of scope strings to a set of {@link AccessScope}
+     * 
+     * @param scopes
+     * @return
+     * @throws KustvaktException
+     */
+    @Deprecated
+    public Set<AccessScope> convertToAccessScope (Collection<String> scopes)
+            throws KustvaktException {
+
+        List<AccessScope> definedScopes = accessScopeDao.retrieveAccessScopes();
+        Set<AccessScope> requestedScopes = new HashSet<AccessScope>(
+                scopes.size());
+        int index;
+        OAuth2Scope oauth2Scope = null;
+        for (String scope : scopes) {
+            try {
+                oauth2Scope = Enum.valueOf(OAuth2Scope.class,
+                        scope.toUpperCase());
+            }
+            catch (IllegalArgumentException e) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        "Invalid scope", OAuth2Error.INVALID_SCOPE);
+            }
+
+            index = definedScopes.indexOf(new AccessScope(oauth2Scope));
+            if (index == -1) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        "Invalid scope", OAuth2Error.INVALID_SCOPE);
+            }
+            else {
+                requestedScopes.add(definedScopes.get(index));
+            }
+        }
+        return requestedScopes;
+    }
+
+    public Set<AccessScope> convertToAccessScope (String scopes)
+            throws KustvaktException {
+
+        String[] scopeArray = scopes.split("\\s+");
+        List<AccessScope> definedScopes = accessScopeDao.retrieveAccessScopes();
+        Set<AccessScope> requestedScopes = new HashSet<AccessScope>(
+                scopeArray.length);
+        int index;
+        OAuth2Scope oauth2Scope = null;
+        for (String scope : scopeArray) {
+            try {
+                oauth2Scope = Enum.valueOf(OAuth2Scope.class,
+                        scope.toUpperCase());
+            }
+            catch (IllegalArgumentException e) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        "Invalid scope", OAuth2Error.INVALID_SCOPE);
+            }
+
+            index = definedScopes.indexOf(new AccessScope(oauth2Scope));
+            if (index == -1) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        "Invalid scope", OAuth2Error.INVALID_SCOPE);
+            }
+            else {
+                requestedScopes.add(definedScopes.get(index));
+            }
+        }
+        return requestedScopes;
+    }
+
+    public String convertAccessScopesToString (Set<AccessScope> scopes) {
+        Set<String> set = convertAccessScopesToStringSet(scopes);
+        return String.join(" ", set);
+    }
+
+    public Set<String> convertAccessScopesToStringSet (
+            Set<AccessScope> scopes) {
+        Set<String> set = scopes.stream().map(scope -> scope.toString())
+                .collect(Collectors.toSet());
+        return set;
+    }
+
+    /**
+     * Simple reduction of requested scopes, i.e. excluding any scopes
+     * that are not default scopes for a specific authorization grant.
+     * 
+     * @param scopes
+     * @param defaultScopes
+     * @return accepted scopes
+     */
+    public Set<String> filterScopes (Set<String> scopes,
+            Set<String> defaultScopes) {
+        Stream<String> stream = scopes.stream();
+        Set<String> filteredScopes = stream
+                .filter(scope -> defaultScopes.contains(scope))
+                .collect(Collectors.toSet());
+        return filteredScopes;
+    }
+
+    /* (non-Javadoc)
+     * @see de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService#verifyScope(de.ids_mannheim.korap.security.context.TokenContext, de.ids_mannheim.korap.oauth2.constant.OAuth2Scope)
+     */
+    @Override
+    public void verifyScope (TokenContext context, OAuth2Scope requiredScope)
+            throws KustvaktException {
+        if (context == null) {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Authentication required. Please log in!");
+        }
+
+        if (!adminDao.isAdmin(context.getUsername())
+                && context.getTokenType().equals(TokenType.BEARER)) {
+            Map<String, Object> parameters = context.getParameters();
+            String authorizedScope = (String) parameters.get(Attributes.SCOPE);
+            if (!authorizedScope.contains(OAuth2Scope.ALL.toString())
+                    && !authorizedScope.contains(requiredScope.toString())) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Scope " + requiredScope + " is not authorized");
+            }
+        }
+    }
+
+    /**
+     * Verify scopes given in a refresh request. The scopes must not
+     * include other scopes than those authorized in the original
+     * access token issued together with the refresh token.
+     * 
+     * @param requestScopes
+     *            requested scopes
+     * @param originalScopes
+     *            authorized scopes
+     * @return a set of requested {@link AccessScope}
+     * @throws KustvaktException
+     */
+    public Set<AccessScope> verifyRefreshScope (Set<String> requestScopes,
+            Set<AccessScope> originalScopes) throws KustvaktException {
+        Set<AccessScope> requestedScopes = convertToAccessScope(requestScopes);
+        for (AccessScope scope : requestedScopes) {
+            if (!originalScopes.contains(scope)) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        "Scope " + scope.getId() + " is not authorized.",
+                        OAuth2Error.INVALID_SCOPE);
+            }
+        }
+        return requestedScopes;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
new file mode 100644
index 0000000..48faecd
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
@@ -0,0 +1,701 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.net.URI;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
+import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.Tokens;
+import com.unboundid.ldap.sdk.LDAPException;
+
+import de.ids_mannheim.korap.authentication.AuthenticationManager;
+import de.ids_mannheim.korap.authentication.LdapAuth3;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
+import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
+import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2TokenDto;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
+import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import jakarta.persistence.NoResultException;
+
+/**
+ * OAuth2TokenService manages business logic related to OAuth2
+ * requesting and creating access token.
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class OAuth2TokenService {
+
+    @Autowired
+    protected OAuth2ClientService clientService;
+
+    @Autowired
+    private OAuth2AuthorizationService authorizationService;
+
+    @Autowired
+    protected OAuth2ScopeServiceImpl scopeService;
+
+    @Autowired
+    protected FullConfiguration config;
+    @Autowired
+    private AuthenticationManager authenticationManager;
+
+    @Autowired
+    private RandomCodeGenerator randomGenerator;
+
+    @Autowired
+    private AccessTokenDao tokenDao;
+    @Autowired
+    private RefreshTokenDao refreshDao;
+
+    /**
+     * RFC 6749:
+     * If the client type is confidential or the client was issued
+     * client credentials, the client MUST authenticate with the
+     * authorization server.
+     * 
+     * @param authorizationCode
+     * @param redirectURI
+     *            required if included in the authorization request
+     * @param clientId
+     *            required if there is no authorization header
+     * @param clientSecret
+     *            client_secret, required if client_secret was issued
+     *            for the client in client registration.
+     * @return an authorization
+     * @throws KustvaktException
+     */
+    protected Authorization retrieveAuthorization (String authorizationCode,
+            String redirectURI, String clientId, String clientSecret)
+            throws KustvaktException {
+
+        Authorization authorization = authorizationService
+                .retrieveAuthorization(authorizationCode);
+        try {
+            clientService.authenticateClient(clientId, clientSecret);
+            authorization = authorizationService
+                    .verifyAuthorization(authorization, clientId, redirectURI);
+        }
+        catch (KustvaktException e) {
+            authorizationService.addTotalAttempts(authorization);
+            throw e;
+        }
+        return authorization;
+    }
+
+    public ZonedDateTime authenticateUser (String username, String password,
+            Set<String> scopes) throws KustvaktException {
+        if (username == null || username.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "username is missing.", OAuth2Error.INVALID_REQUEST);
+        }
+        if (password == null || password.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "password is missing", OAuth2Error.INVALID_REQUEST);
+        }
+
+        Map<String, Object> attributes = new HashMap<>();
+        if (scopes != null && !scopes.isEmpty()) {
+            attributes.put(Attributes.SCOPE, scopes);
+        }
+        authenticationManager.authenticate(
+                config.getOAuth2passwordAuthentication(), username, password,
+                attributes);
+
+        ZonedDateTime authenticationTime = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+        return authenticationTime;
+    }
+
+    public AccessTokenResponse requestAccessToken (TokenRequest tokenRequest,
+            String clientId, String clientSecret) throws KustvaktException {
+
+        AuthorizationGrant authGrant = tokenRequest.getAuthorizationGrant();
+        GrantType grantType = authGrant.getType();
+        Scope scope = tokenRequest.getScope();
+        Set<String> scopeSet = new HashSet<>();
+        if (scope != null)
+            scopeSet.addAll(scope.toStringList());
+
+        if (grantType.equals(GrantType.AUTHORIZATION_CODE)) {
+            AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) authGrant;
+            String authCode = codeGrant.getAuthorizationCode().getValue();
+            URI uri = codeGrant.getRedirectionURI();
+            String redirectionURI = (uri != null) ? uri.toString() : null;
+
+            return requestAccessTokenWithAuthorizationCode(authCode,
+                    redirectionURI, clientId, clientSecret);
+        }
+        else if (grantType.equals(GrantType.PASSWORD)) {
+            ResourceOwnerPasswordCredentialsGrant passwordGrant = (ResourceOwnerPasswordCredentialsGrant) authGrant;
+            String username = passwordGrant.getUsername();
+            String password = passwordGrant.getPassword().getValue();
+            return requestAccessTokenWithPassword(clientId, clientSecret,
+                    username, password, scopeSet);
+        }
+        else if (grantType.equals(GrantType.CLIENT_CREDENTIALS)) {
+            return requestAccessTokenWithClientCredentials(clientId,
+                    clientSecret, scopeSet);
+        }
+        else if (grantType.equals(GrantType.REFRESH_TOKEN)) {
+            RefreshTokenGrant refreshGrant = (RefreshTokenGrant) authGrant;
+            String refreshToken = refreshGrant.getRefreshToken().getValue();
+            return requestAccessTokenWithRefreshToken(refreshToken, scopeSet,
+                    clientId, clientSecret);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.UNSUPPORTED_GRANT_TYPE,
+                    grantType + " is not supported.",
+                    OAuth2Error.UNSUPPORTED_GRANT_TYPE);
+        }
+
+    }
+
+    /**
+     * Revokes all access token associated with the given refresh
+     * token, and creates a new access token and a new refresh
+     * token with the same scopes. Thus, at one point of time,
+     * there is only one active access token associated with
+     * a refresh token.
+     * 
+     * Client authentication is done using the given client
+     * credentials.
+     * 
+     * TODO: should create a new refresh token when the old refresh
+     * token is used (DONE)
+     * 
+     * @param refreshTokenStr
+     * @param requestScopes
+     * @param clientId
+     * @param clientSecret
+     * @return if successful, a new access token
+     * @throws KustvaktException
+     */
+    private AccessTokenResponse requestAccessTokenWithRefreshToken (
+            String refreshTokenStr, Set<String> requestScopes, String clientId,
+            String clientSecret) throws KustvaktException {
+
+        if (refreshTokenStr == null || refreshTokenStr.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "Missing parameter: refresh_token",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        OAuth2Client oAuth2Client = clientService.authenticateClient(clientId,
+                clientSecret);
+
+        RefreshToken refreshToken;
+        try {
+            refreshToken = refreshDao.retrieveRefreshToken(refreshTokenStr);
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token is not found", OAuth2Error.INVALID_GRANT);
+        }
+
+        if (!clientId.equals(refreshToken.getClient().getId())) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Client " + clientId + " is not authorized",
+                    OAuth2Error.INVALID_CLIENT);
+        }
+        else if (refreshToken.isRevoked()) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token has been revoked",
+                    OAuth2Error.INVALID_GRANT);
+        }
+        else if (ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))
+                .isAfter(refreshToken.getExpiryDate())) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token is expired", OAuth2Error.INVALID_GRANT);
+        }
+
+        Set<AccessScope> tokenScopes = new HashSet<>(refreshToken.getScopes());
+        if (requestScopes != null && !requestScopes.isEmpty()) {
+            tokenScopes = scopeService.verifyRefreshScope(requestScopes,
+                    tokenScopes);
+            requestScopes = scopeService
+                    .convertAccessScopesToStringSet(tokenScopes);
+        }
+
+        // revoke the refresh token and all access tokens associated to it
+        revokeRefreshToken(refreshTokenStr);
+
+        return createsAccessTokenResponse(requestScopes, tokenScopes, clientId,
+                refreshToken.getUserId(),
+                refreshToken.getUserAuthenticationTime(), oAuth2Client);
+
+        // without new refresh token
+        // return createsAccessTokenResponse(scopes, requestedScopes,
+        // clientId,
+        // refreshToken.getUserId(),
+        // refreshToken.getUserAuthenticationTime(), refreshToken);
+    }
+
+    /**
+     * Issues an access token for the specified client if the
+     * authorization code is valid and client successfully
+     * authenticates.
+     * 
+     * @param code
+     *            authorization code, required
+     * @param redirectionURI
+     *            client redirect uri, required if specified in the
+     *            authorization request
+     * @param clientId
+     *            client id, required
+     * @param clientSecret
+     *            client secret, required
+     * @return an {@link AccessTokenResponse}
+     * @throws KustvaktException
+     */
+    private AccessTokenResponse requestAccessTokenWithAuthorizationCode (
+            String code, String redirectionURI, String clientId,
+            String clientSecret) throws KustvaktException {
+        Authorization authorization = retrieveAuthorization(code,
+                redirectionURI, clientId, clientSecret);
+
+        Set<String> scopes = scopeService
+                .convertAccessScopesToStringSet(authorization.getScopes());
+        OAuth2Client oAuth2Client = clientService.retrieveClient(clientId);
+        return createsAccessTokenResponse(scopes, authorization.getScopes(),
+                authorization.getClientId(), authorization.getUserId(),
+                authorization.getUserAuthenticationTime(), oAuth2Client);
+    }
+
+    /**
+     * Third party apps must not be allowed to use password grant.
+     * MH: password grant is only allowed for trusted clients (korap
+     * frontend)
+     * 
+     * According to RFC 6749, client authentication is only required
+     * for confidential clients and whenever client credentials are
+     * provided. Moreover, client_id is optional for password grant,
+     * but without it, the authentication server cannot check the
+     * client type. To make sure that confidential clients
+     * authenticate, client_id is made required (similar to
+     * authorization code grant).
+     * 
+     * TODO: FORCE client secret
+     * 
+     * @param clientId
+     *            client_id, required
+     * @param clientSecret
+     *            client_secret, required if client_secret was issued
+     *            for the client in client registration.
+     * @param username
+     *            username, required
+     * @param password
+     *            password, required
+     * @param scopes
+     *            authorization scopes, optional
+     * @return an {@link AccessTokenResponse}
+     * @throws KustvaktException
+     */
+    private AccessTokenResponse requestAccessTokenWithPassword (String clientId,
+            String clientSecret, String username, String password,
+            Set<String> scopes) throws KustvaktException {
+
+        OAuth2Client client = clientService.authenticateClient(clientId,
+                clientSecret);
+        if (!client.isSuper()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Password grant is not allowed for third party clients",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+
+        if (scopes == null || scopes.isEmpty()) {
+            scopes = new HashSet<String>(1);
+            scopes.add("all");
+            // scopes = config.getDefaultAccessScopes();
+        }
+
+        ZonedDateTime authenticationTime = authenticateUser(username, password,
+                scopes);
+
+        Set<AccessScope> accessScopes = scopeService
+                .convertToAccessScope(scopes);
+
+        if (config.getOAuth2passwordAuthentication()
+                .equals(AuthenticationMethod.LDAP)) {
+            try {
+                //username = LdapAuth3.getEmail(username, config.getLdapConfig());
+                username = LdapAuth3.getUsername(username,
+                        config.getLdapConfig());
+            }
+            catch (LDAPException e) {
+                throw new KustvaktException(StatusCodes.LDAP_BASE_ERRCODE,
+                        e.getExceptionMessage());
+            }
+        }
+
+        return createsAccessTokenResponse(scopes, accessScopes, clientId,
+                username, authenticationTime, client);
+    }
+
+    /**
+     * Clients must authenticate.
+     * Client credentials grant is limited to native clients.
+     * 
+     * @param clientId
+     *            client_id parameter, required
+     * @param clientSecret
+     *            client_secret parameter, required
+     * @param scopes
+     *            authorization scopes, optional
+     * @return an {@link AccessTokenResponse}
+     * @throws KustvaktException
+     */
+    protected AccessTokenResponse requestAccessTokenWithClientCredentials (
+            String clientId, String clientSecret, Set<String> scopes)
+            throws KustvaktException {
+
+        if (clientSecret == null || clientSecret.isEmpty()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+                    "Missing parameter: client_secret",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        // OAuth2Client client =
+        OAuth2Client oAuth2Client = clientService.authenticateClient(clientId,
+                clientSecret);
+
+        // if (!client.isNative()) {
+        // throw new KustvaktException(
+        // StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+        // "Client credentials grant is not allowed for third party
+        // clients",
+        // OAuth2Error.UNAUTHORIZED_CLIENT);
+        // }
+
+        ZonedDateTime authenticationTime = ZonedDateTime
+                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+
+        scopes = scopeService.filterScopes(scopes,
+                config.getClientCredentialsScopes());
+        Set<AccessScope> accessScopes = scopeService
+                .convertToAccessScope(scopes);
+        return createsAccessTokenResponse(scopes, accessScopes, clientId, null,
+                authenticationTime, oAuth2Client);
+    }
+
+    /**
+     * Creates an OAuth response containing an access token of type
+     * Bearer. By default, MD generator is used to generates access
+     * token of 128 bit values, represented in hexadecimal comprising
+     * 32 bytes. The generated value is subsequently encoded in
+     * Base64.
+     * 
+     * <br /><br />
+     * Additionally, a refresh token is issued for confidential
+     * clients.
+     * It can be used to request a new access token without requiring
+     * user
+     * re-authentication.
+     * 
+     * @param scopes
+     *            a set of access token scopes in String
+     * @param accessScopes
+     *            a set of access token scopes in {@link AccessScope}
+     * @param clientId
+     *            a client id
+     * @param userId
+     *            a user id
+     * @param authenticationTime
+     *            the user authentication time
+     * @param client
+     *            an OAuth2Client
+     * @return an {@link AccessTokenResponse}
+     * @throws KustvaktException
+     */
+    private AccessTokenResponse createsAccessTokenResponse (Set<String> scopes,
+            Set<AccessScope> accessScopes, String clientId, String userId,
+            ZonedDateTime authenticationTime, OAuth2Client client)
+            throws KustvaktException {
+
+        String random = randomGenerator.createRandomCode();
+        random += randomGenerator.createRandomCode();
+
+        if (clientService.isPublicClient(client)) {
+            // refresh token == null, getAccessTokenLongExpiry
+            return createsAccessTokenResponse(null, scopes, accessScopes,
+                    clientId, userId, authenticationTime);
+        }
+        else {
+            // refresh token != null, getAccessTokenExpiry
+            // default refresh token expiry: 365 days in seconds
+            RefreshToken refreshToken = refreshDao.storeRefreshToken(random,
+                    userId, authenticationTime, client, accessScopes);
+            return createsAccessTokenResponse(refreshToken, scopes,
+                    accessScopes, clientId, userId, authenticationTime);
+        }
+    }
+
+    private AccessTokenResponse createsAccessTokenResponse (
+            RefreshToken refreshToken, Set<String> scopes,
+            Set<AccessScope> accessScopes, String clientId, String userId,
+            ZonedDateTime authenticationTime) throws KustvaktException {
+
+        String accessToken = randomGenerator.createRandomCode();
+        accessToken += randomGenerator.createRandomCode();
+        tokenDao.storeAccessToken(accessToken, refreshToken, accessScopes,
+                userId, clientId, authenticationTime);
+
+        Tokens tokens = null;
+        if (refreshToken != null) {
+            BearerAccessToken bearerToken = new BearerAccessToken(accessToken,
+                    (long) config.getAccessTokenExpiry(), Scope.parse(scopes));
+            com.nimbusds.oauth2.sdk.token.RefreshToken rf = new com.nimbusds.oauth2.sdk.token.RefreshToken(
+                    refreshToken.getToken());
+            tokens = new Tokens(bearerToken, rf);
+        }
+        else {
+            BearerAccessToken bearerToken = new BearerAccessToken(accessToken,
+                    (long) config.getAccessTokenLongExpiry(),
+                    Scope.parse(scopes));
+            tokens = new Tokens(bearerToken, null);
+        }
+        return new AccessTokenResponse(tokens);
+    }
+
+    public void revokeToken (String clientId, String clientSecret, String token,
+            String tokenType) throws KustvaktException {
+        clientService.authenticateClient(clientId, clientSecret);
+        if (tokenType != null && tokenType.equals("refresh_token")) {
+            if (!revokeRefreshToken(token)) {
+                revokeAccessToken(token);
+            }
+            return;
+        }
+
+        if (!revokeAccessToken(token)) {
+            revokeRefreshToken(token);
+        }
+    }
+
+    private boolean revokeAccessToken (String token) throws KustvaktException {
+        try {
+            AccessToken accessToken = tokenDao.retrieveAccessToken(token);
+            revokeAccessToken(accessToken);
+            return true;
+        }
+        catch (KustvaktException e) {
+            if (!e.getStatusCode().equals(StatusCodes.INVALID_ACCESS_TOKEN)) {
+                return false;
+            }
+            throw e;
+        }
+    }
+
+    private void revokeAccessToken (AccessToken accessToken)
+            throws KustvaktException {
+        if (accessToken != null) {
+            accessToken.setRevoked(true);
+            tokenDao.updateAccessToken(accessToken);
+        }
+    }
+
+    private boolean revokeRefreshToken (String token) throws KustvaktException {
+        RefreshToken refreshToken = null;
+        try {
+            refreshToken = refreshDao.retrieveRefreshToken(token);
+        }
+        catch (NoResultException e) {
+            return false;
+        }
+
+        return revokeRefreshToken(refreshToken);
+    }
+
+    public boolean revokeRefreshToken (RefreshToken refreshToken)
+            throws KustvaktException {
+        if (refreshToken != null) {
+            refreshToken.setRevoked(true);
+            refreshDao.updateRefreshToken(refreshToken);
+
+            Set<AccessToken> accessTokenList = refreshToken.getAccessTokens();
+            for (AccessToken accessToken : accessTokenList) {
+                accessToken.setRevoked(true);
+                tokenDao.updateAccessToken(accessToken);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public void revokeAllClientTokensViaSuperClient (String username,
+            String superClientId, String superClientSecret, String clientId)
+            throws KustvaktException {
+        OAuth2Client superClient = clientService
+                .authenticateClient(superClientId, superClientSecret);
+        if (!superClient.isSuper()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED);
+        }
+
+        revokeAllClientTokensForUser(clientId, username);
+    }
+
+    public void revokeAllClientTokensForUser (String clientId, String username)
+            throws KustvaktException {
+        OAuth2Client client = clientService.retrieveClient(clientId);
+        if (clientService.isPublicClient(client)) {
+            List<AccessToken> accessTokens = tokenDao
+                    .retrieveAccessTokenByClientId(clientId, username);
+            for (AccessToken t : accessTokens) {
+                revokeAccessToken(t);
+            }
+        }
+        else {
+            List<RefreshToken> refreshTokens = refreshDao
+                    .retrieveRefreshTokenByClientId(clientId, username);
+            for (RefreshToken r : refreshTokens) {
+                revokeRefreshToken(r);
+            }
+        }
+    }
+
+    public void revokeTokensViaSuperClient (String username,
+            String superClientId, String superClientSecret, String token)
+            throws KustvaktException {
+        OAuth2Client superClient = clientService
+                .authenticateClient(superClientId, superClientSecret);
+        if (!superClient.isSuper()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED);
+        }
+
+        RefreshToken refreshToken = refreshDao.retrieveRefreshToken(token,
+                username);
+        if (!revokeRefreshToken(refreshToken)) {
+            AccessToken accessToken = tokenDao.retrieveAccessToken(token,
+                    username);
+            revokeAccessToken(accessToken);
+        }
+    }
+
+    public List<OAuth2TokenDto> listUserRefreshToken (String username,
+            String superClientId, String superClientSecret, String clientId)
+            throws KustvaktException {
+
+        OAuth2Client client = clientService.authenticateClient(superClientId,
+                superClientSecret);
+        if (!client.isSuper()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Only super client is allowed.",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+
+        List<RefreshToken> tokens = refreshDao
+                .retrieveRefreshTokenByUser(username, clientId);
+        List<OAuth2TokenDto> dtoList = new ArrayList<>(tokens.size());
+        for (RefreshToken t : tokens) {
+            OAuth2Client tokenClient = t.getClient();
+            if (tokenClient.getId().equals(client.getId())) {
+                continue;
+            }
+            OAuth2TokenDto dto = new OAuth2TokenDto();
+            dto.setClientId(tokenClient.getId());
+            dto.setClientName(tokenClient.getName());
+            dto.setClientUrl(tokenClient.getUrl());
+            dto.setClientDescription(tokenClient.getDescription());
+
+            DateTimeFormatter f = DateTimeFormatter.ISO_DATE_TIME;
+            dto.setCreatedDate(t.getCreatedDate().format(f));
+            long difference = ChronoUnit.SECONDS.between(ZonedDateTime.now(),
+                    t.getExpiryDate());
+            dto.setExpiresIn(difference);
+
+            dto.setUserAuthenticationTime(
+                    t.getUserAuthenticationTime().format(f));
+            dto.setToken(t.getToken());
+
+            Set<AccessScope> accessScopes = t.getScopes();
+            Set<String> scopes = new HashSet<>(accessScopes.size());
+            for (AccessScope s : accessScopes) {
+                scopes.add(s.getId().toString());
+            }
+            dto.setScope(scopes);
+            dtoList.add(dto);
+        }
+        return dtoList;
+    }
+
+    public List<OAuth2TokenDto> listUserAccessToken (String username,
+            String superClientId, String superClientSecret, String clientId)
+            throws KustvaktException {
+
+        OAuth2Client superClient = clientService
+                .authenticateClient(superClientId, superClientSecret);
+        if (!superClient.isSuper()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Only super client is allowed.",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+
+        List<AccessToken> tokens = tokenDao.retrieveAccessTokenByUser(username,
+                clientId);
+        List<OAuth2TokenDto> dtoList = new ArrayList<>(tokens.size());
+        for (AccessToken t : tokens) {
+            OAuth2Client tokenClient = t.getClient();
+            if (tokenClient.getId().equals(superClient.getId())) {
+                continue;
+            }
+            OAuth2TokenDto dto = new OAuth2TokenDto();
+            dto.setClientId(tokenClient.getId());
+            dto.setClientName(tokenClient.getName());
+            dto.setClientUrl(tokenClient.getUrl());
+            dto.setClientDescription(tokenClient.getDescription());
+
+            DateTimeFormatter f = DateTimeFormatter.ISO_DATE_TIME;
+            dto.setCreatedDate(t.getCreatedDate().format(f));
+
+            long difference = ChronoUnit.SECONDS.between(ZonedDateTime.now(),
+                    t.getExpiryDate());
+            dto.setExpiresIn(difference);
+
+            dto.setUserAuthenticationTime(
+                    t.getUserAuthenticationTime().format(f));
+            dto.setToken(t.getToken());
+
+            Set<AccessScope> accessScopes = t.getScopes();
+            Set<String> scopes = new HashSet<>(accessScopes.size());
+            for (AccessScope s : accessScopes) {
+                scopes.add(s.getId().toString());
+            }
+            dto.setScope(scopes);
+            dtoList.add(dto);
+        }
+        return dtoList;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/CollectionCleanRewrite.java b/src/main/java/de/ids_mannheim/korap/rewrite/CollectionCleanRewrite.java
new file mode 100644
index 0000000..f827871
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/CollectionCleanRewrite.java
@@ -0,0 +1,72 @@
+package de.ids_mannheim.korap.rewrite;
+
+import java.util.Iterator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.user.User;
+import edu.emory.mathcs.backport.java.util.Arrays;
+
+/**
+ * EM: not used anymore. This rewrite was to remove an empty koral:doc
+ * group in operands.
+ * 
+ * @author hanl
+ * @date 28/07/2015
+ */
+public class CollectionCleanRewrite implements RewriteTask.RewriteNodeAt {
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) {
+        JsonNode jsonNode = process(node.rawNode());
+        return node.wrapNode(jsonNode);
+    }
+
+    private JsonNode process (JsonNode root) {
+        JsonNode sub = root;
+        if (root.isObject()) {
+            if (root.has("operands")) {
+                JsonNode node = root.at("/operands");
+                Iterator<JsonNode> it = node.elements();
+                while (it.hasNext()) {
+                    JsonNode n = it.next();
+                    JsonNode s = process(n);
+                    if (s == null)
+                        it.remove();
+                }
+
+                int count = node.size();
+                // remove group element and replace with single doc
+                if (count == 1)
+                    sub = node.path(0);
+                // indicate empty group
+                else if (count == 0) // can't do anything here -- fixme: edge case?!
+                    return null;
+            }
+
+            // what happens to array nodes?
+            if (!root.equals(sub)) {
+                if (sub.isObject()) {
+                    ObjectNode ob = (ObjectNode) root;
+                    ob.remove(Arrays.asList(
+                            new String[] { "@type", "operation", "operands" }));
+                    ob.putAll((ObjectNode) sub);
+                }
+            }
+        }
+        return root;
+    }
+
+    @Override
+    public JsonNode rewriteResult (KoralNode node) {
+        return null;
+    }
+
+    @Override
+    public String at () {
+        return "/collection";
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/CollectionConstraint.java b/src/main/java/de/ids_mannheim/korap/rewrite/CollectionConstraint.java
new file mode 100644
index 0000000..d2d3610
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/CollectionConstraint.java
@@ -0,0 +1,66 @@
+package de.ids_mannheim.korap.rewrite;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * EM: not used anymore. This rewrite was meant to remove doc from
+ * a collection by checking user access to the doc.
+ * 
+ * @author hanl
+ * @date 03/07/2015
+ */
+public class CollectionConstraint implements RewriteTask.IterableRewritePath {
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) {
+        if (node.get("@type").equals("koral:doc")) {
+            if (node.get("key").equals(Attributes.CORPUS_SIGLE)) {
+                String id = node.get("value");
+                // EM: MH checks if user has access to corpus
+                //                KustvaktResource corpus = check(id, user);
+                //                if (corpus == null)
+                node.removeNode(new KoralNode.RewriteIdentifier(
+                        Attributes.CORPUS_SIGLE, id));
+            }
+        }
+        return node;
+    }
+
+    /**
+     * @param id
+     * @param user
+     * @return boolean if true access granted
+     */
+    //    @Deprecated
+    //    private KustvaktResource check (String id, User user) {
+    //        // todo: can be used to circumvent access control if public filter not applied
+    //        if (user == null)
+    //            return null;
+    //
+    //        KustvaktResource corpus;
+    //        try {
+    //            SecurityManager m = SecurityManager
+    //                    .findbyId(id, user, Corpus.class);
+    //            corpus = m.getResource();
+    //        }
+    //        catch (RuntimeException | KustvaktException e) {
+    //            return null;
+    //        }
+    //        return corpus;
+    //    }
+
+    @Override
+    public JsonNode rewriteResult (KoralNode node) {
+        return null;
+    }
+
+    @Override
+    public String path () {
+        return "collection";
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/CollectionRewrite.java b/src/main/java/de/ids_mannheim/korap/rewrite/CollectionRewrite.java
new file mode 100644
index 0000000..0c3ee07
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/CollectionRewrite.java
@@ -0,0 +1,231 @@
+package de.ids_mannheim.korap.rewrite;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Lists;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.query.object.KoralMatchOperator;
+import de.ids_mannheim.korap.query.object.KoralOperation;
+import de.ids_mannheim.korap.rewrite.KoralNode.RewriteIdentifier;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.utils.KoralCollectionQueryBuilder;
+
+/**
+ * CollectionRewrite determines which availability field values are
+ * possible for a user with respect to his mean and location of
+ * access.
+ * 
+ * <br/><br/>
+ * KorAP differentiates 3 kinds of access:
+ * <ul>
+ * <li>FREE: without login</li>
+ * <li>PUB: login outside IDS network</li>
+ * <li>ALL: login within IDS network</li>
+ * </ul>
+ * 
+ * Each of these accesses corresponds to a regular expression of
+ * license
+ * formats defined in kustvakt.conf. For a given access, only those
+ * resources whose availability field matches its regular expression
+ * are allowed to be retrieved.
+ * 
+ * 
+ * @author margaretha
+ * @last-update 21 Nov 2017
+ * @see CorpusAccess
+ */
+public class CollectionRewrite implements RewriteTask.RewriteQuery {
+
+    public static Logger jlog = LogManager.getLogger(CollectionRewrite.class);
+
+    public static boolean DEBUG = false;
+
+    public CollectionRewrite () {
+        super();
+    }
+
+    private List<String> checkAvailability (JsonNode node,
+            List<String> originalAvailabilities,
+            List<String> updatedAvailabilities, boolean isOperationOr) {
+        if (DEBUG) {
+            try {
+                jlog.debug(JsonUtils.toJSON(node));
+            }
+            catch (KustvaktException e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (node.has("operands")) {
+            ArrayList<JsonNode> operands = Lists
+                    .newArrayList(node.at("/operands").elements());
+
+            if (node.at("/operation").asText()
+                    .equals(KoralOperation.AND.toString())) {
+                for (int i = 0; i < operands.size(); i++) {
+                    updatedAvailabilities = checkAvailability(operands.get(i),
+                            originalAvailabilities, updatedAvailabilities,
+                            false);
+                    if (updatedAvailabilities.isEmpty())
+                        break;
+                }
+            }
+            else {
+                for (int i = 0; i < operands.size(); i++) {
+                    node = operands.get(i);
+                    if (node.has("key") && !node.at("/key").asText()
+                            .equals("availability")) {
+                        jlog.debug("RESET availabilities 1, key="
+                                + node.at("/key").asText());
+                        updatedAvailabilities.clear();
+                        updatedAvailabilities.addAll(originalAvailabilities);
+                        break;
+                    }
+                    else {
+                        updatedAvailabilities = checkAvailability(
+                                operands.get(i), originalAvailabilities,
+                                updatedAvailabilities, true);
+                    }
+                }
+            }
+        }
+        else if (node.has("key")
+                && node.at("/key").asText().equals("availability")) {
+            String queryAvailability = node.at("/value").asText();
+            String matchOp = node.at("/match").asText();
+
+            if (originalAvailabilities.contains(queryAvailability)
+                    && matchOp.equals(KoralMatchOperator.EQUALS.toString())) {
+                if (DEBUG) {
+                    jlog.debug("REMOVE " + queryAvailability);
+                }
+                updatedAvailabilities.remove(queryAvailability);
+            }
+            else if (isOperationOr) {
+                if (DEBUG) {
+                    jlog.debug("RESET availabilities 2");
+                }
+                updatedAvailabilities.clear();
+                updatedAvailabilities.addAll(originalAvailabilities);
+                return updatedAvailabilities;
+            }
+        }
+        return updatedAvailabilities;
+    }
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) throws KustvaktException {
+        JsonNode jsonNode = node.rawNode();
+
+        FullConfiguration fullConfig = (FullConfiguration) config;
+
+        List<String> userAvailabilities = new ArrayList<String>();
+        switch (user.getCorpusAccess()) {
+            case PUB:
+                userAvailabilities.addAll(fullConfig.getFreeRegexList());
+                userAvailabilities.addAll(fullConfig.getPublicRegexList());
+                break;
+            case ALL:
+                userAvailabilities.addAll(fullConfig.getFreeRegexList());
+                userAvailabilities.addAll(fullConfig.getPublicRegexList());
+                userAvailabilities.addAll(fullConfig.getAllRegexList());
+                break;
+            case FREE:
+                userAvailabilities.addAll(fullConfig.getFreeRegexList());
+                break;
+        }
+
+        KoralCollectionQueryBuilder builder = new KoralCollectionQueryBuilder();
+        RewriteIdentifier identifier = new KoralNode.RewriteIdentifier(
+                Attributes.AVAILABILITY, user.getCorpusAccess());
+        JsonNode rewrittenNode;
+
+        if (jsonNode.has("collection")) {
+            List<String> avalabilityCopy = new ArrayList<String>(
+                    userAvailabilities.size());
+            avalabilityCopy.addAll(userAvailabilities);
+            if (DEBUG) {
+                jlog.debug("Availabilities: "
+                        + Arrays.toString(userAvailabilities.toArray()));
+            }
+
+            userAvailabilities = checkAvailability(jsonNode.at("/collection"),
+                    avalabilityCopy, userAvailabilities, false);
+            if (!userAvailabilities.isEmpty()) {
+                builder.with(buildAvailability(avalabilityCopy));
+                if (DEBUG) {
+                    jlog.debug("corpus query: " + builder.toString());
+                }
+                builder.setBaseQuery(builder.toJSON());
+                rewrittenNode = builder.mergeWith(jsonNode).at("/collection");
+                node.set("collection", rewrittenNode, identifier);
+            }
+        }
+        else {
+            builder.with(buildAvailability(userAvailabilities));
+            if (DEBUG) {
+                jlog.debug("corpus query: " + builder.toString());
+            }
+            rewrittenNode = JsonUtils.readTree(builder.toJSON())
+                    .at("/collection");
+            node.set("collection", rewrittenNode, identifier);
+        }
+
+        node = node.at("/collection");
+        if (DEBUG) {
+            jlog.debug("REWRITES: " + node.toString());
+        }
+
+        return node;
+    }
+
+    private String buildAvailability (List<String> userAvailabilities) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < userAvailabilities.size(); i++) {
+            parseAvailability(sb, userAvailabilities.get(i), "|");
+        }
+        String availabilities = sb.toString();
+        return availabilities.substring(0, availabilities.length() - 3);
+    }
+
+    private void parseAvailability (StringBuilder sb, String availability,
+            String operator) {
+        String uaArr[] = null;
+        if (availability.contains("|")) {
+            uaArr = availability.split("\\|");
+            for (int j = 0; j < uaArr.length; j++) {
+                parseAvailability(sb, uaArr[j].trim(), "|");
+            }
+        }
+        // EM: not supported
+        //        else if (availability.contains("&")){
+        //            uaArr = availability.split("&");
+        //            for (int j=0; j < uaArr.length -1; j++){
+        //                parseAvailability(sb, uaArr[j], "&");
+        //            }
+        //            parseAvailability(sb, uaArr[uaArr.length-1], "|");
+        //        } 
+        else {
+            sb.append("availability=/");
+            sb.append(availability);
+            sb.append("/ ");
+            sb.append(operator);
+            sb.append(" ");
+        }
+
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/FoundryInject.java b/src/main/java/de/ids_mannheim/korap/rewrite/FoundryInject.java
new file mode 100644
index 0000000..73615e0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/FoundryInject.java
@@ -0,0 +1,63 @@
+package de.ids_mannheim.korap.rewrite;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.rewrite.KoralNode.RewriteIdentifier;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.UserSettingProcessor;
+
+/**
+ * @author hanl, margaretha
+ * @date 30/06/2015
+ */
+public class FoundryInject implements RewriteTask.IterableRewritePath {
+
+    @Autowired
+    protected LayerMapper mapper;
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) throws KustvaktException {
+
+        if (node.get("@type").equals("koral:span")) {
+            if (!node.isMissingNode("/wrap")) {
+                node = node.at("/wrap");
+                JsonNode term = rewriteQuery(node, config, user).rawNode();
+                node.replaceAt("/wrap", term,
+                        new RewriteIdentifier("koral:term", "replace"));
+            }
+        }
+        else if (node.get("@type").equals("koral:term")
+                && !node.has("foundry")) {
+            String layer;
+            if (node.has("layer")) {
+                layer = node.get("layer");
+            }
+            else {
+                layer = node.get("key");
+            }
+            UserSettingProcessor settingProcessor = null;
+            if (user != null) {
+                settingProcessor = user.getUserSettingProcessor();
+            }
+            String foundry = mapper.findFoundry(layer, settingProcessor);
+            if (foundry != null)
+                node.put("foundry", foundry);
+        }
+        return node;
+    }
+
+    @Override
+    public String path () {
+        return "query";
+    }
+
+    @Override
+    public JsonNode rewriteResult (KoralNode node) {
+        return null;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/FoundryRewrite.java b/src/main/java/de/ids_mannheim/korap/rewrite/FoundryRewrite.java
new file mode 100644
index 0000000..c59097c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/FoundryRewrite.java
@@ -0,0 +1,32 @@
+package de.ids_mannheim.korap.rewrite;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.service.DefaultSettingService;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.UserSettingProcessor;
+
+/**
+ * @author margaretha
+ *
+ */
+public class FoundryRewrite extends FoundryInject {
+
+    @Autowired
+    private DefaultSettingService settingService;
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) throws KustvaktException {
+        String username = user.getUsername();
+        String jsonSettings = settingService.retrieveDefaultSettings(username);
+        if (jsonSettings != null) {
+            UserSettingProcessor processor = new UserSettingProcessor(
+                    jsonSettings);
+            user.setUserSettingProcessor(processor);
+        }
+        return super.rewriteQuery(node, config, user);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/IdWriter.java b/src/main/java/de/ids_mannheim/korap/rewrite/IdWriter.java
new file mode 100644
index 0000000..113c176
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/IdWriter.java
@@ -0,0 +1,38 @@
+package de.ids_mannheim.korap.rewrite;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.config.BeanInjectable;
+import de.ids_mannheim.korap.config.ContextHolder;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * @author hanl
+ * @date 25/09/2015
+ */
+public class IdWriter implements RewriteTask.RewriteKoralToken {
+
+    private int counter;
+
+    public IdWriter () {
+        this.counter = 0;
+    }
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) {
+        if (node.get("@type").equals("koral:token")) {
+            String s = extractToken(node.rawNode());
+            if (s != null && !s.isEmpty())
+                node.put("idn", s + "_" + counter++);
+        }
+        return node;
+    }
+
+    private String extractToken (JsonNode token) {
+        JsonNode wrap = token.path("wrap");
+        if (!wrap.isMissingNode())
+            return wrap.path("key").asText();
+        return null;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/KoralNode.java b/src/main/java/de/ids_mannheim/korap/rewrite/KoralNode.java
new file mode 100644
index 0000000..f05f864
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/KoralNode.java
@@ -0,0 +1,272 @@
+package de.ids_mannheim.korap.rewrite;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+import java.util.*;
+
+/**
+ * @author hanl
+ * @date 04/07/2015
+ */
+public class KoralNode {
+    private JsonNode node;
+    private KoralRewriteBuilder rewrites;
+    private boolean remove;
+
+    public KoralNode (JsonNode node) {
+        this.node = node;
+        this.rewrites = new KoralRewriteBuilder();
+        this.remove = false;
+    }
+
+    public KoralNode (JsonNode node, KoralRewriteBuilder rewrites) {
+        this.node = node;
+        this.rewrites = rewrites;
+        this.remove = false;
+    }
+
+    public static KoralNode wrapNode (JsonNode node) {
+        return new KoralNode(node);
+    }
+
+    public void buildRewrites (JsonNode node) {
+        this.rewrites.build(node);
+    }
+
+    public void buildRewrites () {
+        this.rewrites.build(this.node);
+    }
+
+    @Override
+    public String toString () {
+        return this.node.toString();
+    }
+
+    public void put (String name, Object value) {
+        if (this.node.isObject() && this.node.path(name).isMissingNode()) {
+            ObjectNode node = (ObjectNode) this.node;
+            if (value instanceof String)
+                node.put(name, (String) value);
+            else if (value instanceof Integer)
+                node.put(name, (Integer) value);
+            else if (value instanceof JsonNode)
+                node.put(name, (JsonNode) value);
+            this.rewrites.add("injection", name);
+        }
+        else
+            throw new UnsupportedOperationException(
+                    "node doesn't support this operation");
+    }
+
+    public void remove (Object identifier, RewriteIdentifier ident) {
+        boolean set = false;
+        if (this.node.isObject() && identifier instanceof String) {
+            ObjectNode n = (ObjectNode) this.node;
+            n.remove((String) identifier);
+            set = true;
+        }
+        else if (this.node.isArray() && identifier instanceof Integer) {
+            ArrayNode n = (ArrayNode) this.node;
+            n.remove((Integer) identifier);
+            set = true;
+        }
+
+        if (ident != null)
+            identifier = ident.toString();
+
+        if (set) {
+            this.rewrites.add("deletion", identifier);
+        }
+    }
+
+    public void replace (String name, Object value, RewriteIdentifier ident) {
+        if (this.node.isObject() && this.node.has(name)) {
+            ObjectNode n = (ObjectNode) this.node;
+            if (value instanceof String)
+                n.put(name, (String) value);
+            else if (value instanceof Integer)
+                n.put(name, (Integer) value);
+            else if (value instanceof JsonNode)
+                n.put(name, (JsonNode) value);
+
+            if (ident != null)
+                name = ident.toString();
+
+            this.rewrites.add("override", name);
+        }
+    }
+
+    public void replaceAt (String path, Object value, RewriteIdentifier ident) {
+        if (this.node.isObject() && !this.node.at(path).isMissingNode()) {
+            ObjectNode n = (ObjectNode) this.node.at(path);
+            n.removeAll();
+            n.putAll((ObjectNode) value);
+
+            String name = path;
+            if (ident != null)
+                name = ident.toString();
+
+            this.rewrites.add("override", name);
+        }
+    }
+
+    public void set (String name, Object value, RewriteIdentifier ident) {
+        if (this.node.isObject()) {
+            ObjectNode n = (ObjectNode) this.node;
+            if (value instanceof String)
+                n.put(name, (String) value);
+            else if (value instanceof Integer)
+                n.put(name, (Integer) value);
+            else if (value instanceof JsonNode)
+                n.put(name, (JsonNode) value);
+
+            if (ident != null)
+                name = ident.toString();
+
+            this.rewrites.add("insertion", name);
+        }
+    }
+
+    public void setAll (ObjectNode other) {
+        if (this.node.isObject()) {
+            ObjectNode n = (ObjectNode) this.node;
+            n.setAll(other);
+        }
+        this.rewrites.add("insertion", null);
+    }
+
+    public String get (String name) {
+        if (this.node.isObject())
+            return this.node.path(name).asText();
+        return null;
+    }
+
+    public KoralNode at (String name) {
+        //        this.node = this.node.at(name);
+        //        return this;
+        return new KoralNode(this.node.at(name), this.rewrites);
+    }
+
+    public boolean has (Object ident) {
+        if (ident instanceof String)
+            return this.node.has((String) ident);
+        else if (ident instanceof Integer)
+            return this.node.has((int) ident);
+        return false;
+    }
+
+    public JsonNode rawNode () {
+        return this.node;
+    }
+
+    public void removeNode (RewriteIdentifier ident) {
+        this.rewrites.add("deletion", ident.toString());
+        this.remove = true;
+    }
+
+    public static class RewriteIdentifier {
+
+        private String key, value;
+
+        public RewriteIdentifier (String key, Object value) {
+            this.key = key;
+            this.value = value.toString();
+        }
+
+        @Override
+        public String toString () {
+            return key + "(" + value + ")";
+        }
+
+    }
+
+    public boolean isRemove () {
+        return this.remove;
+    }
+
+    public static class KoralRewriteBuilder {
+
+        private List<KoralRewrite> rewrites;
+
+        public KoralRewriteBuilder () {
+            this.rewrites = new ArrayList<>();
+        }
+
+        public KoralRewriteBuilder add (String op, Object scope) {
+            KoralRewrite rewrite = new KoralRewrite();
+            rewrite.setOperation(op);
+            if (scope != null) {
+                rewrite.setScope(scope.toString());
+            }
+            this.rewrites.add(rewrite);
+            return this;
+        }
+
+        public JsonNode build (JsonNode node) {
+            for (KoralRewrite rewrite : this.rewrites) {
+                if (rewrite.map.get("operation") == null)
+                    throw new UnsupportedOperationException(
+                            "operation not set properly");
+
+                if (node.has("rewrites")) {
+                    ArrayNode n = (ArrayNode) node.path("rewrites");
+                    n.add(JsonUtils.valueToTree(rewrite.map));
+                }
+                else if (node.isObject()) {
+                    ObjectNode n = (ObjectNode) node;
+                    List l = new LinkedList<>();
+                    l.add(JsonUtils.valueToTree(rewrite.map));
+                    n.put("rewrites", JsonUtils.valueToTree(l));
+                }
+                else {
+                    //fixme: matches in result will land here. rewrites need to be placed under root node - though then there might be unclear where they belong to
+                }
+
+            }
+            this.rewrites.clear();
+            return node;
+        }
+
+    }
+
+    private static class KoralRewrite {
+
+        private Map<String, String> map;
+
+        private KoralRewrite () {
+            this.map = new LinkedHashMap<>();
+            this.map.put("@type", "koral:rewrite");
+            this.map.put("src", "Kustvakt");
+        }
+
+        public KoralRewrite setOperation (String op) {
+            if (!op.startsWith("operation:"))
+                op = "operation:" + op;
+            this.map.put("operation", op);
+            return this;
+        }
+
+        public KoralRewrite setScope (String scope) {
+            this.map.put("scope", scope);
+            return this;
+        }
+
+    }
+
+    public boolean isMissingNode (String string) {
+        return this.node.at(string).isMissingNode();
+    }
+
+    public int size () {
+        return this.node.size();
+    }
+
+    public KoralNode get (int i) {
+        //        this.node = this.node.get(i);
+        return this.wrapNode(this.node.get(i));
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/LayerMapper.java b/src/main/java/de/ids_mannheim/korap/rewrite/LayerMapper.java
new file mode 100644
index 0000000..59eb614
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/LayerMapper.java
@@ -0,0 +1,124 @@
+package de.ids_mannheim.korap.rewrite;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.user.UserSettingProcessor;
+
+/**
+ * EM:
+ * <ul>
+ * <li> Added default morphology foundry </li>
+ * <li> Made this class as a spring component</li>
+ * </ul>
+ * 
+ * @author hanl, margaretha
+ * @date 14/10/2014
+ */
+@Component
+public class LayerMapper {
+
+    @Autowired
+    private KustvaktConfiguration config;
+
+    public String findFoundry (String layer) {
+        return findFoundry(layer, null);
+    }
+
+    /**
+     * find foundry entry in settings specific settings. Includes a
+     * call to #translateLayer to get the
+     * correct mapping for the layer denomination!
+     * 
+     * @param layer
+     * @return
+     */
+
+    //todo: make mapping configurable!
+    public String findFoundry (String layer, UserSettingProcessor settings) {
+        if (settings != null) {
+            switch (translateLayer(layer.toLowerCase().trim())) {
+                case "d":
+                    return (String) settings
+                            .get(Attributes.DEFAULT_FOUNDRY_RELATION);
+                case "c":
+                    return (String) settings
+                            .get(Attributes.DEFAULT_FOUNDRY_CONSTITUENT);
+                case "pos":
+                    return (String) settings
+                            .get(Attributes.DEFAULT_FOUNDRY_POS);
+                case "lemma":
+                    return (String) settings
+                            .get(Attributes.DEFAULT_FOUNDRY_LEMMA);
+                case "surface":
+                    return "opennlp";
+                // EM: added
+                case "s":
+                    return (String) settings
+                            .get(Attributes.DEFAULT_FOUNDRY_STRUCTURE);
+                case "morphology":
+                    return (String) settings
+                            .get(Attributes.DEFAULT_FOUNDRY_MORPHOLOGY);
+                default:
+                    // if the layer is not in this specific listing, assume a default layer
+                    // like orth or other tokenization layers
+                    return null;
+            }
+        }
+        else {
+            switch (translateLayer(layer.toLowerCase().trim())) {
+                case "d":
+                    return config.getDefault_dep();
+                case "c":
+                    return config.getDefault_const();
+                case "pos":
+                    return config.getDefault_pos();
+                case "lemma":
+                    return config.getDefault_lemma();
+                case "surface":
+                    return config.getDefault_orthography();
+                // refers to "structure" and is used for paragraphs or sentence boundaries
+                case "s":
+                    return config.getDefaultStructureFoundry();
+                //EM: added
+                case "morphology":
+                    return config.getDefault_morphology();
+                default:
+                    // if the layer is not in this specific listing, assume a default layer
+                    // like orth or other tokenization layers
+                    return null;
+            }
+        }
+    }
+
+    // relevance: map to access control id references. p is usually mapped to pos, l to lemma, etc.
+    public String translateLayer (String layer) {
+        switch (layer.toLowerCase().trim()) {
+            //            case "pos":
+            //                return "p";
+            //            case "lemma":
+            //                return "l";
+            case "m":
+                return "morphology"; // EM: changed msd to morphology
+            //                return "msd";
+            //todo the orth layer does not need a foundry entry
+            case "orth":
+                return "surface";
+            // EM: layer t does not exist at all   
+            //            case "t":
+            //                return "surface";
+            // EM: this islegacy support    
+            case "const":
+                return "c";
+            case "p":
+                return "pos";
+            case "l":
+                return "lemma";
+            default:
+                return layer;
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/MetaConstraint.java b/src/main/java/de/ids_mannheim/korap/rewrite/MetaConstraint.java
new file mode 100644
index 0000000..2838f67
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/MetaConstraint.java
@@ -0,0 +1,36 @@
+package de.ids_mannheim.korap.rewrite;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.config.BeanInjectable;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * @author hanl
+ * @date 04/07/2015
+ */
+public class MetaConstraint implements RewriteTask.RewriteNodeAt {
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) {
+        // redundant
+        if (node.rawNode().has("meta")) {
+            JsonNode meta = node.rawNode().path("meta");
+            //todo: check meta parameter
+            System.out.println("HAVE TO CHECK THE META ENTRIES");
+        }
+        return node;
+    }
+
+    @Override
+    public JsonNode rewriteResult (KoralNode node) {
+        return null;
+    }
+
+    @Override
+    public String at () {
+        return "/meta";
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java b/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java
new file mode 100644
index 0000000..9521c72
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/QueryReferenceRewrite.java
@@ -0,0 +1,101 @@
+package de.ids_mannheim.korap.rewrite;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.rewrite.KoralNode.RewriteIdentifier;
+import de.ids_mannheim.korap.service.QueryService;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * Rewrites query reference with the corresponding koral
+ * query describing the actual query fragment.
+ * 
+ * Based on VirtualCorpusRewrite.
+ *
+ * @author diewald, margaretha
+ *
+ */
+@Component
+public class QueryReferenceRewrite implements RewriteTask.RewriteQuery {
+
+    @Autowired
+    private QueryService service;
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) throws KustvaktException {
+        if (node.has("query")) {
+            node = node.at("/query");
+            findQueryRef(user.getUsername(), node);
+        }
+        return node;
+    }
+
+    private void findQueryRef (String username, KoralNode koralNode)
+            throws KustvaktException {
+        if (koralNode.has("@type")
+                && koralNode.get("@type").equals("koral:queryRef")) {
+            if (!koralNode.has("ref")) {
+                throw new KustvaktException(
+                        de.ids_mannheim.korap.util.StatusCodes.MISSING_QUERY_REFERENCE,
+                        "ref is not found");
+            }
+            else {
+                String queryRefName = koralNode.get("ref");
+                String queryRefOwner = "system";
+                if (queryRefName.contains("/")) {
+                    String[] names = queryRefName.split("/");
+                    if (names.length == 2) {
+                        queryRefOwner = names[0];
+                        queryRefName = names[1];
+                    }
+                }
+
+                QueryDO qr = service.searchQueryByName(username, queryRefName,
+                        queryRefOwner, QueryType.QUERY);
+
+                if (qr == null) {
+                    throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                            "Query reference " + queryRefName
+                                    + " is not found.",
+                            String.valueOf(queryRefName));
+                }
+
+                // TODO:
+                //   checkVCAcess(q, username);
+                JsonNode qref = JsonUtils.readTree(qr.getKoralQuery());;
+                rewriteQuery(qref, koralNode);
+            }
+        }
+
+        else if (koralNode.has("operands")) {
+            KoralNode operands = koralNode.at("/operands");
+
+            for (int i = 0; i < operands.size(); i++) {
+                KoralNode operand = operands.get(i);
+                this.findQueryRef(username, operand);
+                operand.buildRewrites();
+            }
+        }
+    }
+
+    private void rewriteQuery (JsonNode qref, KoralNode koralNode)
+            throws KustvaktException {
+        JsonNode jsonNode = koralNode.rawNode();
+        koralNode.remove("@type",
+                new RewriteIdentifier("@type", jsonNode.at("/@type").asText()));
+        koralNode.remove("ref",
+                new RewriteIdentifier("ref", jsonNode.at("/ref").asText()));
+        koralNode.setAll((ObjectNode) qref);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/RewriteHandler.java b/src/main/java/de/ids_mannheim/korap/rewrite/RewriteHandler.java
new file mode 100644
index 0000000..6bad856
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/RewriteHandler.java
@@ -0,0 +1,293 @@
+package de.ids_mannheim.korap.rewrite;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * @author hanl
+ * @date 30/06/2015
+ */
+// todo: do post processing!
+//todo: load rewritenode and rewritequery automatically from classpath by default, but namespaced from package
+public class RewriteHandler {
+    //implements BeanInjectable {
+
+    private static Logger jlog = LogManager.getLogger(RewriteHandler.class);
+    private Collection<RewriteTask.IterableRewritePath> node_processors;
+    private Collection<RewriteTask.RewriteKoralToken> token_node_processors;
+    private Collection<RewriteTask> query_processors;
+
+    private Set<Class> failed_task_registration;
+    @Autowired
+    private KustvaktConfiguration config;
+
+    public RewriteHandler (List<RewriteTask> rewriteTasks) {
+        this();
+        for (RewriteTask t : rewriteTasks) {
+            addProcessor(t);
+        }
+    }
+
+    // EM: for testing
+    public RewriteHandler (KustvaktConfiguration config) {
+        this();
+        this.config = config;
+    }
+
+    public RewriteHandler () {
+        this.node_processors = new HashSet<>();
+        this.token_node_processors = new HashSet<>();
+        this.query_processors = new LinkedHashSet<>();
+        this.failed_task_registration = new HashSet<>();
+    }
+
+    public Set getFailedProcessors () {
+        return this.failed_task_registration;
+    }
+
+    public boolean addProcessor (RewriteTask rewriter) {
+        if (rewriter instanceof RewriteTask.RewriteKoralToken)
+            return this.token_node_processors
+                    .add((RewriteTask.RewriteKoralToken) rewriter);
+        else if (rewriter instanceof RewriteTask.IterableRewritePath)
+            return this.node_processors
+                    .add((RewriteTask.IterableRewritePath) rewriter);
+        else if (rewriter instanceof RewriteTask.RewriteQuery
+                | rewriter instanceof RewriteTask.RewriteResult)
+            return this.query_processors.add(rewriter);
+
+        this.failed_task_registration.add(rewriter.getClass());
+        return false;
+    }
+
+    @Override
+    public String toString () {
+        StringBuilder b = new StringBuilder();
+        b.append("--------------------------");
+        b.append("pre/post: " + this.node_processors.toString()).append("\n")
+                .append("\n")
+                .append("query: " + this.query_processors.toString())
+                .append("\n")
+                .append("koraltoken: " + this.token_node_processors.toString());
+        b.append("---------------------------");
+        return b.toString();
+    }
+
+    /**
+     * expects extended RewriteNode/Query class with empty default
+     * constructor
+     * 
+     * @param rewriter
+     * @return boolean if rewriter class was successfully added to
+     *         rewrite handler!
+     */
+    @Deprecated
+    public boolean add (Class<? extends RewriteTask> rewriter) {
+        RewriteTask task;
+        try {
+            Constructor c = rewriter.getConstructor();
+            task = (RewriteTask) c.newInstance();
+        }
+        catch (NoSuchMethodException | InvocationTargetException
+                | IllegalAccessException | InstantiationException e) {
+            this.failed_task_registration.add(rewriter);
+            return false;
+        }
+        return addProcessor(task);
+    }
+
+    public String processQuery (JsonNode root, User user)
+            throws KustvaktException {
+        RewriteProcess process = new RewriteProcess(root, user);
+        JsonNode pre = process.start(false);
+        return JsonUtils.toJSON(pre);
+    }
+
+    public String processQuery (String json, User user)
+            throws KustvaktException {
+        return processQuery(JsonUtils.readTree(json), user);
+    }
+
+    public String processResult (String json, User user)
+            throws KustvaktException {
+        return processResult(JsonUtils.readTree(json), user);
+    }
+
+    public String processResult (JsonNode node, User user)
+            throws KustvaktException {
+        RewriteProcess process = new RewriteProcess(node, user);
+        JsonNode pre = process.start(true);
+        return JsonUtils.toJSON(pre);
+    }
+
+    public void clear () {
+        this.node_processors.clear();
+        this.query_processors.clear();
+        this.token_node_processors.clear();
+    }
+
+    //    public <T extends ContextHolder> void insertBeans (T beans) {
+    //        this.beans = beans;
+    //        this.config = beans.getConfiguration();
+    //    }
+
+    public class RewriteProcess {
+
+        private static final boolean DEBUG = false;
+        private JsonNode root;
+        private User user;
+
+        private RewriteProcess (JsonNode root, User user) {
+            this.root = root;
+            this.user = user;
+        }
+
+        private KoralNode processNode (String key, JsonNode value,
+                boolean result) throws KustvaktException {
+            KoralNode kroot = KoralNode.wrapNode(value);
+            if (value.isObject()) {
+                if (value.has("operands")) {
+                    JsonNode ops = value.at("/operands");
+                    Iterator<JsonNode> it = ops.elements();
+                    while (it.hasNext()) {
+                        JsonNode next = it.next();
+                        KoralNode kn = processNode(key, next, result);
+                        if (kn.isRemove())
+                            it.remove();
+                    }
+                }
+                else if (value.path("@type").asText().equals("koral:token")) {
+                    // todo: koral:token nodes cannot be flagged for deletion --> creates the possibility for empty koral:token nodes
+                    rewrite(key, kroot,
+                            RewriteHandler.this.token_node_processors, result);
+                    return processNode(key, value.path("wrap"), result);
+                }
+                else {
+                    return rewrite(key, kroot,
+                            RewriteHandler.this.node_processors, result);
+                }
+            }
+            else if (value.isArray()) {
+                Iterator<JsonNode> it = value.elements();
+                while (it.hasNext()) {
+                    JsonNode next = it.next();
+                    KoralNode kn = processNode(key, next, result);
+                    if (kn.isRemove())
+                        it.remove();
+                }
+            }
+            return kroot;
+        }
+
+        private JsonNode start (boolean result) throws KustvaktException {
+            if (DEBUG) {
+                jlog.debug("Running rewrite process on query " + root);
+            }
+            if (root != null) {
+                Iterator<Map.Entry<String, JsonNode>> it = root.fields();
+                while (it.hasNext()) {
+                    Map.Entry<String, JsonNode> next = it.next();
+                    processNode(next.getKey(), next.getValue(), result);
+                }
+                processFixedNode(root, RewriteHandler.this.query_processors,
+                        result);
+            }
+            return root;
+        }
+
+        /**
+         * @param node
+         * @param tasks
+         * @return boolean true if node is to be removed from parent!
+         *         Only
+         *         applies if parent is an array node
+         */
+        private KoralNode rewrite (String rootNode, KoralNode node,
+                Collection<? extends RewriteTask> tasks, boolean result)
+                throws KustvaktException {
+            if (RewriteHandler.this.config == null)
+                throw new RuntimeException(
+                        "KustvaktConfiguration must be set!");
+
+            for (RewriteTask task : tasks) {
+                if (DEBUG) {
+                    jlog.debug("running processor on node: " + node);
+                    jlog.debug("on processor: " + task.getClass().toString());
+                }
+
+                //                if (RewriteHandler.this.beans != null
+                //                        && task instanceof BeanInjectable)
+                //                    ((BeanInjectable) task)
+                //                            .insertBeans(RewriteHandler.this.beans);
+
+                if (task instanceof RewriteTask.IterableRewritePath) {
+                    RewriteTask.IterableRewritePath rw = (RewriteTask.IterableRewritePath) task;
+                    if (rw.path() != null && !rw.path().equals(rootNode)) {
+                        if (DEBUG) {
+                            jlog.debug("skipping node: " + node);
+                        }
+                        continue;
+                    }
+                }
+                if (!result && task instanceof RewriteTask.RewriteQuery) {
+                    ((RewriteTask.RewriteQuery) task).rewriteQuery(node,
+                            RewriteHandler.this.config, this.user);
+                }
+                else if (task instanceof RewriteTask.RewriteResult) {
+                    ((RewriteTask.RewriteResult) task).rewriteResult(node);
+                }
+
+                if (node.isRemove()) {
+                    node.buildRewrites(this.root.at("/" + rootNode));
+                    break;
+                }
+                else
+                    node.buildRewrites();
+            }
+            return node;
+        }
+
+        // fixme: merge with processNode!
+        private void processFixedNode (JsonNode node,
+                Collection<RewriteTask> tasks, boolean post)
+                throws KustvaktException {
+            for (RewriteTask task : tasks) {
+                KoralNode next = KoralNode.wrapNode(node);
+                if (task instanceof RewriteTask.RewriteNodeAt) {
+                    RewriteTask.RewriteNodeAt rwa = (RewriteTask.RewriteNodeAt) task;
+                    if ((rwa.at() != null
+                            && !node.at(rwa.at()).isMissingNode()))
+                        next = next.at(rwa.at());
+                }
+
+                if (!post & task instanceof RewriteTask.RewriteQuery)
+                    next = ((RewriteTask.RewriteQuery) task).rewriteQuery(next,
+                            RewriteHandler.this.config, user);
+                else if (task instanceof RewriteTask.RewriteResult)
+                    ((RewriteTask.RewriteResult) task).rewriteResult(next);
+                next.buildRewrites();
+
+            }
+        }
+
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/RewriteTask.java b/src/main/java/de/ids_mannheim/korap/rewrite/RewriteTask.java
new file mode 100644
index 0000000..b015c0e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/RewriteTask.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.rewrite;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * @author hanl
+ * @date 30/06/2015
+ */
+public interface RewriteTask {
+
+    /**
+     * unspecified query rewrite that gets injected the entire root
+     * node during preprocessing
+     */
+    interface RewriteQuery extends RewriteTask {
+        /**
+         * @param node
+         *            Json node in KoralNode wrapper
+         * @param config
+         *            {@link KustvaktConfiguration} singleton instance
+         *            to use default configuration parameters
+         * @param user
+         *            injected by rewrite handler if available. Might
+         *            cause {@link NullPointerException} if not
+         *            checked properly
+         * @return
+         */
+        KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+                User user) throws KustvaktException;
+
+    }
+
+    /**
+     * Post processor targeted at result sets for queries
+     * {@link RewriteResult} queries will run
+     * after {@link IterableRewritePath} have been processed
+     */
+    interface RewriteResult extends RewriteTask {
+        JsonNode rewriteResult (KoralNode node) throws KustvaktException;
+    }
+
+    /**
+     * nodes subject to rewrites at fixed json pointer location.
+     * Json-pointer based rewrites are processed after iterable
+     * rewrites
+     * Deletion via KoralNode not allowed. Supports pre- and
+     * post-processing
+     */
+    interface RewriteNodeAt extends RewriteQuery, RewriteResult {
+        String at ();
+    }
+
+    /**
+     * terminal object nodes that are subject to rewrites through node
+     * iteration
+     * (both object and array node iteration supported)
+     */
+    interface IterableRewritePath extends RewriteQuery, RewriteResult {
+        String path ();
+    }
+
+    /**
+     * koral token nodes that are subject to rewrites
+     * Be aware that node rewrites are processed before query
+     * rewrites. Thus query rewrite may override previous node
+     * rewrites {@link RewriteKoralToken} rewrite DOES NOT support the
+     * deletion of the respective node
+     */
+    interface RewriteKoralToken extends RewriteQuery {}
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/TreeConstraint.java b/src/main/java/de/ids_mannheim/korap/rewrite/TreeConstraint.java
new file mode 100644
index 0000000..81f254a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/TreeConstraint.java
@@ -0,0 +1,77 @@
+package de.ids_mannheim.korap.rewrite;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.config.BeanInjectable;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.user.User;
+
+/**
+ * #ELEM(W ANA=N)
+ * <p/>
+ * {
+ * "@context":
+ * "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
+ * "errors": [],
+ * "warnings": [],
+ * "messages": [],
+ * "collection": {},
+ * "query": {
+ * "@type": "koral:span",
+ * "key": "w",
+ * "attr": {
+ * "@type": "koral:term",
+ * "layer": "p",
+ * "key": "N",
+ * "match": "match:eq"
+ * }
+ * },
+ * "meta": {}
+ * }
+ * <p/>
+ * <p/>
+ * email reference:
+ * Hallo Michael,
+ * mir fiel gestern bei der neuen KoralQuery Serialisierung noch ein
+ * Fall
+ * für default-Werte ein, die zumindest für viele Beispiele, die wir
+ * haben,
+ * relevant ist: Wenn ein koral:term in einem koral:span gewrappt ist,
+ * dann
+ * kann er eventuell nur einen Schlüssel haben ("s" oder "p" von "<s>"
+ * oder
+ * "<p>". In diesem Fall wäre der default layer "s" und die default
+ * foundry
+ * "base". (Im alten KoralQuery wurden spans nicht gewrappt - der Fall
+ * sollte aber erstmal weiter unterstützt werden.)
+ * Viele Grüße,
+ * Nils
+ * 
+ * @author hanl
+ * @date 02/07/2015
+ */
+public class TreeConstraint implements RewriteTask.RewriteNodeAt {
+
+    private String pointer;
+
+    public TreeConstraint () {
+        super();
+    }
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) {
+        System.out.println("FIND PATH " + node.rawNode().findParent(pointer));
+
+        return node;
+    }
+
+    @Override
+    public JsonNode rewriteResult (KoralNode node) {
+        return null;
+    }
+
+    @Override
+    public String at () {
+        return null;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/rewrite/VirtualCorpusRewrite.java b/src/main/java/de/ids_mannheim/korap/rewrite/VirtualCorpusRewrite.java
new file mode 100644
index 0000000..3a64df9
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/rewrite/VirtualCorpusRewrite.java
@@ -0,0 +1,120 @@
+package de.ids_mannheim.korap.rewrite;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.rewrite.KoralNode.RewriteIdentifier;
+import de.ids_mannheim.korap.service.QueryService;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.util.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+/**
+ * Rewrites virtual corpus reference with the corresponding koral
+ * query describing the actual virtual corpus query.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class VirtualCorpusRewrite implements RewriteTask.RewriteQuery {
+
+    @Autowired
+    private KustvaktConfiguration config;
+    @Autowired
+    private QueryService queryService;
+
+    @Override
+    public KoralNode rewriteQuery (KoralNode node, KustvaktConfiguration config,
+            User user) throws KustvaktException {
+        if (node.has("collection")) {
+            node = node.at("/collection");
+            findVCRef(user.getUsername(), node);
+        }
+        return node;
+    }
+
+    private void findVCRef (String username, KoralNode koralNode)
+            throws KustvaktException {
+        if (koralNode.has("@type")
+                && koralNode.get("@type").equals("koral:docGroupRef")) {
+            if (!koralNode.has("ref")) {
+                throw new KustvaktException(StatusCodes.MISSING_VC_REFERENCE,
+                        "ref is not found");
+            }
+            else {
+                String vcName = koralNode.get("ref");
+                String vcOwner = "system";
+                boolean ownerExist = false;
+                if (vcName.contains("/")) {
+                    String[] names = vcName.split("/");
+                    if (names.length == 2) {
+                        vcOwner = names[0];
+                        vcName = names[1];
+                        ownerExist = true;
+                    }
+                }
+
+                String vcInCaching = config.getVcInCaching();
+                if (vcName.equals(vcInCaching)) {
+                    throw new KustvaktException(
+                            de.ids_mannheim.korap.exceptions.StatusCodes.CACHING_VC,
+                            "VC is currently busy and unaccessible due to "
+                                    + "caching process",
+                            koralNode.get("ref"));
+                }
+
+                QueryDO vc = queryService.searchQueryByName(username, vcName,
+                        vcOwner, QueryType.VIRTUAL_CORPUS);
+                if (!vc.isCached()) {
+                    rewriteVC(vc, koralNode);
+                }
+                // required for named-vc since they are stored by filenames in the cache
+                else if (ownerExist) {
+                    removeOwner(vc.getKoralQuery(), vcOwner, koralNode);
+                }
+            }
+
+        }
+        else if (koralNode.has("operands")) {
+            KoralNode operands = koralNode.at("/operands");
+
+            for (int i = 0; i < operands.size(); i++) {
+                KoralNode operand = operands.get(i);
+                findVCRef(username, operand);
+                operand.buildRewrites();
+            }
+
+        }
+    }
+
+    private void removeOwner (String koralQuery, String vcOwner,
+            KoralNode koralNode) throws KustvaktException {
+        JsonNode jsonNode = koralNode.rawNode();
+        String ref = jsonNode.at("/ref").asText();
+        koralNode.remove("ref", new RewriteIdentifier("ref", ref));
+
+        ref = ref.substring(vcOwner.length() + 1, ref.length());
+        koralNode.set("ref", ref, new RewriteIdentifier("ref", ref));
+    }
+
+    private void rewriteVC (QueryDO vc, KoralNode koralNode)
+            throws KustvaktException {
+        String koralQuery = vc.getKoralQuery();
+        JsonNode kq = JsonUtils.readTree(koralQuery).at("/collection");
+        JsonNode jsonNode = koralNode.rawNode();
+        // rewrite
+        koralNode.remove("@type",
+                new RewriteIdentifier("@type", jsonNode.at("/@type").asText()));
+        koralNode.remove("ref",
+                new RewriteIdentifier("ref", jsonNode.at("/ref").asText()));
+        koralNode.setAll((ObjectNode) kq);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/context/KustvaktContext.java b/src/main/java/de/ids_mannheim/korap/security/context/KustvaktContext.java
new file mode 100644
index 0000000..ac09489
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/context/KustvaktContext.java
@@ -0,0 +1,41 @@
+package de.ids_mannheim.korap.security.context;
+
+import java.security.Principal;
+
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * @author hanl
+ * @date 13/05/2014
+ * 
+ *       wrapper for REST security context
+ * 
+ */
+public class KustvaktContext implements SecurityContext {
+
+    private TokenContext user;
+
+    public KustvaktContext (final TokenContext user) {
+        this.user = user;
+    }
+
+    @Override
+    public Principal getUserPrincipal () {
+        return this.user;
+    }
+
+    @Override
+    public boolean isUserInRole (String role) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSecure () {
+        return false;
+    }
+
+    @Override
+    public String getAuthenticationScheme () {
+        return SecurityContext.BASIC_AUTH;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java b/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
new file mode 100644
index 0000000..10b60ee
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
@@ -0,0 +1,154 @@
+package de.ids_mannheim.korap.security.context;
+
+import java.io.Serializable;
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.Setter;
+
+/**
+ * EM:
+ * - change datatype of tokenType from string to enum
+ * - added authenticationTime
+ * 
+ * @author hanl
+ * @date 27/01/2014
+ */
+@Data
+public class TokenContext implements java.security.Principal, Serializable {
+
+    private ZonedDateTime authenticationTime;
+    /**
+     * session relevant data. Are never persisted into a database
+     */
+    private String username;
+    private long expirationTime;
+    // either "session_token " / "api_token
+    private TokenType tokenType;
+    private String token;
+    private boolean secureRequired;
+
+    //    @Getter(AccessLevel.PRIVATE)
+    @Setter(AccessLevel.PRIVATE)
+    private Map<String, Object> parameters;
+    private String hostAddress;
+    private String userAgent;
+
+    public TokenContext () {
+        this.parameters = new HashMap<>();
+        this.setUsername("");
+        this.setToken("");
+        this.setSecureRequired(false);
+        this.setExpirationTime(-1);
+    }
+
+    private Map statusMap () {
+        Map m = new HashMap();
+        if (username != null && !username.isEmpty())
+            m.put(Attributes.USERNAME, username);
+        m.put(Attributes.TOKEN_EXPIRATION,
+                TimeUtils.format(this.expirationTime));
+        m.put(Attributes.TOKEN, this.token);
+        m.put(Attributes.TOKEN_TYPE, this.tokenType);
+        return m;
+    }
+
+    public Map<String, Object> params () {
+        return new HashMap<>(parameters);
+    }
+
+    public boolean match (TokenContext other) {
+        if (other.getToken().equals(this.token))
+            if (this.getHostAddress().equals(this.hostAddress))
+                // user agent should be irrelvant -- what about os
+                // system version?
+                // if (other.getUserAgent().equals(this.userAgent))
+                return true;
+        return false;
+    }
+
+    public void addContextParameter (String key, String value) {
+        this.parameters.put(key, value);
+    }
+
+    public void addParams (Map<String, Object> map) {
+        for (Map.Entry<String, Object> e : map.entrySet())
+            this.parameters.put(e.getKey(), String.valueOf(e.getValue()));
+    }
+
+    public void removeContextParameter (String key) {
+        this.parameters.remove(key);
+    }
+
+    public void setExpirationTime (long date) {
+        this.expirationTime = date;
+    }
+
+    // todo: complete
+    public static TokenContext fromJSON (String s) throws KustvaktException {
+        JsonNode node = JsonUtils.readTree(s);
+        TokenContext c = new TokenContext();
+        if (node != null) {
+            c.setUsername(node.path(Attributes.USERNAME).asText());
+            c.setToken(node.path(Attributes.TOKEN).asText());
+        }
+        return c;
+    }
+
+    public static TokenContext fromOAuth2 (String s) throws KustvaktException {
+        JsonNode node = JsonUtils.readTree(s);
+        TokenContext c = new TokenContext();
+        if (node != null) {
+            c.setToken(node.path("token").asText());
+            c.setTokenType(TokenType.valueOf(node.path("token_type").asText()));
+            c.setExpirationTime(node.path("expires_in").asLong());
+            c.addContextParameter("refresh_token",
+                    node.path("refresh_token").asText());
+
+        }
+        return c;
+    }
+
+    public boolean isValid () {
+        return (this.username != null && !this.username.isEmpty())
+                && (this.token != null && !this.token.isEmpty())
+                && (this.tokenType != null);
+    }
+
+    public String getToken () {
+        return token;
+    }
+
+    public String toJson () throws KustvaktException {
+        return JsonUtils.toJSON(this.statusMap());
+    }
+
+    public boolean isDemo () {
+        return User.UserFactory.isDemo(this.username);
+    }
+
+    @Override
+    public String getName () {
+        return this.getUsername();
+    }
+
+    public ZonedDateTime getAuthenticationTime () {
+        return authenticationTime;
+    }
+
+    public void setAuthenticationTime (ZonedDateTime authTime) {
+        this.authenticationTime = authTime;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java b/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
new file mode 100644
index 0000000..4b1c17e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.server;
+
+import com.unboundid.ldap.listener.*;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.util.CryptoHelper;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+import de.ids_mannheim.korap.authentication.LDAPConfig;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+
+public class EmbeddedLdapServer {
+    static InMemoryDirectoryServer server;
+
+    public static void startIfNotRunning (LDAPConfig ldapConfig)
+            throws LDAPException, GeneralSecurityException,
+            UnknownHostException {
+        if (server != null) {
+            return;
+        }
+
+        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(
+                ldapConfig.searchBase);
+        final MessageDigest sha1Digest = CryptoHelper.getMessageDigest("SHA1");
+        final MessageDigest sha256Digest = CryptoHelper
+                .getMessageDigest("SHA-256");
+        config.setPasswordEncoders(
+                new ClearInMemoryPasswordEncoder("{CLEAR}", null),
+                new ClearInMemoryPasswordEncoder("{HEX}",
+                        HexPasswordEncoderOutputFormatter
+                                .getLowercaseInstance()),
+                new ClearInMemoryPasswordEncoder("{BASE64}",
+                        Base64PasswordEncoderOutputFormatter.getInstance()),
+                new UnsaltedMessageDigestInMemoryPasswordEncoder("{SHA}",
+                        Base64PasswordEncoderOutputFormatter.getInstance(),
+                        sha1Digest),
+                new UnsaltedMessageDigestInMemoryPasswordEncoder("{SHA256}",
+                        Base64PasswordEncoderOutputFormatter.getInstance(),
+                        sha256Digest));
+        config.addAdditionalBindCredentials(ldapConfig.sLoginDN,
+                ldapConfig.sPwd);
+        config.setSchema(null);
+
+        final SSLUtil clientSslUtil = new SSLUtil(new TrustAllTrustManager());
+
+        config.setListenerConfigs(
+                InMemoryListenerConfig.createLDAPConfig("LDAP", // Listener name
+                        InetAddress.getByName("localhost"), // Listen address. (null = listen on all interfaces)
+                        ldapConfig.port, // Listen port (0 = automatically choose an available port)
+                        clientSslUtil.createSSLSocketFactory()));
+        server = new InMemoryDirectoryServer(config);
+
+        server.importFromLDIF(true, ldapConfig.ldif);
+        server.startListening();
+    }
+
+    public static void startIfNotRunning (String ldapConfigFilename)
+            throws LDAPException, GeneralSecurityException,
+            UnknownHostException {
+        LDAPConfig ldapConfig = new LDAPConfig(ldapConfigFilename);
+        startIfNotRunning(ldapConfig);
+    }
+
+    public static void stop () {
+        if (server != null) {
+            server.shutDown(true);
+            server = null;
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java b/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java
new file mode 100644
index 0000000..d13517c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java
@@ -0,0 +1,167 @@
+package de.ids_mannheim.korap.server;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.util.Scanner;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.server.handler.ShutdownHandler;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.springframework.web.context.ContextLoaderListener;
+import org.springframework.web.context.support.XmlWebApplicationContext;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author hanl
+ * @date 01/06/2015
+ */
+public abstract class KustvaktBaseServer {
+
+    protected static KustvaktConfiguration config;
+    protected static String springConfig = "default-config.xml";
+
+    protected static String rootPackages;
+    protected static KustvaktArgs kargs;
+
+    public KustvaktBaseServer () {
+        rootPackages = "de.ids_mannheim.korap.core.web;"
+                + "de.ids_mannheim.korap.web;"
+//                + "com.fasterxml.jackson.jaxrs.json;"
+                ;
+
+        File d = new File(KustvaktConfiguration.DATA_FOLDER);
+        if (!d.exists()) {
+            d.mkdir();
+        }
+    }
+
+    protected KustvaktArgs readAttributes (String[] args) {
+        KustvaktArgs kargs = new KustvaktArgs();
+        for (int i = 0; i < args.length; i++) {
+            switch ((args[i])) {
+                case "--spring-config":
+                    kargs.setSpringConfig(args[i + 1]);
+                    break;
+                case "--port":
+                    kargs.setPort(Integer.valueOf(args[i + 1]));
+                    break;
+                case "--help":
+                    StringBuffer b = new StringBuffer();
+
+                    b.append("Parameter description: \n").append(
+                            "--spring-config  <Spring XML configuration>\n")
+                            .append("--port  <Server port number>\n")
+                            .append("--help : This help menu\n");
+                    System.out.println(b.toString());
+                    System.out.println();
+                    return (KustvaktArgs) null;
+            }
+        }
+        return kargs;
+    }
+
+    protected void start ()
+            throws KustvaktException, IOException, NoSuchAlgorithmException {
+
+        if (kargs.port == -1) {
+            kargs.setPort(config.getPort());
+        }
+
+        String adminToken = "";
+        File f = new File("adminToken");
+        if (!f.exists()) {
+            RandomCodeGenerator random = new RandomCodeGenerator();
+            adminToken = random.createRandomCode(config);
+            FileOutputStream fos = new FileOutputStream(new File("adminToken"));
+            OutputStreamWriter writer = new OutputStreamWriter(fos,
+                    StandardCharsets.UTF_8.name());
+            writer.append("token=");
+            writer.append(adminToken);
+            writer.flush();
+            writer.close();
+        }
+        else {
+            Scanner scanner = new Scanner(f);
+            adminToken = scanner.nextLine().substring(6);
+            scanner.close();
+        }
+
+        Server server = new Server();
+
+        String configLocation = "classpath:" + springConfig;
+        if (kargs.getSpringConfig() != null) {
+            configLocation = "file:" + kargs.getSpringConfig();
+        }
+        XmlWebApplicationContext context = new XmlWebApplicationContext();
+        context.setConfigLocation(configLocation);
+        
+        ServletContextHandler contextHandler = new ServletContextHandler(
+                ServletContextHandler.NO_SESSIONS);
+        contextHandler.setContextPath("/");
+        contextHandler.addEventListener(new ContextLoaderListener(context));
+        contextHandler.setInitParameter("adminToken", adminToken);
+
+        ServletHolder servletHolder = new ServletHolder(
+                new ServletContainer());
+        servletHolder.setInitParameter(ServerProperties.PROVIDER_PACKAGES,
+                rootPackages);
+        servletHolder.setInitOrder(1);
+        contextHandler.addServlet(servletHolder, config.getBaseURL());
+
+        ServerConnector connector = new ServerConnector(server);
+        connector.setPort(kargs.port);
+        connector.setIdleTimeout(60000);
+        connector.getConnectionFactory(HttpConnectionFactory.class)
+                .getHttpConfiguration().setRequestHeaderSize(64000);
+
+        ShutdownHandler shutdownHandler = new ShutdownHandler(adminToken, true,
+                false);
+
+        HandlerList handlers = new HandlerList();
+        handlers.addHandler(shutdownHandler);
+        handlers.addHandler(contextHandler);
+
+        server.setHandler(handlers);
+
+        server.setConnectors(new Connector[] { connector });
+        try {
+            server.start();
+            server.join();
+        }
+        catch (Exception e) {
+            System.out.println("Server could not be started!");
+            System.out.println(e.getMessage());
+            e.printStackTrace();
+            System.exit(-1);
+        }
+    }
+
+    @Setter
+    public static class KustvaktArgs {
+
+        @Getter
+        private String springConfig;
+        private int port;
+
+        public KustvaktArgs () {
+            this.port = -1;
+            this.springConfig = null;
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/server/KustvaktLiteServer.java b/src/main/java/de/ids_mannheim/korap/server/KustvaktLiteServer.java
new file mode 100644
index 0000000..d6b1c80
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/server/KustvaktLiteServer.java
@@ -0,0 +1,46 @@
+package de.ids_mannheim.korap.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Properties;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+
+public class KustvaktLiteServer extends KustvaktBaseServer {
+
+    public static void main (String[] args) throws Exception {
+        KustvaktLiteServer server = new KustvaktLiteServer();
+        kargs = server.readAttributes(args);
+
+        if (kargs == null)
+            System.exit(0);
+
+        File f = new File("kustvakt-lite.conf");
+        Properties properties = new Properties();
+        InputStream in = null;
+
+        if (!f.exists()) {
+            in = KustvaktLiteServer.class.getClassLoader()
+                    .getResourceAsStream("kustvakt-lite.conf");
+        }
+        else {
+            in = new FileInputStream(f);
+        }
+
+        properties.load(in);
+        in.close();
+        config = new KustvaktConfiguration();
+        config.loadBasicProperties(properties);
+
+        springConfig = "default-lite-config.xml";
+
+        rootPackages = "de.ids_mannheim.korap.core.web; "
+                + "de.ids_mannheim.korap.web.filter; "
+                + "de.ids_mannheim.korap.web.utils; "
+                + "com.fasterxml.jackson.jaxrs.json;";
+
+        server.start();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/server/KustvaktServer.java b/src/main/java/de/ids_mannheim/korap/server/KustvaktServer.java
new file mode 100644
index 0000000..392190e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/server/KustvaktServer.java
@@ -0,0 +1,45 @@
+package de.ids_mannheim.korap.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Properties;
+
+import de.ids_mannheim.korap.config.FullConfiguration;
+
+/**
+ * pu
+ * 
+ * @author hanl
+ * @date 28/01/2014
+ */
+public class KustvaktServer extends KustvaktBaseServer {
+
+    public static void main (String[] args) throws Exception {
+        KustvaktServer server = new KustvaktServer();
+        kargs = server.readAttributes(args);
+
+        File f = new File("kustvakt.conf");
+        Properties properties = new Properties();
+
+        InputStream in = null;
+        if (!f.exists()) {
+            in = KustvaktServer.class.getClassLoader()
+                    .getResourceAsStream("kustvakt.conf");
+        }
+        else {
+            in = new FileInputStream(f);
+        }
+
+        properties.load(in);
+        in.close();
+
+        config = new FullConfiguration();
+        config.loadBasicProperties(properties);
+
+        if (kargs == null)
+            System.exit(0);
+
+        server.start();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/DefaultSettingService.java b/src/main/java/de/ids_mannheim/korap/service/DefaultSettingService.java
new file mode 100644
index 0000000..b0ba7d0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/DefaultSettingService.java
@@ -0,0 +1,130 @@
+package de.ids_mannheim.korap.service;
+
+import java.util.Map;
+
+import org.apache.http.HttpStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.dao.DefaultSettingDao;
+import de.ids_mannheim.korap.entity.DefaultSetting;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.user.UserSettingProcessor;
+import de.ids_mannheim.korap.validator.ApacheValidator;
+
+/**
+ * DefaultSettingService handles all business logic related to user
+ * default setting.
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class DefaultSettingService {
+
+    @Autowired
+    private DefaultSettingDao settingDao;
+    @Autowired
+    private ApacheValidator validator;
+
+    private String verifiyUsername (String username, String contextUsername)
+            throws KustvaktException {
+        username = username.substring(1);
+        if (!username.equals(contextUsername)) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "Username verification failed. Path parameter username "
+                            + "without prefix must be the same as the "
+                            + "authenticated username.",
+                    username);
+        }
+        return username;
+    }
+
+    private void validateSettingMap (Map<String, Object> map)
+            throws KustvaktException {
+        if (map == null || map.isEmpty()) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "Entity body is empty. No settings are given.");
+        }
+        for (String k : map.keySet()) {
+            validator.validateEntry(k, "setting");
+        }
+    }
+
+    public int handlePutRequest (String username, Map<String, Object> map,
+            String contextUsername) throws KustvaktException {
+        username = verifiyUsername(username, contextUsername);
+        validateSettingMap(map);
+
+        UserSettingProcessor processor = new UserSettingProcessor();
+        processor.readQuietly(map, false);
+
+        DefaultSetting defaultSetting = settingDao
+                .retrieveDefaultSetting(username);
+        if (defaultSetting == null) {
+            createDefaultSetting(username, processor);
+            return HttpStatus.SC_CREATED;
+        }
+        else {
+            updateDefaultSetting(defaultSetting, processor);
+            return HttpStatus.SC_OK;
+        }
+    }
+
+    public void createDefaultSetting (String username,
+            UserSettingProcessor processor) throws KustvaktException {
+        String jsonSettings = processor.serialize();
+        settingDao.createDefaultSetting(username, jsonSettings);
+    }
+
+    public void updateDefaultSetting (DefaultSetting setting,
+            UserSettingProcessor newProcessor) throws KustvaktException {
+        UserSettingProcessor processor = new UserSettingProcessor(
+                setting.getSettings());
+        processor.update(newProcessor);
+
+        String jsonSettings = processor.serialize();
+        setting.setSettings(jsonSettings);
+        settingDao.updateDefaultSetting(setting);
+    }
+
+    public String retrieveDefaultSettings (String username,
+            String contextUsername) throws KustvaktException {
+
+        username = verifiyUsername(username, contextUsername);
+        return retrieveDefaultSettings(username);
+    }
+
+    public String retrieveDefaultSettings (String username)
+            throws KustvaktException {
+        DefaultSetting defaultSetting = settingDao
+                .retrieveDefaultSetting(username);
+        if (defaultSetting == null) {
+            return null;
+        }
+        return defaultSetting.getSettings();
+    }
+
+    public void deleteKey (String username, String contextUsername, String key)
+            throws KustvaktException {
+        username = verifiyUsername(username, contextUsername);
+        DefaultSetting defaultSetting = settingDao
+                .retrieveDefaultSetting(username);
+
+        String jsonSettings = defaultSetting.getSettings();
+        UserSettingProcessor processor = new UserSettingProcessor(jsonSettings);
+        processor.removeField(key);
+        String json = processor.serialize();
+
+        defaultSetting.setSettings(json);
+        settingDao.updateDefaultSetting(defaultSetting);
+    }
+
+    public void deleteSetting (String username, String contextUsername)
+            throws KustvaktException {
+        username = verifiyUsername(username, contextUsername);
+        settingDao.deleteDefaultSetting(username);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/MailAuthenticator.java b/src/main/java/de/ids_mannheim/korap/service/MailAuthenticator.java
new file mode 100644
index 0000000..21f2202
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/MailAuthenticator.java
@@ -0,0 +1,28 @@
+package de.ids_mannheim.korap.service;
+
+import jakarta.mail.Authenticator;
+import jakarta.mail.PasswordAuthentication;
+
+/**
+ * Defines Authenticator for creating javax.mail.Session.
+ * 
+ * @see src/main/resources/default-config.xml
+ * 
+ * @author margaretha
+ *
+ */
+public class MailAuthenticator extends Authenticator {
+
+    private PasswordAuthentication passwordAuthentication;
+
+    public MailAuthenticator (String username, String password) {
+        passwordAuthentication = new PasswordAuthentication(username, password);
+
+    }
+
+    @Override
+    protected PasswordAuthentication getPasswordAuthentication () {
+        return passwordAuthentication;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/MailService.java b/src/main/java/de/ids_mannheim/korap/service/MailService.java
new file mode 100644
index 0000000..7619a25
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/MailService.java
@@ -0,0 +1,91 @@
+package de.ids_mannheim.korap.service;
+
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.context.Context;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.mail.javamail.MimeMessagePreparator;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.authentication.AuthenticationManager;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+
+/**
+ * Manages mail related services, such as sending group member
+ * invitations per email.
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class MailService {
+
+    public static Logger jlog = LogManager.getLogger(MailService.class);
+    public static boolean DEBUG = false;
+
+    @Autowired
+    private AuthenticationManager authenticationManager;
+    @Autowired
+    private JavaMailSender mailSender;
+    @Autowired
+    private VelocityEngine velocityEngine;
+    @Autowired
+    private FullConfiguration config;
+
+    public void sendMemberInvitationNotification (String inviteeName,
+            String groupName, String inviter) throws KustvaktException {
+
+        ParameterChecker.checkStringValue(inviteeName, "inviteeName");
+        ParameterChecker.checkStringValue(groupName, "groupName");
+        ParameterChecker.checkStringValue(inviter, "inviter");
+
+        MimeMessagePreparator preparator = new MimeMessagePreparator() {
+
+            public void prepare (MimeMessage mimeMessage) throws Exception {
+
+                User invitee = authenticationManager.getUser(inviteeName,
+                        config.getEmailAddressRetrieval());
+
+                MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
+                message.setTo(new InternetAddress(invitee.getEmail()));
+                message.setFrom(config.getNoReply());
+                message.setSubject("Invitation to join " + groupName);
+                message.setText(prepareGroupInvitationText(inviteeName,
+                        groupName, inviter), true);
+            }
+
+        };
+        mailSender.send(preparator);
+    }
+
+    private String prepareGroupInvitationText (String username,
+            String groupName, String inviter) {
+        Context context = new VelocityContext();
+        context.put("username", username);
+        context.put("group", groupName);
+        context.put("inviter", inviter);
+
+        StringWriter stringWriter = new StringWriter();
+
+        velocityEngine.mergeTemplate(
+                "templates/" + config.getGroupInvitationTemplate(),
+                StandardCharsets.UTF_8.name(), context, stringWriter);
+
+        String message = stringWriter.toString();
+        if (DEBUG)
+            jlog.debug(message);
+        return message;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/QueryService.java b/src/main/java/de/ids_mannheim/korap/service/QueryService.java
new file mode 100644
index 0000000..cc5f51b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/QueryService.java
@@ -0,0 +1,737 @@
+package de.ids_mannheim.korap.service;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+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 com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.cache.VirtualCorpusCache;
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.QueryAccessStatus;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.dao.QueryAccessDao;
+import de.ids_mannheim.korap.dao.QueryDao;
+import de.ids_mannheim.korap.dto.QueryAccessDto;
+import de.ids_mannheim.korap.dto.QueryDto;
+import de.ids_mannheim.korap.dto.converter.QueryAccessConverter;
+import de.ids_mannheim.korap.dto.converter.QueryConverter;
+import de.ids_mannheim.korap.entity.QueryAccess;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.query.serialize.QuerySerializer;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import de.ids_mannheim.korap.utils.JsonUtils;
+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;
+import jakarta.ws.rs.core.Response.Status;
+
+/**
+ * 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
+ *
+ */
+@Service
+public class QueryService {
+
+    public static Logger jlog = LogManager.getLogger(QueryService.class);
+
+    public static boolean DEBUG = false;
+
+    public static Pattern queryNamePattern = Pattern
+            .compile("[a-zA-Z0-9]+[a-zA-Z_0-9-.]+");
+
+    @Autowired
+    private QueryDao queryDao;
+    @Autowired
+    private QueryAccessDao accessDao;
+    @Autowired
+    private AdminDao adminDao;
+    @Autowired
+    private UserGroupService userGroupService;
+    @Autowired
+    private SearchKrill krill;
+    @Autowired
+    private FullConfiguration config;
+    @Autowired
+    private QueryConverter converter;
+    @Autowired
+    private QueryAccessConverter accessConverter;
+
+    private void verifyUsername (String contextUsername, String pathUsername)
+            throws KustvaktException {
+        if (!contextUsername.equals(pathUsername)
+                && !adminDao.isAdmin(contextUsername)) {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + contextUsername,
+                    contextUsername);
+        }
+    }
+
+    public List<QueryDto> listOwnerQuery (String username, QueryType queryType)
+            throws KustvaktException {
+        List<QueryDO> list = queryDao.retrieveOwnerQuery(username, queryType);
+        return createQueryDtos(list, queryType);
+    }
+
+    public List<QueryDto> listSystemQuery (QueryType queryType)
+            throws KustvaktException {
+        List<QueryDO> list = queryDao.retrieveQueryByType(ResourceType.SYSTEM,
+                null, queryType);
+        return createQueryDtos(list, queryType);
+    }
+
+    public List<QueryDto> listAvailableQueryForUser (String username,
+            QueryType queryType) throws KustvaktException {
+        List<QueryDO> list = queryDao.retrieveQueryByUser(username, queryType);
+        return createQueryDtos(list, queryType);
+    }
+
+    public List<QueryDto> listQueryByType (String createdBy, ResourceType type,
+            QueryType queryType) throws KustvaktException {
+
+        List<QueryDO> virtualCorpora = queryDao.retrieveQueryByType(type,
+                createdBy, queryType);
+        Collections.sort(virtualCorpora);
+        return createQueryDtos(virtualCorpora, queryType);
+
+    }
+
+    private ArrayList<QueryDto> createQueryDtos (List<QueryDO> queryList,
+            QueryType queryType) throws KustvaktException {
+        ArrayList<QueryDto> dtos = new ArrayList<>(queryList.size());
+        QueryDO query;
+        Iterator<QueryDO> i = queryList.iterator();
+        while (i.hasNext()) {
+            query = i.next();
+            // String json = query.getKoralQuery();
+            String statistics = null;
+            // if (queryType.equals(QueryType.VIRTUAL_CORPUS)) {
+            // statistics = krill.getStatistics(json);
+            // }
+            QueryDto dto = converter.createQueryDto(query, statistics);
+            dtos.add(dto);
+        }
+        return dtos;
+    }
+
+    public void deleteQueryByName (String username, String queryName,
+            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,
+                    "Query " + code + " is not found.", String.valueOf(code));
+        }
+        else if (query.getCreatedBy().equals(username)
+                || adminDao.isAdmin(username)) {
+
+            if (query.getType().equals(ResourceType.PUBLISHED)) {
+                QueryAccess access = accessDao
+                        .retrieveHiddenAccess(query.getId());
+                accessDao.deleteAccess(access, "system");
+                userGroupService.deleteAutoHiddenGroup(
+                        access.getUserGroup().getId(), "system");
+            }
+            if (type.equals(QueryType.VIRTUAL_CORPUS)
+                    && VirtualCorpusCache.contains(queryName)) {
+                VirtualCorpusCache.delete(queryName);
+            }
+            queryDao.deleteQuery(query);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public Status handlePutRequest (String username, String queryCreator,
+            String queryName, QueryJson queryJson) throws KustvaktException {
+
+        verifyUsername(username, queryCreator);
+        QueryDO query = queryDao.retrieveQueryByName(queryName, queryCreator);
+
+        if (query == null) {
+            storeQuery(queryJson, queryName, queryCreator, username);
+            return Status.CREATED;
+        }
+        else {
+            editQuery(query, queryJson, queryName, username);
+            return Status.NO_CONTENT;
+        }
+    }
+
+    public void editQuery (QueryDO existingQuery, QueryJson newQuery,
+            String queryName, String username) throws KustvaktException {
+
+        if (!username.equals(existingQuery.getCreatedBy())
+                && !adminDao.isAdmin(username)) {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+
+        String koralQuery = null;
+        CorpusAccess requiredAccess = null;
+        String corpusQuery = newQuery.getCorpusQuery();
+        String query = newQuery.getQuery();
+        String queryLanguage = newQuery.getQueryLanguage();
+        if (corpusQuery != null && !corpusQuery.isEmpty()) {
+            koralQuery = serializeCorpusQuery(corpusQuery);
+            requiredAccess = determineRequiredAccess(newQuery.isCached(),
+                    queryName, koralQuery);
+        }
+        else if (query != null && !query.isEmpty() && queryLanguage != null
+                && !queryLanguage.isEmpty()) {
+            koralQuery = serializeQuery(query, queryLanguage);
+        }
+
+        ResourceType type = newQuery.getType();
+        if (type != null) {
+            if (existingQuery.getType().equals(ResourceType.PUBLISHED)) {
+                // withdraw from publication
+                if (!type.equals(ResourceType.PUBLISHED)) {
+                    QueryAccess hiddenAccess = accessDao
+                            .retrieveHiddenAccess(existingQuery.getId());
+                    deleteQueryAccess(hiddenAccess.getId(), "system");
+                    int groupId = hiddenAccess.getUserGroup().getId();
+                    userGroupService.deleteAutoHiddenGroup(groupId, "system");
+                    // EM: should the users within the hidden group
+                    // receive
+                    // notifications?
+                }
+                // else remains the same
+            }
+            else if (type.equals(ResourceType.PUBLISHED)) {
+                publishQuery(existingQuery.getId());
+            }
+        }
+
+        queryDao.editQuery(existingQuery, queryName, type, requiredAccess,
+                koralQuery, newQuery.getDefinition(), newQuery.getDescription(),
+                newQuery.getStatus(), newQuery.isCached(), query,
+                queryLanguage);
+    }
+
+    private void publishQuery (int queryId) throws KustvaktException {
+
+        QueryAccess access = accessDao.retrieveHiddenAccess(queryId);
+        // check if hidden access exists
+        if (access == null) {
+            QueryDO query = queryDao.retrieveQueryById(queryId);
+            // create and assign a new hidden group
+            int groupId = userGroupService.createAutoHiddenGroup();
+            UserGroup autoHidden = userGroupService
+                    .retrieveUserGroupById(groupId);
+            accessDao.createAccessToQuery(query, autoHidden, "system",
+                    QueryAccessStatus.HIDDEN);
+        }
+        else {
+            // should not happened
+            jlog.error("Cannot publish query with id: " + queryId
+                    + ". Hidden access exists! Access id: " + access.getId());
+        }
+    }
+
+    public void storeQuery (QueryJson query, String queryName,
+            String queryCreator, String username) throws KustvaktException {
+        QueryType queryType = query.getQueryType();
+        if (!checkNumberOfQueryLimit(username, queryType)) {
+            String type = queryType.displayName().toLowerCase();
+            throw new KustvaktException(StatusCodes.NOT_ALLOWED,
+                    "Cannot create " + type + ". The maximum number " + "of "
+                            + type + " has been reached.");
+        }
+
+        String koralQuery = computeKoralQuery(query);
+        storeQuery(username, queryName, query.getType(), query.getQueryType(),
+                koralQuery, query.getDefinition(), query.getDescription(),
+                query.getStatus(), query.isCached(), queryCreator,
+                query.getQuery(), query.getQueryLanguage());
+    }
+
+    private boolean checkNumberOfQueryLimit (String username,
+            QueryType queryType) throws KustvaktException {
+        Long num = queryDao.countNumberOfQuery(username, queryType);
+        if (num < config.getMaxNumberOfUserQueries())
+            return true;
+        else
+            return false;
+    }
+
+    private String computeKoralQuery (QueryJson query)
+            throws KustvaktException {
+        if (query.getQueryType().equals(QueryType.VIRTUAL_CORPUS)) {
+            ParameterChecker.checkStringValue(query.getCorpusQuery(),
+                    "corpusQuery");
+            return serializeCorpusQuery(query.getCorpusQuery());
+        }
+
+        if (query.getQueryType().equals(QueryType.QUERY)) {
+            ParameterChecker.checkStringValue(query.getQuery(), "query");
+            ParameterChecker.checkStringValue(query.getQueryLanguage(),
+                    "queryLanguage");
+            return serializeQuery(query.getQuery(), query.getQueryLanguage());
+        }
+
+        return null;
+    }
+
+    public void storeQuery (String username, String queryName,
+            ResourceType type, QueryType queryType, String koralQuery,
+            String definition, String description, String status,
+            boolean isCached, String queryCreator, String query,
+            String queryLanguage) throws KustvaktException {
+        ParameterChecker.checkNameValue(queryName, "queryName");
+        ParameterChecker.checkObjectValue(type, "type");
+
+        if (!queryNamePattern.matcher(queryName).matches()) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT, queryType
+                    .displayName()
+                    + " must consists of alphanumerical characters "
+                    + "(limited to ASCII), underscores, dashes and periods. "
+                    + "The name has to start with an alphanumerical character.",
+                    queryName);
+        }
+
+        if (type.equals(ResourceType.SYSTEM)) {
+            if (adminDao.isAdmin(username)) {
+                queryCreator = "system";
+            }
+            else if (!username.equals("system")) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Unauthorized operation for user: " + username,
+                        username);
+            }
+        }
+
+        CorpusAccess requiredAccess = CorpusAccess.PUB;
+        if (queryType.equals(QueryType.VIRTUAL_CORPUS)) {
+            requiredAccess = determineRequiredAccess(isCached, queryName,
+                    koralQuery);
+        }
+
+        if (DEBUG) {
+            jlog.debug("Storing query: " + queryName + "in the database ");
+        }
+
+        int queryId = 0;
+        try {
+            queryId = queryDao.createQuery(queryName, type, queryType,
+                    requiredAccess, koralQuery, definition, description, status,
+                    isCached, queryCreator, query, queryLanguage);
+
+        }
+        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());
+        }
+        if (type.equals(ResourceType.PUBLISHED)) {
+            publishQuery(queryId);
+        }
+    }
+
+    private String serializeCorpusQuery (String corpusQuery)
+            throws KustvaktException {
+        QuerySerializer serializer = new QuerySerializer();
+        serializer.setCollection(corpusQuery);
+        String koralQuery;
+        try {
+            koralQuery = serializer.convertCollectionToJson();
+        }
+        catch (JsonProcessingException e) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "Invalid argument: " + corpusQuery, corpusQuery);
+        }
+        if (DEBUG) {
+            jlog.debug(koralQuery);
+        }
+        return koralQuery;
+    }
+
+    private String serializeQuery (String query, String queryLanguage)
+            throws KustvaktException {
+        QuerySerializer serializer = new QuerySerializer();
+        String koralQuery;
+        koralQuery = serializer.setQuery(query, queryLanguage).toJSON();
+        if (DEBUG) {
+            jlog.debug(koralQuery);
+        }
+        return koralQuery;
+    }
+
+    public CorpusAccess determineRequiredAccess (boolean isCached, String name,
+            String koralQuery) throws KustvaktException {
+
+        if (isCached) {
+            KoralCollectionQueryBuilder koral = new KoralCollectionQueryBuilder();
+            koral.with("referTo " + name);
+            koralQuery = koral.toJSON();
+            if (DEBUG) {
+                jlog.debug("Determine vc access with vc ref: " + koralQuery);
+            }
+
+        }
+
+        if (findDocWithLicense(koralQuery, config.getAllOnlyRegex())) {
+            return CorpusAccess.ALL;
+        }
+        else if (findDocWithLicense(koralQuery, config.getPublicOnlyRegex())) {
+            return CorpusAccess.PUB;
+        }
+        else {
+            return CorpusAccess.FREE;
+        }
+    }
+
+    private boolean findDocWithLicense (String koralQuery, String license)
+            throws KustvaktException {
+        KoralCollectionQueryBuilder koral = new KoralCollectionQueryBuilder();
+        koral.setBaseQuery(koralQuery);
+        koral.with("availability=/" + license + "/");
+        String json = koral.toJSON();
+
+        String statistics = krill.getStatistics(json);
+        JsonNode node = JsonUtils.readTree(statistics);
+        int numberOfDoc = node.at("/documents").asInt();
+        if (DEBUG) {
+            jlog.debug(
+                    "License: " + license + ", number of docs: " + numberOfDoc);
+        }
+        return (numberOfDoc > 0) ? true : false;
+    }
+
+    public void shareQuery (String username, String createdBy, String queryName,
+            String groupName) throws KustvaktException {
+
+        QueryDO query = queryDao.retrieveQueryByName(queryName, createdBy);
+        if (query == null) {
+            String code = createdBy + "/" + queryName;
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Query " + code + " is not found.", String.valueOf(code));
+        }
+        if (!username.equals(query.getCreatedBy())
+                && !adminDao.isAdmin(username)) {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+
+        UserGroup userGroup = userGroupService
+                .retrieveUserGroupByName(groupName);
+
+        if (!isQueryAccessAdmin(userGroup, username)
+                && !adminDao.isAdmin(username)) {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+        else {
+            try {
+                accessDao.createAccessToQuery(query, userGroup, username,
+                        QueryAccessStatus.ACTIVE);
+            }
+            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());
+            }
+
+            queryDao.editQuery(query, null, ResourceType.PROJECT, null, null,
+                    null, null, null, query.isCached(), null, null);
+        }
+    }
+
+    private boolean isQueryAccessAdmin (UserGroup userGroup, String username)
+            throws KustvaktException {
+        List<UserGroupMember> accessAdmins = userGroupService
+                .retrieveQueryAccessAdmins(userGroup);
+        for (UserGroupMember m : accessAdmins) {
+            if (username.equals(m.getUserId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // public void editVCAccess (VirtualCorpusAccess access, String
+    // username)
+    // throws KustvaktException {
+    //
+    // // get all the VCA admins
+    // UserGroup userGroup = access.getUserGroup();
+    // List<UserGroupMember> accessAdmins =
+    // userGroupService.retrieveVCAccessAdmins(userGroup);
+    //
+    // User user = authManager.getUser(username);
+    // if (!user.isSystemAdmin()) {
+    // throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+    // "Unauthorized operation for user: " + username, username);
+    // }
+    // }
+
+    public List<QueryAccessDto> listQueryAccessByUsername (String username)
+            throws KustvaktException {
+        List<QueryAccess> accessList = new ArrayList<>();
+        if (adminDao.isAdmin(username)) {
+            accessList = accessDao.retrieveAllAccess();
+        }
+        else {
+            List<UserGroup> groups = userGroupService
+                    .retrieveUserGroup(username);
+            for (UserGroup g : groups) {
+                if (isQueryAccessAdmin(g, username)) {
+                    accessList.addAll(
+                            accessDao.retrieveActiveAccessByGroup(g.getId()));
+                }
+            }
+        }
+        return accessConverter.createQueryAccessDto(accessList);
+    }
+
+    public List<QueryAccessDto> listQueryAccessByQuery (String username,
+            String queryCreator, String queryName) throws KustvaktException {
+
+        List<QueryAccess> accessList;
+        if (adminDao.isAdmin(username)) {
+            accessList = accessDao.retrieveAllAccessByQuery(queryCreator,
+                    queryName);
+        }
+        else {
+            accessList = accessDao.retrieveActiveAccessByQuery(queryCreator,
+                    queryName);
+            List<QueryAccess> filteredAccessList = new ArrayList<>();
+            for (QueryAccess access : accessList) {
+                UserGroup userGroup = access.getUserGroup();
+                if (isQueryAccessAdmin(userGroup, username)) {
+                    filteredAccessList.add(access);
+                }
+            }
+            accessList = filteredAccessList;
+        }
+        return accessConverter.createQueryAccessDto(accessList);
+    }
+
+    @Deprecated
+    public List<QueryAccessDto> listVCAccessByGroup (String username,
+            int groupId) throws KustvaktException {
+        UserGroup userGroup = userGroupService.retrieveUserGroupById(groupId);
+
+        List<QueryAccess> accessList;
+        if (adminDao.isAdmin(username)) {
+            accessList = accessDao.retrieveAllAccessByGroup(groupId);
+        }
+        else if (isQueryAccessAdmin(userGroup, username)) {
+            accessList = accessDao.retrieveActiveAccessByGroup(groupId);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+
+        return accessConverter.createQueryAccessDto(accessList);
+    }
+
+    public List<QueryAccessDto> listQueryAccessByGroup (String username,
+            String groupName) throws KustvaktException {
+        UserGroup userGroup = userGroupService
+                .retrieveUserGroupByName(groupName);
+
+        List<QueryAccess> accessList;
+        if (adminDao.isAdmin(username)) {
+            accessList = accessDao.retrieveAllAccessByGroup(userGroup.getId());
+        }
+        else if (isQueryAccessAdmin(userGroup, username)) {
+            accessList = accessDao
+                    .retrieveActiveAccessByGroup(userGroup.getId());
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+        return accessConverter.createQueryAccessDto(accessList);
+    }
+
+    public void deleteQueryAccess (int accessId, String username)
+            throws KustvaktException {
+
+        QueryAccess access = accessDao.retrieveAccessById(accessId);
+        UserGroup userGroup = access.getUserGroup();
+        if (isQueryAccessAdmin(userGroup, username)
+                || adminDao.isAdmin(username)) {
+            accessDao.deleteAccess(access, username);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+
+    }
+
+    public JsonNode retrieveKoralQuery (String username, String queryName,
+            String createdBy, QueryType queryType) throws KustvaktException {
+        QueryDO query = searchQueryByName(username, queryName, createdBy,
+                queryType);
+        String koralQuery = query.getKoralQuery();
+        JsonNode kq = JsonUtils.readTree(koralQuery);
+        return kq;
+    }
+
+    public JsonNode retrieveFieldValues (String username, String queryName,
+            String createdBy, QueryType queryType, String fieldName)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(fieldName, "fieldName");
+
+        //        if (!adminDao.isAdmin(username)) {
+        //            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+        //                    "Unauthorized operation for user: " + username, username);
+        //        }
+
+        if (fieldName.equals("tokens") || fieldName.equals("base")) {
+            throw new KustvaktException(StatusCodes.NOT_ALLOWED,
+                    "Retrieving values of field " + fieldName
+                            + " is not allowed.");
+        }
+        else {
+            QueryDO query = searchQueryByName(username, queryName, createdBy,
+                    queryType);
+            String koralQuery = query.getKoralQuery();
+            return krill.getFieldValuesForVC(koralQuery, fieldName);
+        }
+    }
+
+    public QueryDO searchQueryByName (String username, String queryName,
+            String createdBy, QueryType queryType) throws KustvaktException {
+        QueryDO query = queryDao.retrieveQueryByName(queryName, createdBy);
+        if (query == null) {
+            String code = createdBy + "/" + queryName;
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    queryType.displayName() + " " + code + " is not found.",
+                    String.valueOf(code));
+        }
+        checkQueryAccess(query, username);
+        return query;
+    }
+
+    public QueryDto retrieveQueryByName (String username, String queryName,
+            String createdBy, QueryType queryType) throws KustvaktException {
+        QueryDO query = searchQueryByName(username, queryName, createdBy,
+                queryType);
+        // String json = query.getKoralQuery();
+        String statistics = null;
+        // long start,end;
+        // start = System.currentTimeMillis();
+        // if (query.getQueryType().equals(QueryType.VIRTUAL_CORPUS))
+        // {
+        // statistics = krill.getStatistics(json);
+        // }
+        // end = System.currentTimeMillis();
+        // jlog.debug("{} statistics duration: {}", queryName, (end -
+        // start));
+        return converter.createQueryDto(query, statistics);
+    }
+
+    public QueryDto searchQueryById (String username, int queryId)
+            throws KustvaktException {
+
+        QueryDO query = queryDao.retrieveQueryById(queryId);
+        checkQueryAccess(query, username);
+        // String json = query.getKoralQuery();
+        // String statistics = krill.getStatistics(json);
+        return converter.createQueryDto(query, null);
+    }
+
+    private void checkQueryAccess (QueryDO query, String username)
+            throws KustvaktException {
+        ResourceType type = query.getType();
+
+        if (!adminDao.isAdmin(username)
+                && !username.equals(query.getCreatedBy())) {
+            if (type.equals(ResourceType.PRIVATE)
+                    || (type.equals(ResourceType.PROJECT)
+                            && !hasAccess(username, query.getId()))) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Unauthorized operation for user: " + username,
+                        username);
+            }
+
+            else if (ResourceType.PUBLISHED.equals(type)
+                    && !username.equals("guest")) {
+                // add user in the query's auto group
+                UserGroup userGroup = userGroupService
+                        .retrieveHiddenUserGroupByQuery(query.getId());
+                try {
+                    userGroupService.addGroupMember(username, userGroup,
+                            "system", GroupMemberStatus.ACTIVE);
+                    // member roles are not set (not necessary)
+                }
+                catch (KustvaktException e) {
+                    // member exists
+                    // skip adding user to hidden group
+                }
+            }
+            // else VirtualCorpusType.SYSTEM
+        }
+    }
+
+    private boolean hasAccess (String username, int queryId)
+            throws KustvaktException {
+        UserGroup userGroup;
+        List<QueryAccess> accessList = accessDao
+                .retrieveActiveAccessByQuery(queryId);
+        for (QueryAccess access : accessList) {
+            userGroup = access.getUserGroup();
+            if (userGroupService.isMember(username, userGroup)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/ResourceService.java b/src/main/java/de/ids_mannheim/korap/service/ResourceService.java
new file mode 100644
index 0000000..6bfb1af
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/ResourceService.java
@@ -0,0 +1,44 @@
+package de.ids_mannheim.korap.service;
+
+import java.util.List;
+
+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.core.entity.Resource;
+import de.ids_mannheim.korap.dao.ResourceDao;
+import de.ids_mannheim.korap.dto.ResourceDto;
+import de.ids_mannheim.korap.dto.converter.ResourceConverter;
+import de.ids_mannheim.korap.web.controller.ResourceController;
+
+/**
+ * ResourceService defines the logic behind
+ * {@link ResourceController}.
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class ResourceService {
+
+    public static Logger jlog = LogManager.getLogger(ResourceService.class);
+    public static boolean DEBUG = false;
+
+    @Autowired
+    private ResourceDao resourceDao;
+    @Autowired
+    private ResourceConverter resourceConverter;
+
+    public List<ResourceDto> getResourceDtos () {
+        List<Resource> resources = resourceDao.getAllResources();
+        List<ResourceDto> resourceDtos = resourceConverter
+                .convertToResourcesDto(resources);
+        if (DEBUG) {
+            jlog.debug("/info " + resourceDtos.toString());
+        }
+        return resourceDtos;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java b/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
new file mode 100644
index 0000000..22fe070
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
@@ -0,0 +1,679 @@
+package de.ids_mannheim.korap.service;
+
+import java.sql.SQLException;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+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.FullConfiguration;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.dao.RoleDao;
+import de.ids_mannheim.korap.dao.UserGroupDao;
+import de.ids_mannheim.korap.dao.UserGroupMemberDao;
+import de.ids_mannheim.korap.dto.UserGroupDto;
+import de.ids_mannheim.korap.dto.converter.UserGroupConverter;
+import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import de.ids_mannheim.korap.web.controller.UserGroupController;
+
+/**
+ * UserGroupService defines the logic behind user group web
+ * controller.
+ * 
+ * @see UserGroupController
+ * 
+ * @author margaretha
+ *
+ */
+@Service
+public class UserGroupService {
+
+    public static Logger jlog = LogManager.getLogger(UserGroupService.class);
+    public static boolean DEBUG = false;
+
+    public static Pattern groupNamePattern = Pattern
+            .compile("[a-zA-Z0-9]+[a-zA-Z_0-9-.]+");
+
+    @Autowired
+    private UserGroupDao userGroupDao;
+    @Autowired
+    private UserGroupMemberDao groupMemberDao;
+    @Autowired
+    private RoleDao roleDao;
+    @Autowired
+    private AdminDao adminDao;
+    @Autowired
+    private UserGroupConverter converter;
+    @Autowired
+    private FullConfiguration config;
+    @Autowired
+    private MailService mailService;
+    @Autowired
+    private RandomCodeGenerator random;
+
+    private static Set<Role> memberRoles;
+
+    /**
+     * Only users with {@link PredefinedRole#USER_GROUP_ADMIN}
+     * are allowed to see the members of the group.
+     * 
+     * @param username
+     *            username
+     * @return a list of usergroups
+     * @throws KustvaktException
+     * 
+     * @see {@link PredefinedRole}
+     */
+    public List<UserGroup> retrieveUserGroup (String username)
+            throws KustvaktException {
+
+        List<UserGroup> userGroups = userGroupDao
+                .retrieveGroupByUserId(username);
+        Collections.sort(userGroups);
+        return userGroups;
+    }
+
+    public List<UserGroupDto> retrieveUserGroupDto (String username)
+            throws KustvaktException {
+        List<UserGroup> userGroups = retrieveUserGroup(username);
+
+        ArrayList<UserGroupDto> dtos = new ArrayList<>(userGroups.size());
+        UserGroupMember userAsMember;
+        List<UserGroupMember> members;
+        UserGroupDto groupDto;
+        for (UserGroup group : userGroups) {
+            members = retrieveMembers(group.getId(), username);
+            userAsMember = groupMemberDao.retrieveMemberById(username,
+                    group.getId());
+            groupDto = converter.createUserGroupDto(group, members,
+                    userAsMember.getStatus(), userAsMember.getRoles());
+            dtos.add(groupDto);
+        }
+
+        return dtos;
+
+    }
+
+    private List<UserGroupMember> retrieveMembers (int groupId, String username)
+            throws KustvaktException {
+        List<UserGroupMember> groupAdmins = groupMemberDao.retrieveMemberByRole(
+                groupId, PredefinedRole.USER_GROUP_ADMIN.getId());
+
+        List<UserGroupMember> members = null;
+        for (UserGroupMember admin : groupAdmins) {
+            if (admin.getUserId().equals(username)) {
+                members = groupMemberDao.retrieveMemberByGroupId(groupId);
+                break;
+            }
+        }
+
+        return members;
+    }
+
+    public UserGroup retrieveUserGroupById (int groupId)
+            throws KustvaktException {
+        return userGroupDao.retrieveGroupById(groupId);
+    }
+
+    public UserGroup retrieveUserGroupByName (String groupName)
+            throws KustvaktException {
+        return userGroupDao.retrieveGroupByName(groupName, false);
+    }
+
+    public UserGroup retrieveHiddenUserGroupByQuery (int queryId)
+            throws KustvaktException {
+        return userGroupDao.retrieveHiddenGroupByQuery(queryId);
+    }
+
+    public List<UserGroupDto> retrieveUserGroupByStatus (String username,
+            UserGroupStatus status) throws KustvaktException {
+
+        List<UserGroup> userGroups = userGroupDao
+                .retrieveGroupByStatus(username, status);
+        Collections.sort(userGroups);
+        ArrayList<UserGroupDto> dtos = new ArrayList<>(userGroups.size());
+
+        List<UserGroupMember> members;
+        UserGroupDto groupDto;
+        for (UserGroup group : userGroups) {
+            members = groupMemberDao.retrieveMemberByGroupId(group.getId(),
+                    true);
+            groupDto = converter.createUserGroupDto(group, members, null, null);
+            dtos.add(groupDto);
+        }
+        return dtos;
+    }
+
+    public List<UserGroupMember> retrieveQueryAccessAdmins (UserGroup userGroup)
+            throws KustvaktException {
+        List<UserGroupMember> groupAdmins = groupMemberDao.retrieveMemberByRole(
+                userGroup.getId(), PredefinedRole.VC_ACCESS_ADMIN.getId());
+        return groupAdmins;
+    }
+
+    private void setMemberRoles () {
+        if (memberRoles == null) {
+            memberRoles = new HashSet<Role>(2);
+            memberRoles.add(roleDao.retrieveRoleById(
+                    PredefinedRole.USER_GROUP_MEMBER.getId()));
+            memberRoles.add(roleDao
+                    .retrieveRoleById(PredefinedRole.VC_ACCESS_MEMBER.getId()));
+        }
+    }
+
+    /**
+     * Group owner is automatically added when creating a group.
+     * Do not include owners in group members.
+     * 
+     * {@link PredefinedRole#USER_GROUP_MEMBER} and
+     * {@link PredefinedRole#VC_ACCESS_MEMBER} roles are
+     * automatically assigned to each group member.
+     * 
+     * {@link PredefinedRole#USER_GROUP_MEMBER} restrict users
+     * to see other group members and allow users to remove
+     * themselves from the groups.
+     * 
+     * {@link PredefinedRole#VC_ACCESS_MEMBER} allow user to
+     * read group query.
+     * 
+     * @see /full/src/main/resources/db/predefined/V3.2__insert_predefined_roles.sql
+     * 
+     * @param createdBy
+     *            the user creating the group
+     * @throws KustvaktException
+     * 
+     * 
+     */
+    public boolean createUpdateUserGroup (String groupName, String description,
+            String createdBy) throws KustvaktException {
+        ParameterChecker.checkNameValue(groupName, "groupName");
+        ParameterChecker.checkStringValue(createdBy, "createdBy");
+
+        if (!groupNamePattern.matcher(groupName).matches()) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "User-group name must consists of alphanumerical characters "
+                            + "(limited to ASCII), underscores, dashes and periods. "
+                            + "The name has to start with an alphanumerical character.",
+                    groupName);
+        }
+
+        UserGroup userGroup = null;
+        boolean groupExists = false;
+        try {
+            userGroup = userGroupDao.retrieveGroupByName(groupName, false);
+            groupExists = true;
+        }
+        catch (KustvaktException e) {
+            if (e.getStatusCode() != StatusCodes.NO_RESOURCE_FOUND) {
+                throw e;
+            }
+        }
+
+        if (!groupExists) {
+            try {
+                userGroupDao.createGroup(groupName, description, createdBy,
+                        UserGroupStatus.ACTIVE);
+                userGroup = userGroupDao.retrieveGroupByName(groupName, false);
+            }
+            // 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());
+            }
+        }
+        else if (description != null) {
+            userGroup.setDescription(description);
+            userGroupDao.updateGroup(userGroup);
+        }
+        return groupExists;
+    }
+
+    public void deleteGroup (String groupName, String username)
+            throws KustvaktException {
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
+                false);
+        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
+            // EM: should this be "not found" instead?
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Group " + userGroup.getName() + " has been deleted.",
+                    userGroup.getName());
+        }
+        else if (userGroup.getCreatedBy().equals(username)
+                || adminDao.isAdmin(username)) {
+            // soft delete
+            userGroupDao.deleteGroup(userGroup.getId(), username,
+                    config.isSoftDeleteGroup());
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public int createAutoHiddenGroup () throws KustvaktException {
+        String code = random.createRandomCode();
+        String groupName = "auto-" + code;
+        int groupId = userGroupDao.createGroup(groupName, "auto-hidden-group",
+                "system", UserGroupStatus.HIDDEN);
+
+        return groupId;
+    }
+
+    public void deleteAutoHiddenGroup (int groupId, String deletedBy)
+            throws KustvaktException {
+        // default hard delete
+        userGroupDao.deleteGroup(groupId, deletedBy,
+                config.isSoftDeleteAutoGroup());
+    }
+
+    /**
+     * Adds a user to the specified usergroup. If the username with
+     * {@link GroupMemberStatus} DELETED exists as a member of the
+     * group,
+     * the entry will be deleted first, and a new entry will be added.
+     * 
+     * If a username with other statuses exists, a KustvaktException
+     * will
+     * be thrown.
+     * 
+     * @see GroupMemberStatus
+     * 
+     * @param username
+     *            a username
+     * @param userGroup
+     *            a user group
+     * @param createdBy
+     *            the user (query-access admin/system) adding the user
+     *            the user-group
+     * @param status
+     *            the status of the membership
+     * @throws KustvaktException
+     */
+    public void inviteGroupMember (String username, UserGroup userGroup,
+            String createdBy, GroupMemberStatus status)
+            throws KustvaktException {
+
+        addGroupMember(username, userGroup, createdBy, status);
+
+        if (config.isMailEnabled()
+                && userGroup.getStatus() != UserGroupStatus.HIDDEN) {
+            mailService.sendMemberInvitationNotification(username,
+                    userGroup.getName(), createdBy);
+        }
+    }
+
+    public void addGroupMember (String username, UserGroup userGroup,
+            String createdBy, GroupMemberStatus status)
+            throws KustvaktException {
+        int groupId = userGroup.getId();
+        ParameterChecker.checkIntegerValue(groupId, "userGroupId");
+
+        GroupMemberStatus existingStatus = memberExists(username, groupId,
+                status);
+        if (existingStatus != null) {
+            throw new KustvaktException(StatusCodes.GROUP_MEMBER_EXISTS,
+                    "Username " + username + " with status " + existingStatus
+                            + " exists in the user-group "
+                            + userGroup.getName(),
+                    username, existingStatus.name(), userGroup.getName());
+        }
+
+        UserGroupMember member = new UserGroupMember();
+        member.setCreatedBy(createdBy);
+        member.setGroup(userGroup);
+        member.setStatus(status);
+        member.setUserId(username);
+        groupMemberDao.addMember(member);
+    }
+
+    private GroupMemberStatus memberExists (String username, int groupId,
+            GroupMemberStatus status) throws KustvaktException {
+        UserGroupMember existingMember;
+        try {
+            existingMember = groupMemberDao.retrieveMemberById(username,
+                    groupId);
+        }
+        catch (KustvaktException e) {
+            return null;
+        }
+
+        GroupMemberStatus existingStatus = existingMember.getStatus();
+        if (existingStatus.equals(GroupMemberStatus.ACTIVE)
+                || existingStatus.equals(status)) {
+            return existingStatus;
+        }
+        else if (existingStatus.equals(GroupMemberStatus.DELETED)) {
+            // hard delete, not customizable
+            doDeleteMember(username, groupId, "system", false);
+        }
+
+        return null;
+    }
+
+    public void inviteGroupMembers (String groupName, String groupMembers,
+            String inviter) throws KustvaktException {
+        String[] members = groupMembers.split(",");
+        ParameterChecker.checkStringValue(groupName, "group name");
+        ParameterChecker.checkStringValue(groupMembers, "members");
+
+        UserGroup userGroup = retrieveUserGroupByName(groupName);
+        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Group " + userGroup.getName() + " has been deleted.",
+                    userGroup.getName());
+        }
+
+        if (isUserGroupAdmin(inviter, userGroup) || adminDao.isAdmin(inviter)) {
+            for (String memberName : members) {
+                inviteGroupMember(memberName, userGroup, inviter,
+                        GroupMemberStatus.PENDING);
+            }
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + inviter, inviter);
+        }
+    }
+
+    private boolean isUserGroupAdmin (String username, UserGroup userGroup)
+            throws KustvaktException {
+
+        List<UserGroupMember> userGroupAdmins = groupMemberDao
+                .retrieveMemberByRole(userGroup.getId(),
+                        PredefinedRole.USER_GROUP_ADMIN.getId());
+
+        for (UserGroupMember admin : userGroupAdmins) {
+            if (username.equals(admin.getUserId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Updates the {@link GroupMemberStatus} of a pending member
+     * to {@link GroupMemberStatus#ACTIVE} and add default member
+     * roles.
+     * 
+     * @param groupId
+     *            groupId
+     * @param username
+     *            the username of the group member
+     * @throws KustvaktException
+     */
+    public void acceptInvitation (String groupName, String username)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(username, "userId");
+        ParameterChecker.checkStringValue(groupName, "groupId");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
+                false);
+        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Group " + userGroup.getName() + " has been deleted.",
+                    userGroup.getName());
+        }
+
+        UserGroupMember member = groupMemberDao.retrieveMemberById(username,
+                userGroup.getId());
+        GroupMemberStatus status = member.getStatus();
+        if (status.equals(GroupMemberStatus.DELETED)) {
+            throw new KustvaktException(StatusCodes.GROUP_MEMBER_DELETED,
+                    username + " has already been deleted from the group "
+                            + userGroup.getName(),
+                    username, userGroup.getName());
+        }
+        else if (member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+            throw new KustvaktException(StatusCodes.GROUP_MEMBER_EXISTS,
+                    "Username " + username + " with status " + status
+                            + " exists in the user-group "
+                            + userGroup.getName(),
+                    username, status.name(), userGroup.getName());
+        }
+        // status pending
+        else {
+            if (DEBUG) {
+                jlog.debug("status: " + member.getStatusDate());
+            }
+            ZonedDateTime expiration = member.getStatusDate().plusMinutes(30);
+            ZonedDateTime now = ZonedDateTime.now();
+            if (DEBUG) {
+                jlog.debug("expiration: " + expiration + ", now: " + now);
+            }
+
+            if (expiration.isAfter(now)) {
+                member.setStatus(GroupMemberStatus.ACTIVE);
+                setMemberRoles();
+                member.setRoles(memberRoles);
+                groupMemberDao.updateMember(member);
+            }
+            else {
+                throw new KustvaktException(StatusCodes.INVITATION_EXPIRED);
+            }
+        }
+    }
+
+    public boolean isMember (String username, UserGroup userGroup)
+            throws KustvaktException {
+        List<UserGroupMember> members = groupMemberDao
+                .retrieveMemberByGroupId(userGroup.getId());
+        for (UserGroupMember member : members) {
+            if (member.getUserId().equals(username)
+                    && member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void deleteGroupMember (String memberId, String groupName,
+            String deletedBy) throws KustvaktException {
+
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
+                false);
+        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Group " + userGroup.getName() + " has been deleted.",
+                    userGroup.getName());
+        }
+        else if (memberId.equals(userGroup.getCreatedBy())) {
+            throw new KustvaktException(StatusCodes.NOT_ALLOWED,
+                    "Operation " + "'delete group owner'" + "is not allowed.",
+                    "delete group owner");
+        }
+        else if (memberId.equals(deletedBy)
+                || isUserGroupAdmin(deletedBy, userGroup)
+                || adminDao.isAdmin(deletedBy)) {
+            // soft delete
+            doDeleteMember(memberId, userGroup.getId(), deletedBy,
+                    config.isSoftDeleteGroupMember());
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + deletedBy, deletedBy);
+        }
+    }
+
+    /**
+     * Updates the {@link GroupMemberStatus} of a member to
+     * {@link GroupMemberStatus#DELETED}
+     * 
+     * @param userId
+     *            user to be deleted
+     * @param groupId
+     *            user-group id
+     * @param deletedBy
+     *            user that issue the delete
+     * @param isSoftDelete
+     *            true if database entry is to be deleted
+     *            permanently, false otherwise
+     * @throws KustvaktException
+     */
+    private void doDeleteMember (String username, int groupId, String deletedBy,
+            boolean isSoftDelete) throws KustvaktException {
+
+        UserGroup group = userGroupDao.retrieveGroupById(groupId);
+
+        UserGroupMember member = groupMemberDao.retrieveMemberById(username,
+                groupId);
+        GroupMemberStatus status = member.getStatus();
+        if (isSoftDelete && status.equals(GroupMemberStatus.DELETED)) {
+            throw new KustvaktException(StatusCodes.GROUP_MEMBER_DELETED,
+                    username + " has already been deleted from the group "
+                            + group.getName(),
+                    username, group.getName());
+        }
+
+        groupMemberDao.deleteMember(member, deletedBy, isSoftDelete);
+    }
+
+    public UserGroupDto searchByName (String groupName)
+            throws KustvaktException {
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName, true);
+        UserGroupDto groupDto = converter.createUserGroupDto(userGroup,
+                userGroup.getMembers(), null, null);
+        return groupDto;
+    }
+
+    public void editMemberRoles (String username, String groupName,
+            String memberUsername, List<Integer> roleIds)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(groupName, "groupName");
+        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName, true);
+        UserGroupStatus groupStatus = userGroup.getStatus();
+        if (groupStatus == UserGroupStatus.DELETED) {
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Usergroup has been deleted.");
+        }
+        else if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+
+            UserGroupMember member = groupMemberDao
+                    .retrieveMemberById(memberUsername, userGroup.getId());
+
+            if (!member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+                throw new KustvaktException(StatusCodes.GROUP_MEMBER_INACTIVE,
+                        memberUsername + " has status " + member.getStatus(),
+                        memberUsername, member.getStatus().name());
+            }
+
+            Set<Role> roles = new HashSet<>();
+            for (int i = 0; i < roleIds.size(); i++) {
+                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
+            }
+            member.setRoles(roles);
+            groupMemberDao.updateMember(member);
+
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public void addMemberRoles (String username, String groupName,
+            String memberUsername, List<Integer> roleIds)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(groupName, "groupName");
+        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName, true);
+        UserGroupStatus groupStatus = userGroup.getStatus();
+        if (groupStatus == UserGroupStatus.DELETED) {
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Usergroup has been deleted.");
+        }
+        else if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+
+            UserGroupMember member = groupMemberDao
+                    .retrieveMemberById(memberUsername, userGroup.getId());
+
+            if (!member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+                throw new KustvaktException(StatusCodes.GROUP_MEMBER_INACTIVE,
+                        memberUsername + " has status " + member.getStatus(),
+                        memberUsername, member.getStatus().name());
+            }
+
+            Set<Role> roles = member.getRoles();
+            for (int i = 0; i < roleIds.size(); i++) {
+                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
+            }
+            member.setRoles(roles);
+            groupMemberDao.updateMember(member);
+
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public void deleteMemberRoles (String username, String groupName,
+            String memberUsername, List<Integer> roleIds)
+            throws KustvaktException {
+
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(groupName, "groupName");
+        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName, true);
+
+        if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+
+            UserGroupMember member = groupMemberDao
+                    .retrieveMemberById(memberUsername, userGroup.getId());
+
+            Set<Role> roles = member.getRoles();
+            Iterator<Role> i = roles.iterator();
+            while (i.hasNext()) {
+                if (roleIds.contains(i.next().getId())) {
+                    i.remove();
+                }
+            }
+
+            member.setRoles(roles);
+            groupMemberDao.updateMember(member);
+
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/service/UserService.java b/src/main/java/de/ids_mannheim/korap/service/UserService.java
new file mode 100644
index 0000000..59e5e5b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/service/UserService.java
@@ -0,0 +1,16 @@
+package de.ids_mannheim.korap.service;
+
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@Service
+public class UserService {
+
+    final static ObjectMapper mapper = new ObjectMapper();
+
+    public JsonNode retrieveUserInfo (String username) {
+        return mapper.createObjectNode().put("username", username);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/DataFactory.java b/src/main/java/de/ids_mannheim/korap/user/DataFactory.java
new file mode 100644
index 0000000..07212bd
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/DataFactory.java
@@ -0,0 +1,248 @@
+package de.ids_mannheim.korap.user;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.validator.Validator;
+
+/**
+ * EM: util class
+ * 
+ * @author hanl, margaretha
+ * @date 27/01/2016
+ */
+public abstract class DataFactory {
+
+    private static DataFactory factory;
+
+    private DataFactory () {}
+
+    public static DataFactory getFactory () {
+        if (factory == null)
+            factory = new DefaultFactory();
+        return factory;
+    }
+
+    /**
+     * if data string null, returns an empty data holding object
+     * 
+     * @param data
+     * @return
+     */
+    public abstract Object convertData (String data);
+
+    public abstract int size (Object data);
+
+    public abstract Set<String> keys (Object data);
+
+    public abstract Collection<Object> values (Object data);
+
+    public abstract Object validate (Object data, Validator validator)
+            throws KustvaktException;
+
+    @Deprecated
+    public abstract Map<String, Object> fields (Object data);
+
+    public abstract Object getValue (Object data, String pointer);
+
+    public abstract boolean addValue (Object data, String field, Object value);
+
+    public abstract boolean removeValue (Object data, String field);
+
+    public abstract String toStringValue (Object data) throws KustvaktException;
+
+    public abstract Object filter (Object data, String ... keys);
+
+    public boolean checkDataType (Object data) {
+        throw new RuntimeException("Wrong data type for factory setting!");
+    }
+
+    /**
+     * updates data1 with values from data2
+     * 
+     * @param data1
+     *            data object that needs update
+     * @param data2
+     *            values that update data1
+     * @return
+     */
+    public abstract Object merge (Object data1, Object data2);
+
+    private static class DefaultFactory extends DataFactory {
+
+        @Override
+        public Object convertData (String data) {
+            if (data == null)
+                return JsonUtils.createObjectNode();
+            try {
+                return JsonUtils.readTree(data);
+            }
+            catch (KustvaktException e) {
+                return null;
+            }
+        }
+
+        @Override
+        public int size (Object data) {
+            if (checkDataType(data))
+                return ((JsonNode) data).size();
+            return -1;
+        }
+
+        @Override
+        public Set<String> keys (Object data) {
+            Set<String> keys = new HashSet<>();
+            if (checkDataType(data) && ((JsonNode) data).isObject()) {
+                Iterator<String> it = ((JsonNode) data).fieldNames();
+                while (it.hasNext())
+                    keys.add((String) it.next());
+            }
+            return keys;
+        }
+
+        @Override
+        public Collection<Object> values (Object data) {
+            return new HashSet<>();
+        }
+
+        @Override
+        public Object validate (Object data, Validator validator)
+                throws KustvaktException {
+            if (checkDataType(data) && ((JsonNode) data).isObject()) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> mdata = JsonUtils
+                            .read(toStringValue(data), HashMap.class);
+                    return validator.validateMap(mdata);
+                }
+                catch (IOException e) {
+                    // do nothing
+                }
+            }
+            return JsonUtils.createObjectNode();
+        }
+
+        @Override
+        public Map<String, Object> fields (Object data) {
+            return new HashMap<>();
+        }
+
+        @Override
+        public Object getValue (Object data, String key) {
+            if (checkDataType(data)) {
+                JsonNode value;
+                if (key.startsWith("/"))
+                    value = ((JsonNode) data).at(key);
+                else
+                    value = ((JsonNode) data).path(key);
+
+                if (value.canConvertToInt())
+                    return value.asInt();
+                else if (value.isBoolean())
+                    return value.asBoolean();
+                else if (value.isTextual())
+                    return value.asText();
+            }
+            return null;
+        }
+
+        //fixme: test that this works with different types
+        @Override
+        public boolean addValue (Object data, String field, Object value) {
+            if (checkDataType(data)) {
+                if (((JsonNode) data).isObject()) {
+                    ObjectNode node = (ObjectNode) data;
+                    if (value instanceof String)
+                        node.put(field, (String) value);
+                    if (value instanceof Boolean)
+                        node.put(field, (Boolean) value);
+                    if (value instanceof Integer)
+                        node.put(field, (Integer) value);
+                    if (value instanceof JsonNode)
+                        node.set(field, (JsonNode) value);
+                    // EM: added
+                    if (value instanceof Collection<?>) {
+                        Collection<?> list = (Collection<?>) value;
+                        ArrayNode arrayNode = JsonUtils.createArrayNode();
+                        for (Object o : list) {
+                            addValue(arrayNode, null, o);
+                        }
+                        node.set(field, arrayNode);
+                    }
+                    return true;
+                }
+                else if (((JsonNode) data).isArray()) {
+                    ArrayNode node = (ArrayNode) data;
+                    if (value instanceof String)
+                        node.add((String) value);
+                    if (value instanceof Boolean)
+                        node.add((Boolean) value);
+                    if (value instanceof Integer)
+                        node.add((Integer) value);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean removeValue (Object data, String field) {
+            if (checkDataType(data) && ((JsonNode) data).isObject()) {
+                ObjectNode node = (ObjectNode) data;
+                node.remove(field);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public String toStringValue (Object data) throws KustvaktException {
+            if (data instanceof JsonNode)
+                return JsonUtils.toJSON(data);
+            return data.toString();
+        }
+
+        @Override
+        public Object filter (Object data, String ... keys) {
+            if (checkDataType(data) && ((JsonNode) data).isObject()) {
+                ObjectNode node = ((JsonNode) data).deepCopy();
+                return node.retain(keys);
+            }
+            return JsonUtils.createObjectNode();
+        }
+
+        @Override
+        public boolean checkDataType (Object data) {
+            if (!(data instanceof JsonNode))
+                super.checkDataType(data);
+            return true;
+        }
+
+        @Override
+        public Object merge (Object data1, Object data2) {
+            if (checkDataType(data1) && checkDataType(data2)) {
+                if (((JsonNode) data1).isObject()
+                        && ((JsonNode) data2).isObject()) {
+                    ((ObjectNode) data1).setAll((ObjectNode) data2);
+                }
+                else if (((JsonNode) data1).isArray()
+                        && ((JsonNode) data2).isArray()) {
+                    ((ArrayNode) data1).addAll((ArrayNode) data2);
+                }
+            }
+            return data1;
+        }
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/DemoUser.java b/src/main/java/de/ids_mannheim/korap/user/DemoUser.java
new file mode 100644
index 0000000..06b1310
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/DemoUser.java
@@ -0,0 +1,27 @@
+package de.ids_mannheim.korap.user;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+public class DemoUser extends User implements Serializable {
+    private static final long serialVersionUID = -5015206272520970500L;
+    public static final String DEMOUSER_NAME = "guest";
+    public static final Integer DEMOUSER_ID = 1654234534;
+    private static final long ACCOUNT_CREATED = 1377102171202L;
+    public static final String PASSPHRASE = "demo";
+
+    protected DemoUser () {
+        super(DEMOUSER_NAME, 2);
+        this.setId(-1);
+        this.setAccountCreation(ACCOUNT_CREATED);
+    }
+
+    protected User clone () {
+        return new DemoUser();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/GenericUserData.java b/src/main/java/de/ids_mannheim/korap/user/GenericUserData.java
new file mode 100644
index 0000000..506e262
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/GenericUserData.java
@@ -0,0 +1,21 @@
+package de.ids_mannheim.korap.user;
+
+/**
+ * Created by hanl on 07.06.16.
+ */
+public class GenericUserData extends Userdata {
+
+    public GenericUserData () {
+        super(-1);
+    }
+
+    @Override
+    public String[] requiredFields () {
+        return new String[0];
+    }
+
+    @Override
+    public String[] defaultFields () {
+        return new String[0];
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/KorAPUser.java b/src/main/java/de/ids_mannheim/korap/user/KorAPUser.java
new file mode 100644
index 0000000..7a4ebec
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/KorAPUser.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.user;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class KorAPUser extends User {
+    private static Logger jlog = LogManager.getLogger(KorAPUser.class);
+    private static final long serialVersionUID = -7108308497625884584L;
+
+    //fixme: accountlink to shibboleth account
+    private String accountLink;
+
+    private String password;
+    private String URIFragment;
+    private Long URIExpiration;
+
+    public KorAPUser (String username) {
+        super(username, 0);
+        this.URIFragment = "";
+        this.URIExpiration = 0L;
+    }
+
+    public KorAPUser (Integer id, String username) {
+        this(username);
+        this.setId(id);
+    }
+
+    public KorAPUser () {
+        super();
+    }
+
+    @Override
+    protected User clone () {
+        KorAPUser user = new KorAPUser(this.getUsername());
+        user.setUsername(this.getUsername());
+        user.setAccountCreation(this.getAccountCreation());
+        return user;
+    }
+
+    @Override
+    public int hashCode () {
+        int result = super.hashCode();
+        result = 31 * result + (jlog != null ? jlog.hashCode() : 0);
+        result = 31 * result + (password != null ? password.hashCode() : 0);
+        result = 31 * result
+                + (URIFragment != null ? URIFragment.hashCode() : 0);
+        result = 31 * result
+                + (URIExpiration != null ? URIExpiration.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals (Object o) {
+        if (this == o)
+            return true;
+        if (!(o instanceof KorAPUser))
+            return false;
+        if (!super.equals(o))
+            return false;
+
+        KorAPUser korAPUser = (KorAPUser) o;
+        if (URIExpiration != korAPUser.URIExpiration)
+            return false;
+        if (URIFragment != null ? !URIFragment.equals(korAPUser.URIFragment)
+                : korAPUser.URIFragment != null)
+            return false;
+        return true;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/ShibbolethUser.java b/src/main/java/de/ids_mannheim/korap/user/ShibbolethUser.java
new file mode 100644
index 0000000..42c2e57
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/ShibbolethUser.java
@@ -0,0 +1,79 @@
+package de.ids_mannheim.korap.user;
+
+/**
+ * User: hanl
+ * Date: 10/16/13
+ * Time: 2:02 PM
+ * 
+ * @author margaretha
+ * @last-update 18/04/2018
+ */
+public class ShibbolethUser extends User {
+
+    /**
+     * Auto generated serial Id
+     */
+    private static final long serialVersionUID = -4008236368010397075L;
+    private String mail;
+    private String affiliation;
+    // EM: common name
+    private String commonName;
+
+    protected ShibbolethUser () {
+        super(1);
+    }
+
+    private ShibbolethUser (String eduPersonID, String mail, String cn,
+                            String affiliation) {
+        this(eduPersonID);
+        this.setUsername(eduPersonID);
+        this.setMail(mail);
+        this.setAffiliation(affiliation);
+        this.setCommonName(cn);
+    }
+
+    public ShibbolethUser (String username) {
+        super(username, 1);
+
+    }
+
+    @Override
+    public String toString () {
+        final StringBuffer sb = new StringBuffer("ShibbolethUser{");
+        sb.append(", mail='").append(getMail()).append('\'');
+        sb.append(", affiliation='").append(getAffiliation()).append('\'');
+        sb.append(", common-name='").append(getCommonName()).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    protected User clone () {
+        return new ShibbolethUser(this.getUsername(), this.getMail(),
+                this.getCommonName(), this.getAffiliation());
+    }
+
+    public String getMail () {
+        return mail;
+    }
+
+    public void setMail (String mail) {
+        this.mail = mail;
+    }
+
+    public String getAffiliation () {
+        return affiliation;
+    }
+
+    public void setAffiliation (String affiliation) {
+        this.affiliation = affiliation;
+    }
+
+    public String getCommonName () {
+        return commonName;
+    }
+
+    public void setCommonName (String commonName) {
+        this.commonName = commonName;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/User.java b/src/main/java/de/ids_mannheim/korap/user/User.java
new file mode 100644
index 0000000..5f46910
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/User.java
@@ -0,0 +1,283 @@
+package de.ids_mannheim.korap.user;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.ParamFields;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import de.ids_mannheim.korap.web.utils.KustvaktMap;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.joda.time.DateTime;
+
+import jakarta.persistence.Entity;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Getter
+@Setter
+@Entity
+public abstract class User implements Serializable {
+
+    //EM: add
+    private String email;
+    //EM: finish
+
+    private Integer id;
+    // in local its username, in shib it's edupersonPrincipalName
+    private String username;
+    private Long accountCreation;
+    private boolean isAccountLocked;
+    private int type;
+    private ParamFields fields;
+    @Getter(AccessLevel.PRIVATE)
+    @Setter(AccessLevel.PRIVATE)
+    private UserSettingProcessor settings;
+    //todo: remove!
+    @Getter(AccessLevel.PRIVATE)
+    @Setter(AccessLevel.PRIVATE)
+    private UserDetails details;
+    @Getter(AccessLevel.PRIVATE)
+    @Setter(AccessLevel.PRIVATE)
+    private List<UserQuery> queries;
+
+    private UserSettingProcessor userSettingProcessor;
+
+    //    private boolean isSystemAdmin;
+
+    // Values for corpusAccess:
+    public enum CorpusAccess {
+        FREE, 	// Access to licence free corpora only, without login   
+        PUB,	// Access to public (= öffentliche Korpora) only, externes Login.
+        ALL 	// Access to all corpora, internes Login.
+    };
+
+    @Getter
+    @Setter
+    private CorpusAccess corpusAccess = CorpusAccess.FREE;
+
+    // values for location (set using the X-forwarded-for Header):
+    public enum Location {
+        INTERN, 	// KorAP accessed by internal Client (inside intranet).
+        EXTERN		// KorAP accessed by external Client (outside intranet).
+    };
+
+    @Getter
+    @Setter
+    private Location location = Location.EXTERN;
+
+    protected User () {
+        this.fields = new ParamFields();
+        this.accountCreation = TimeUtils.getNow().getMillis();
+        this.isAccountLocked = false;
+        this.username = "";
+        this.id = -1;
+        this.location = Location.EXTERN;
+        this.corpusAccess = CorpusAccess.FREE;
+    }
+
+    protected User (int type) {
+        this();
+        this.type = type;
+    }
+
+    protected User (String username, int type) {
+        this(type);
+        this.username = username;
+    }
+
+    public void addField (ParamFields.Param param) {
+        this.fields.add(param);
+    }
+
+    public <T extends ParamFields.Param> T getField (Class<T> cl) {
+        return this.fields.get(cl);
+    }
+
+    public void setId (Integer id) {
+        this.id = id;
+        //        if (this.settings != null)
+        //            this.settings.setUserID(this.id);
+        //        if (this.details != null)
+        //            this.details.setUserID(this.id);
+    }
+
+    public Map<String, Object> toMap () {
+        Map map = new HashMap();
+        map.put(Attributes.USERNAME, this.username);
+        //TimeUtils.format(new DateTime(this.accountCreation))
+        map.put(Attributes.ACCOUNT_CREATION, this.accountCreation);
+
+        //        if (this.getDetails() != null)
+        //            map.putAll(this.getDetails().toMap());
+        return map;
+    }
+
+    public Map toCache () {
+        Map map = new HashMap();
+        map.put(Attributes.ID, this.id);
+        map.put(Attributes.USERNAME, this.username);
+        map.put(Attributes.ACCOUNT_CREATION,
+                TimeUtils.format(new DateTime(this.accountCreation)));
+        map.put(Attributes.ACCOUNTLOCK, this.isAccountLocked);
+        map.put(Attributes.TYPE, this.type);
+        return map;
+    }
+
+    @Override
+    public boolean equals (Object o) {
+        if (this == o)
+            return true;
+        if (!(o instanceof User))
+            return false;
+        User user = (User) o;
+        if (!username.equals(user.username))
+            return false;
+        return true;
+    }
+
+    //    public boolean isAdmin () {
+    //        return this.getUsername().equals(ADMINISTRATOR_ID);
+    //    }
+
+    protected abstract User clone ();
+
+    @Override
+    public String toString () {
+        final StringBuffer sb = new StringBuffer();
+        sb.append("id='").append(id).append('\'');
+        sb.append(", username='").append(username).append('\'');
+        return sb.toString();
+    }
+
+    public String locationtoString ()
+
+    {
+        if (this.location == Location.INTERN)
+            return "INTERN";
+        else if (this.location == Location.EXTERN)
+            return "EXTERN";
+        else
+            return "???";
+    }
+
+    public String accesstoString ()
+
+    {
+        if (this.corpusAccess == CorpusAccess.ALL)
+            return "ALL";
+        else if (this.corpusAccess == CorpusAccess.PUB)
+            return "PUB";
+        else if (this.corpusAccess == CorpusAccess.FREE)
+            return "FREE";
+        else
+            return "???";
+    }
+
+    public static class UserFactory {
+
+        public static KorAPUser getUser (String username) {
+            return new KorAPUser(username);
+        }
+
+        public static KorAPUser getUser (String username, String password) {
+            KorAPUser user = new KorAPUser(username);
+            user.setPassword(password);
+            return user;
+        }
+
+        //        public static KorAPUser getAdmin () {
+        //            return new KorAPUser(ADMINISTRATOR_ID, ADMINISTRATOR_NAME);
+        //        }
+
+        public static DemoUser getDemoUser () {
+            return new DemoUser();
+        }
+
+        public static DemoUser getDemoUser (Integer id) {
+            DemoUser demo = new DemoUser();
+            demo.setId(id);
+            return demo;
+        }
+
+        public static boolean isDemo (String username) {
+            return new DemoUser().getUsername().equalsIgnoreCase(username);
+        }
+
+        //        public static ShibUser getShibInstance (String eduPersonID,
+        //                String mail, String cn) {
+        //            ShibUser u = new ShibUser(eduPersonID);
+        //            u.setAffiliation("");
+        //            u.setMail(mail);
+        //            u.setUsername(eduPersonID);
+        //            u.setCn(cn);
+        //            return u;
+        //        }
+
+        public static KorAPUser toKorAPUser (Map<String, Object> map) {
+            KorAPUser user = UserFactory
+                    .getUser((String) map.get(Attributes.USERNAME));
+            user.setPassword((String) map.get(Attributes.PASSWORD));
+            int id = map.get(Attributes.ID) == null ? -1
+                    : (int) map.get(Attributes.ID);
+            if (id != -1)
+                user.setId(id);
+            long cr = map.get(Attributes.ACCOUNT_CREATION) == null ? -1
+                    : (long) map.get(Attributes.ACCOUNT_CREATION);
+            if (cr != -1)
+                user.setAccountCreation(
+                        (Long) map.get(Attributes.ACCOUNT_CREATION));
+            return user;
+        }
+
+        public static User toUser (Map<String, Object> map) {
+            KustvaktMap kmap = new KustvaktMap(map);
+            int type = map.get(Attributes.TYPE) == null ? 0
+                    : (Integer) kmap.get(Attributes.TYPE, Integer.class);
+            User user;
+            long created = -1;
+            int id = kmap.get(Attributes.ID, Integer.class) == null ? -1
+                    : (Integer) kmap.get(Attributes.ID, Integer.class);
+
+            if (map.get(Attributes.ACCOUNT_CREATION) != null)
+                created = TimeUtils
+                        .getTime(kmap.get(Attributes.ACCOUNT_CREATION))
+                        .getMillis();
+            switch (type) {
+                case 0:
+                    user = UserFactory.getUser(kmap.get(Attributes.USERNAME));
+                    if (id != -1)
+                        user.setId((Integer) kmap.get(Attributes.ID,
+                                Integer.class));
+                    user.setAccountLocked(
+                            map.get(Attributes.ACCOUNTLOCK) == null ? false
+                                    : (Boolean) kmap.get(Attributes.ACCOUNTLOCK,
+                                            Boolean.class));
+                    user.setAccountCreation(created);
+                    break;
+                default:
+                    user = UserFactory.getDemoUser();
+                    user.setAccountCreation(created);
+            }
+            return user;
+        }
+
+        public static KorAPUser toUser (String value) throws KustvaktException {
+            JsonNode node = JsonUtils.readTree(value);
+            KorAPUser user = UserFactory
+                    .getUser(node.path(Attributes.USERNAME).asText());
+            user.setAccountLocked(
+                    node.path(Attributes.ACCOUNTLOCK).asBoolean());
+            user.setAccountLink(node.path(Attributes.ACCOUNTLINK).asText());
+            user.setAccountCreation(
+                    node.path(Attributes.ACCOUNT_CREATION).asLong());
+            user.setPassword(node.path(Attributes.PASSWORD).asText());
+            return user;
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/UserDetails.java b/src/main/java/de/ids_mannheim/korap/user/UserDetails.java
new file mode 100644
index 0000000..9559813
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/UserDetails.java
@@ -0,0 +1,34 @@
+package de.ids_mannheim.korap.user;
+
+import de.ids_mannheim.korap.config.Attributes;
+
+/**
+ * @author hanl
+ * @date 22/01/2016
+ *       persistence issue with query request
+ */
+public class UserDetails extends Userdata {
+
+    public UserDetails () {
+
+    }
+
+    public UserDetails (Integer userid) {
+        super(userid);
+    }
+
+    //todo: make configurable!
+    @Override
+    public String[] requiredFields () {
+        return new String[] { Attributes.EMAIL, Attributes.ADDRESS,
+                Attributes.LASTNAME, Attributes.FIRSTNAME };
+    }
+
+    @Override
+    public String[] defaultFields () {
+        return new String[] { Attributes.EMAIL, Attributes.ADDRESS,
+                Attributes.LASTNAME, Attributes.FIRSTNAME, Attributes.PHONE,
+                Attributes.COUNTRY, Attributes.INSTITUTION, Attributes.GENDER };
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/UserQuery.java b/src/main/java/de/ids_mannheim/korap/user/UserQuery.java
new file mode 100644
index 0000000..10037b2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/UserQuery.java
@@ -0,0 +1,135 @@
+package de.ids_mannheim.korap.user;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * User: hanl
+ * Date: 9/16/13
+ * Time: 4:38 PM
+ */
+@Data
+public class UserQuery {
+
+    private Integer id;
+    private String queryLanguage;
+    private String query;
+    private String name;
+    private String description;
+    private Integer creator;
+
+    public UserQuery (Integer id, int creator) {
+        setId(id);
+        setCreator(creator);
+        setName("");
+        setDescription("");
+        setQuery("");
+        setQueryLanguage("");
+    }
+
+    public UserQuery (String ql, String query, String description) {
+        setDescription(description);
+        setQuery(query);
+        setQueryLanguage(ql);
+    }
+
+    public UserQuery () {
+        setDescription("");
+        setQuery("");
+        setQueryLanguage("");
+        setName("");
+    }
+
+    public void setQuery (String query) {
+        this.query = query;
+        setName("Query: " + query.substring(0,
+                query.length() > 20 ? 20 : query.length()));
+    }
+
+    // todo: use example queries or store in database
+    public static List<UserQuery> demoUserQueries () {
+
+        List<UserQuery> queries = new ArrayList<>();
+        UserQuery q1 = new UserQuery();
+        q1.setQueryLanguage("COSMAS2");
+        q1.setQuery("$wegen #IN(L) <s>");
+        q1.setDescription(
+                "Findet 'wegen' an Satzanfängen. Berücksichtigt auch Groß- und Kleinschreibung");
+
+        //todo: change query
+        UserQuery q2 = new UserQuery();
+        q2.setQueryLanguage("COSMAS2");
+        q2.setQuery("base/cons:Buchstabe base/aggr:Buchstabe");
+
+        UserQuery q3 = new UserQuery();
+        q3.setQueryLanguage("COSMAS2");
+        q3.setDescription("Regular Expression Search");
+        q3.setQuery("id:/WPD_AAA.*/ AND textClass:sport");
+
+        UserQuery q4 = new UserQuery();
+        q4.setQueryLanguage("COSMAS2");
+        q4.setQuery("mpt/syntax_pos:@CC\\|und");
+
+        UserQuery q5 = new UserQuery();
+        q5.setQueryLanguage("COSMAS2");
+        q5.setQuery("VVINF\\|.*en");
+
+        queries.add(q1);
+        //        queries.add(q2);
+        //        queries.add(q3);
+        queries.add(q4);
+        queries.add(q5);
+        return queries;
+    }
+
+    //id is irrevelant, since data was coming
+    // from frontend and thus this object does not contain a id that could be compared!
+    // same with the userAccount. Not set yet!
+    @Override
+    public boolean equals (Object o) {
+        if (this == o)
+            return true;
+        if (!(o instanceof UserQuery))
+            return false;
+        UserQuery userQuery = (UserQuery) o;
+        if (!query.equals(userQuery.query))
+            return false;
+        if (!queryLanguage.equals(userQuery.queryLanguage))
+            return false;
+        return true;
+    }
+
+    @Override
+    public int hashCode () {
+        int result = getId() != null ? getId().hashCode() : 0;
+        result = 31 * result
+                + (queryLanguage != null ? queryLanguage.hashCode() : 0);
+        result = 31 * result + (query != null ? query.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString () {
+        final StringBuffer sb = new StringBuffer("UserQuery{");
+        sb.append("id=").append(getId());
+        //        sb.append(", owner=").append(getOwner());
+        sb.append(", queryLanguage='").append(queryLanguage).append('\'');
+        sb.append(", query='").append(query).append('\'');
+        sb.append(", description='").append(getDescription()).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public Map toMap () {
+        Map map = new HashMap();
+        map.put("name", this.name);
+        map.put("description", this.description);
+        map.put("query", this.query);
+        map.put("queryLanguage", this.queryLanguage);
+        return map;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/UserSettingProcessor.java b/src/main/java/de/ids_mannheim/korap/user/UserSettingProcessor.java
new file mode 100644
index 0000000..3fcd976
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/UserSettingProcessor.java
@@ -0,0 +1,38 @@
+package de.ids_mannheim.korap.user;
+
+import de.ids_mannheim.korap.config.Attributes;
+
+/**
+ * @author hanl, margaretha
+ * @date 28/01/2016
+ */
+public class UserSettingProcessor extends Userdata {
+
+    public UserSettingProcessor () {
+
+    }
+
+    @Deprecated
+    public UserSettingProcessor (Integer userid) {
+        super(userid);
+    }
+
+    // EM: added
+    public UserSettingProcessor (String data) {
+        super(data);
+    }
+
+    @Override
+    public String[] requiredFields () {
+        return new String[] {};
+    }
+
+    @Override
+    public String[] defaultFields () {
+        return new String[] { Attributes.DEFAULT_FOUNDRY_RELATION,
+                Attributes.DEFAULT_FOUNDRY_POS,
+                Attributes.DEFAULT_FOUNDRY_CONSTITUENT,
+                Attributes.DEFAULT_FOUNDRY_LEMMA, Attributes.QUERY_LANGUAGE,
+                Attributes.PAGE_LENGTH };
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/user/Userdata.java b/src/main/java/de/ids_mannheim/korap/user/Userdata.java
new file mode 100644
index 0000000..f607112
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/user/Userdata.java
@@ -0,0 +1,154 @@
+package de.ids_mannheim.korap.user;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.validator.Validator;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.*;
+
+/**
+ * @author hanl, margaretha
+ * @date 22/01/2016
+ * 
+ */
+public abstract class Userdata {
+
+    public DataFactory dataFactory = DataFactory.getFactory();
+
+    @Deprecated
+    @Getter
+    @Setter
+    private Integer id;
+    @Getter(AccessLevel.PRIVATE)
+    private Object data;
+    @Deprecated
+    @Getter
+    @Setter
+    private Integer userId;
+
+    public Userdata () {
+        this(-1);
+    }
+
+    // EM: replace with username
+    @Deprecated
+    public Userdata (Integer userid) {
+        this.userId = userid;
+        this.id = -1;
+        this.data = dataFactory.convertData(null);
+    }
+
+    public Userdata (String data) {
+        this.data = dataFactory.convertData(data);
+    }
+
+    public int size () {
+        return dataFactory.size(this.data);
+    }
+
+    public Object get (String key) {
+        return dataFactory.getValue(this.data, key);
+    }
+
+    public Object filter (String ... keys) {
+        return dataFactory.filter(this.data, keys);
+    }
+
+    /**
+     * 
+     * @return
+     */
+    public boolean isValid () {
+        return findMissingFields().length == 0;
+    }
+
+    public String[] findMissingFields () {
+        Set<String> missing = new HashSet<>();
+        Set<String> keys = dataFactory.keys(this.data);
+        for (String key : requiredFields()) {
+            if (!keys.contains(key))
+                missing.add(key);
+        }
+        return missing.toArray(new String[0]);
+    }
+
+    public void checkRequired () throws KustvaktException {
+        String[] fields = findMissingFields();
+        if (findMissingFields().length != 0) {
+            throw new KustvaktException(userId, StatusCodes.MISSING_PARAMETER,
+                    "User data object not valid. Object has missing fields!",
+                    Arrays.asList(fields).toString());
+        }
+    }
+
+    //fixme: if data array, return empty?!
+    public Set<String> keys () {
+        return dataFactory.keys(this.data);
+    }
+
+    public Collection<Object> values () {
+        return dataFactory.values(this.data);
+    }
+
+    public void setData (String data) {
+        this.data = dataFactory.convertData(data);
+    }
+
+    public void update (Userdata other) {
+        if (other != null && this.getClass().equals(other.getClass()))
+            this.data = dataFactory.merge(this.data, other.data);
+    }
+
+    public String serialize () throws KustvaktException {
+        // to have consistency with required fields --> updates/deletion may cause required fields to be missing.
+        this.checkRequired();
+        return dataFactory.toStringValue(this.data);
+    }
+
+    public void setField (String key, Object value) {
+        dataFactory.addValue(this.data, key, value);
+    }
+
+    // EM: de.ids_mannheim.korap.interfaces.defaults.ApacheValidator.validateMap(Map<String, Object>)
+    // is not reliable
+    // todo: test
+    public void validate (Validator validator) throws KustvaktException {
+        dataFactory.validate(this.data, validator);
+    }
+
+    public void read (Map<String, Object> map, boolean defaults_only)
+            throws KustvaktException {
+        this.readQuietly(map, defaults_only);
+        this.checkRequired();
+    }
+
+    public void readQuietly (Map<String, Object> map, boolean defaults_only) {
+        if (map != null) {
+            if (defaults_only) {
+                for (String k : defaultFields()) {
+                    Object o = map.get(k);
+                    if (o != null) {
+                        dataFactory.addValue(this.data, k, o);
+                    }
+                }
+            }
+            else {
+                for (String key : map.keySet())
+                    dataFactory.addValue(this.data, key, map.get(key));
+            }
+        }
+    }
+
+    // EM: added
+    public boolean removeField (String field) {
+        return dataFactory.removeValue(this.data, field);
+    }
+
+    public abstract String[] requiredFields ();
+
+    public abstract String[] defaultFields ();
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/BooleanUtils.java b/src/main/java/de/ids_mannheim/korap/utils/BooleanUtils.java
new file mode 100644
index 0000000..a682dd3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/BooleanUtils.java
@@ -0,0 +1,24 @@
+package de.ids_mannheim.korap.utils;
+
+/**
+ * @author hanl
+ * @date 19/02/2014
+ */
+public class BooleanUtils {
+
+    public static String dbname;
+
+    public static Object getBoolean (Object val) {
+        if (val == null)
+            val = false;
+        if (dbname != null && dbname.equalsIgnoreCase("sqlite")) {
+            if (val instanceof Boolean) {
+                return ((boolean) val) ? 1 : 0;
+            }
+            else if (val instanceof Integer) {
+                return ((Integer) val == 1);
+            }
+        }
+        return val;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/CollectionTypes.java b/src/main/java/de/ids_mannheim/korap/utils/CollectionTypes.java
new file mode 100644
index 0000000..b4572ac
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/CollectionTypes.java
@@ -0,0 +1,96 @@
+package de.ids_mannheim.korap.utils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 04/12/2013
+ */
+public class CollectionTypes {
+
+    private ObjectMapper mapper;
+
+    public CollectionTypes () {
+        this.mapper = new ObjectMapper();
+    }
+
+    public Map createGroup (String relation, String field, List terms) {
+        if (relation == null)
+            return null;
+
+        Map kgroup = new LinkedHashMap<>();
+        kgroup.put("@type", "korap:group");
+        if (field != null)
+            kgroup.put("@field", "korap:field#" + field);
+        kgroup.put("relation", relation);
+        kgroup.put("operands", terms);
+        return kgroup;
+    }
+
+    public Map createTerm (String field, String subtype, String value,
+            String type) {
+        Map term = new LinkedHashMap<>();
+        if (type == null)
+            type = "korap:term";
+        term.put("@type", type);
+        if (field != null)
+            term.put("@field", "korap:field#" + field);
+        if (subtype != null)
+            term.put("@subtype", "korap:value#" + subtype);
+        term.put("@value", value);
+        return term;
+    }
+
+    public Map createTerm (String field, String value, String type) {
+        return createTerm(field, null, value, type);
+    }
+
+    public Map createTerm (String field, String value) {
+        return createTerm(field, value, null);
+    }
+
+    public Map createResourceFilter (String resource, Map value) {
+        Map meta = new LinkedHashMap();
+        meta.put("@type", "korap:meta-filter");
+        meta.put("@id", "korap-filter#" + resource);
+        meta.put("@value", value);
+        return meta;
+    }
+
+    public Map createResourceFilter (String resource, String value)
+            throws IOException {
+        return createResourceFilter(resource, mapify(value));
+    }
+
+    public Map createResourceExtend (String resource, Map value) {
+        Map meta = new LinkedHashMap();
+        meta.put("@type", "korap:meta-extend");
+        meta.put("@id", "korap-filter#" + resource);
+        meta.put("@value", value);
+        return meta;
+    }
+
+    public Map createMetaFilter (Map value) {
+        Map meta = new LinkedHashMap();
+        meta.put("@type", "korap:meta-filter");
+        meta.put("@value", value);
+        return meta;
+    }
+
+    public Map createMetaExtend (Map value) {
+        Map meta = new LinkedHashMap();
+        meta.put("@type", "korap:meta-extend");
+        meta.put("@value", value);
+        return meta;
+    }
+
+    public Map mapify (String s) throws IOException {
+        return mapper.readValue(s, Map.class);
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/ConcurrentMultiMap.java b/src/main/java/de/ids_mannheim/korap/utils/ConcurrentMultiMap.java
new file mode 100644
index 0000000..12fc139
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/ConcurrentMultiMap.java
@@ -0,0 +1,129 @@
+package de.ids_mannheim.korap.utils;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentMap;
+
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A general purpose Multimap implementation for delayed processing and concurrent insertion/deletes.
+ * This code is based on an implementation by Guido Medina!
+ *
+ * @param <K> A comparable Key
+ * @param <V> A comparable Value
+ */
+
+/**
+ * User: hanl
+ * Date: 8/27/13
+ * Time: 11:18 AM
+ */
+
+public class ConcurrentMultiMap<K extends Comparable, V extends Comparable> {
+
+    private final int initialCapacity;
+    private final LockMap<K> locks;
+    private final ConcurrentMap<K, List<V>> cache;
+
+    public ConcurrentMultiMap () {
+        this(16, 64);
+    }
+
+    public ConcurrentMultiMap (final int concurrencyLevel) {
+        this(concurrencyLevel, 64);
+    }
+
+    public ConcurrentMultiMap (final int concurrencyLevel,
+                               final int initialCapacity) {
+        this.initialCapacity = initialCapacity;
+        cache = new MapMaker().concurrencyLevel(concurrencyLevel)
+                .initialCapacity(initialCapacity).makeMap();
+        locks = new LockMap<K>(concurrencyLevel, initialCapacity);
+    }
+
+    public void put (final K key, final V value) {
+        synchronized (locks.getLock(key)) {
+            List<V> set = cache.get(key);
+            if (set == null) {
+                set = Lists.newArrayListWithExpectedSize(initialCapacity);
+                cache.put(key, set);
+            }
+            set.add(value);
+        }
+    }
+
+    public void putAll (final K key, final Collection<V> values) {
+        synchronized (locks.getLock(key)) {
+            List<V> set = cache.get(key);
+            if (set == null) {
+                set = Lists.newArrayListWithExpectedSize(initialCapacity);
+                cache.put(key, set);
+            }
+            set.addAll(values);
+        }
+    }
+
+    public List<V> remove (final K key) {
+        synchronized (locks.getLock(key)) {
+            return cache.remove(key);
+        }
+    }
+
+    public void remove (final K key, final V value) {
+        List<V> values = cache.get(key);
+        synchronized (locks.getLock(key)) {
+            values.remove(value);
+        }
+    }
+
+    public Set<K> getKeySet () {
+        return cache.keySet();
+    }
+
+    public int size () {
+        return cache.size();
+    }
+
+    public boolean containsKey (K key) {
+        return cache.containsKey(key);
+    }
+
+    public List<V> get (K key) {
+        return cache.get(key);
+    }
+
+    public class LockMap<K extends Comparable> {
+        private final ConcurrentMap<K, Object> locks;
+
+        public LockMap () {
+            this(16, 64);
+        }
+
+        public LockMap (final int concurrencyLevel) {
+            this(concurrencyLevel, 64);
+        }
+
+        public LockMap (final int concurrencyLevel, final int initialCapacity) {
+            locks = new MapMaker().concurrencyLevel(concurrencyLevel)
+                    .initialCapacity(initialCapacity).weakValues().makeMap();
+        }
+
+        public Object getLock (final K key) {
+            final Object object = new Object();
+            Object lock = locks.putIfAbsent(key, object);
+            return lock == null ? object : lock;
+        }
+
+    }
+
+    public String toString () {
+        return cache.toString();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/IPNetMask.java b/src/main/java/de/ids_mannheim/korap/utils/IPNetMask.java
new file mode 100644
index 0000000..133c330
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/IPNetMask.java
@@ -0,0 +1,119 @@
+package de.ids_mannheim.korap.utils;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * User: hanl
+ * Date: 9/13/13
+ * Time: 2:25 PM
+ * 
+ * currently only supports IPv4!
+ * 
+ */
+// todo: integrate to gerrit
+public class IPNetMask {
+
+    private final Inet4Address i4addr;
+    private final byte maskCtr;
+
+    private final int addrInt;
+    private final int maskInt;
+
+    private static final int default_mask = 16;
+
+    private IPNetMask (Inet4Address i4addr, byte mask) {
+        this.i4addr = i4addr;
+        this.maskCtr = mask;
+
+        this.addrInt = addrToInt(i4addr);
+        this.maskInt = ~((1 << (32 - maskCtr)) - 1);
+    }
+
+    /**
+     * IPNetMask factory method.
+     * 
+     * @param addrSlashMask
+     *            IP/Mask String in format "nnn.nnn.nnn.nnn/mask". If
+     *            the "/mask" is omitted, "/32" (just the single
+     *            address) is assumed.
+     * @return a new IPNetMask
+     * @throws UnknownHostException
+     *             if address part cannot be parsed by
+     *             InetAddress
+     */
+    public static IPNetMask getIPMask (String addrSlashMask)
+            throws UnknownHostException {
+        int pos = addrSlashMask.indexOf('/');
+        String addr;
+        byte maskCtr;
+        if (pos == -1) {
+            addr = addrSlashMask;
+            maskCtr = default_mask;
+        }
+        else {
+            addr = addrSlashMask.substring(0, pos);
+            maskCtr = Byte.parseByte(addrSlashMask.substring(pos + 1));
+        }
+
+        return new IPNetMask((Inet4Address) InetAddress.getByName(addr),
+                maskCtr);
+    }
+
+    /**
+     * Test given IPv4 address against this IPNetMask object.
+     * 
+     * @param testAddr
+     *            address to isSystem.
+     * @return true if address is in the IP Mask range, false if not.
+     */
+    public boolean matches (Inet4Address testAddr) {
+        int testAddrInt = addrToInt(testAddr);
+        return ((addrInt & maskInt) == (testAddrInt & maskInt));
+    }
+
+    /**
+     * Convenience method that converts String host to IPv4 address.
+     * 
+     * @param addr
+     *            IP address to match in nnn.nnn.nnn.nnn format or
+     *            hostname.
+     * @return true if address is in the IP Mask range, false if not.
+     * @throws UnknownHostException
+     *             if the string cannot be decoded.
+     */
+    public boolean matches (String addr) throws UnknownHostException {
+        return matches((Inet4Address) InetAddress.getByName(addr));
+    }
+
+    /**
+     * Converts IPv4 address to integer representation.
+     */
+    private int addrToInt (Inet4Address i4addr) {
+        byte[] ba = i4addr.getAddress();
+        return (ba[0] << 24) | ((ba[1] & 0xFF) << 16) | ((ba[2] & 0xFF) << 8)
+                | (ba[3] & 0xFF);
+    }
+
+    @Override
+    public String toString () {
+        return i4addr.getHostAddress() + "/" + maskCtr;
+    }
+
+    @Override
+    public boolean equals (Object obj) {
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        final IPNetMask that = (IPNetMask) obj;
+        return (this.addrInt == that.addrInt && this.maskInt == that.maskInt);
+    }
+
+    @Override
+    public int hashCode () {
+        return this.maskInt + this.addrInt;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/JerseyUtils.java b/src/main/java/de/ids_mannheim/korap/utils/JerseyUtils.java
new file mode 100644
index 0000000..08f73d1
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/JerseyUtils.java
@@ -0,0 +1,43 @@
+package de.ids_mannheim.korap.utils;
+
+import org.glassfish.jersey.message.internal.MediaTypes;
+import org.glassfish.jersey.server.ContainerRequest;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.MediaType;
+
+public class JerseyUtils {
+
+    /**
+     * Get the form parameters of the request entity.
+     * <p>
+     * This method will ensure that the request entity is buffered
+     * such that it may be consumed by the application.
+     *
+     * @return the form parameters, if there is a request entity and
+     *         the
+     *         content type is "application/x-www-form-urlencoded",
+     *         otherwise an
+     *         instance containing no parameters will be returned.
+     */
+    public static Form getFormParameters (
+            ContainerRequestContext requestContext) {
+        if (requestContext instanceof ContainerRequest) {
+            return getFormParameters((ContainerRequest) requestContext);
+        }
+        return new Form();
+    }
+
+    private static Form getFormParameters (ContainerRequest request) {
+        if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE,
+                request.getMediaType())) {
+            request.bufferEntity();
+            Form form = request.readEntity(Form.class);
+            return (form == null ? new Form() : form);
+        }
+        else {
+            return new Form();
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/JsonUtils.java b/src/main/java/de/ids_mannheim/korap/utils/JsonUtils.java
new file mode 100644
index 0000000..ab67779
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/JsonUtils.java
@@ -0,0 +1,116 @@
+package de.ids_mannheim.korap.utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+
+/**
+ * @author hanl
+ * @date 28/01/2014
+ */
+public class JsonUtils {
+    private static ObjectMapper mapper = new ObjectMapper();
+
+    private JsonUtils () {}
+
+    public static String toJSON (Object values) throws KustvaktException {
+        try {
+            return mapper.writeValueAsString(values);
+        }
+        catch (JsonProcessingException e) {
+            throw new KustvaktException(StatusCodes.SERIALIZATION_FAILED,
+                    "Failed serializing object in json", e);
+        }
+    }
+
+    public static JsonNode readTree (String json) throws KustvaktException {
+        try {
+            return mapper.readTree(json);
+        }
+        catch (IOException e) {
+            throw new KustvaktException(StatusCodes.DESERIALIZATION_FAILED,
+                    "Failed deserializing json object: " + json, json, e);
+        }
+    }
+
+    public static ObjectNode createObjectNode () {
+        return mapper.createObjectNode();
+    }
+
+    public static ArrayNode createArrayNode () {
+        return mapper.createArrayNode();
+    }
+
+    public static JsonNode valueToTree (Object value) {
+        return mapper.valueToTree(value);
+    }
+
+    public static <T> T convert (JsonNode json, Class<T> cl)
+            throws IOException {
+        return mapper.convertValue(json, cl);
+    }
+
+    public static <T> T read (String json, Class<T> cl) throws IOException {
+        return mapper.readValue(json, cl);
+    }
+
+    public static <T> T read (InputStream is, Class<T> cl) throws IOException {
+        return mapper.readValue(is, cl);
+    }
+
+    public static <T> T readFile (String path, Class<T> clazz)
+            throws IOException {
+        return mapper.readValue(new File(path), clazz);
+    }
+
+    public static void writeFile (String path, Object content)
+            throws IOException {
+        mapper.writeValue(new File(path), content);
+    }
+
+    public static <T> T convertToClass (String json, Class<T> cl)
+            throws KustvaktException {
+        T t = null;
+        try {
+            t = mapper.readValue(json, cl);
+        }
+        catch (IOException e) {
+            throw new KustvaktException(StatusCodes.DESERIALIZATION_FAILED,
+                    e.getMessage(), json, e);
+        }
+        return t;
+    }
+
+    public static List<Map<String, Object>> convertToList (String json)
+            throws JsonProcessingException, KustvaktException {
+        List d = new ArrayList();
+        JsonNode node = JsonUtils.readTree(json);
+        if (node.isArray()) {
+            Iterator<JsonNode> nodes = node.iterator();
+            while (nodes.hasNext()) {
+                Map<String, Object> map = mapper.treeToValue(nodes.next(),
+                        Map.class);
+                d.add(map);
+            }
+        }
+        else if (node.isObject()) {
+            Map<String, Object> map = mapper.treeToValue(node, Map.class);
+            d.add(map);
+        }
+        return d;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/KoralCollectionQueryBuilder.java b/src/main/java/de/ids_mannheim/korap/utils/KoralCollectionQueryBuilder.java
new file mode 100644
index 0000000..7dd99ad
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/KoralCollectionQueryBuilder.java
@@ -0,0 +1,223 @@
+package de.ids_mannheim.korap.utils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.query.serialize.CollectionQueryProcessor;
+import de.ids_mannheim.korap.response.Notifications;
+import edu.emory.mathcs.backport.java.util.Arrays;
+
+/**
+ * convenience builder class for collection query
+ * 
+ * @author hanl, margaretha
+ * @date 29/06/2017
+ */
+public class KoralCollectionQueryBuilder {
+
+    public enum EQ {
+        EQUAL, UNEQUAL
+    }
+
+    private boolean verbose;
+    private JsonNode base;
+    private StringBuilder builder;
+    private String mergeOperator;
+
+    public KoralCollectionQueryBuilder () {
+        this(false);
+    }
+
+    public KoralCollectionQueryBuilder (boolean verbose) {
+        this.verbose = verbose;
+        this.builder = new StringBuilder();
+        this.base = null;
+        this.mergeOperator = null;
+    }
+
+    /**
+     * raw method for field - value pair adding. Supports all
+     * operators (leq, geq, contains, etc.)
+     * 
+     * @param field
+     * @param op
+     * @param value
+     * @return
+     */
+    public KoralCollectionQueryBuilder with (String field, String op,
+            String value) {
+        //String end = this.builder.substring(this.builder.length() - 4,
+        //        this.builder.length() - 1);
+        //if (this.builder.length() != 0
+        //        && (!end.contains("&") | !end.contains("|")))
+        //    throw new RuntimeException("no join operator given!");
+        this.with(field + op + value);
+        return this;
+    }
+
+    /**
+     * element can be a more complex sub query like
+     * (textClass=freizeit & Attributes.CORPUS_SIGLE=WPD)
+     * 
+     * @param query
+     *            will be enclosed by parenthesis in order to make sub
+     *            query
+     *            element
+     * @return
+     */
+    public KoralCollectionQueryBuilder with (String query) {
+        if (!query.startsWith("(") && !query.endsWith(")"))
+            query = "(" + query + ")";
+        this.builder.append(query);
+        return this;
+    }
+
+    public KoralCollectionQueryBuilder and () {
+        if (this.builder.length() != 0)
+            this.builder.append(" & ");
+        if (this.base != null && this.mergeOperator == null)
+            this.mergeOperator = "AND";
+        return this;
+    }
+
+    public KoralCollectionQueryBuilder or () {
+        if (this.builder.length() != 0)
+            this.builder.append(" | ");
+        if (this.base != null && this.mergeOperator == null)
+            this.mergeOperator = "OR";
+        return this;
+    }
+
+    public JsonNode rebaseCollection () throws KustvaktException {
+        if (this.builder.length() == 0 && this.base == null)
+            return null;
+
+        JsonNode request = null;
+        if (this.builder.length() != 0) {
+            CollectionQueryProcessor tree = new CollectionQueryProcessor(
+                    this.verbose);
+            tree.process(this.builder.toString());
+            if (tree.getErrors().size() > 0) {
+                // legacy support
+                for (List<Object> e : tree.getErrors()) {
+                    if (e.get(1) instanceof String[]) {
+                        Notifications notif = new Notifications();
+                        int code = (int) e.get(0);
+                        notif.addError(code, (String[]) e.get(1));
+                        String notificationStr = notif.toJsonString();
+                        throw new KustvaktException(
+                                StatusCodes.SERIALIZATION_FAILED,
+                                notificationStr, true);
+                    }
+                    else {
+                        break;
+                    }
+                }
+                // -- end of legacy support
+
+                Map<String, Object> map = new HashMap<>();
+                map.put("errors", tree.getErrors());
+                String errors = JsonUtils.toJSON(map);
+                throw new KustvaktException(StatusCodes.SERIALIZATION_FAILED,
+                        errors, true);
+            }
+            request = JsonUtils.valueToTree(tree.getRequestMap());
+        }
+
+        if (this.base != null) {
+            // check that collection non empty
+            JsonNode tmp = this.base.deepCopy();
+            if (request != null)
+                request = mergeWith(request);
+            else
+                request = tmp;
+        }
+        return request;
+    }
+
+    public JsonNode mergeWith (JsonNode node) {
+        if (this.base != null) {
+            if (node != null) {
+                JsonNode tobase = node.at("/collection");
+                JsonNode base = this.base.deepCopy();
+                JsonNode result = base.at("/collection");
+
+                if (result.isMissingNode() && !tobase.isMissingNode())
+                    result = tobase;
+                else if (result.isMissingNode() && tobase.isMissingNode())
+                    return base;
+                else {
+                    result = JsonBuilder
+                            .buildDocGroup(this.mergeOperator != null
+                                    ? this.mergeOperator.toLowerCase()
+                                    : "and", result, tobase);
+                }
+                ((ObjectNode) base).put("collection", result);
+                return base;
+            }
+            return this.base;
+        }
+        throw new RuntimeException("no query found to merge with!");
+    }
+
+    /**
+     * sets base query. All consequent queries are added to the first
+     * koral:docGroup within the collection base query
+     * If no group in base query, consequent queries are skipped.
+     * 
+     * @param query
+     * @throws KustvaktException
+     */
+    public KoralCollectionQueryBuilder setBaseQuery (String query)
+            throws KustvaktException {
+        this.base = JsonUtils.readTree(query);
+        return this;
+    }
+
+    public KoralCollectionQueryBuilder setBaseQuery (JsonNode query) {
+        this.base = query;
+        return this;
+    }
+
+    public String toJSON () throws KustvaktException {
+        return JsonUtils.toJSON(rebaseCollection());
+    }
+
+    @Override
+    public String toString () {
+        return this.builder.toString();
+    }
+
+    private static class JsonBuilder {
+
+        public static ObjectNode buildDoc (String key, String value) {
+            ObjectNode node = JsonUtils.createObjectNode();
+            node.put("@type", "koral:doc");
+            // eq.equals(EQ.EQUAL) ? "match:eq" : "match:ne"
+            node.put("match", "match:eq");
+            node.put("key", key);
+            node.put("value", value);
+            return node;
+        }
+
+        public static ObjectNode buildDocGroup (String op,
+                JsonNode ... groups) {
+            ObjectNode node = JsonUtils.createObjectNode();
+            node.put("@type", "koral:docGroup");
+            node.put("operation", "operation:" + op);
+            ArrayNode ops = JsonUtils.createArrayNode();
+            ops.addAll(Arrays.asList(groups));
+            node.put("operands", ops);
+            return node;
+        }
+
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/NamingUtils.java b/src/main/java/de/ids_mannheim/korap/utils/NamingUtils.java
new file mode 100644
index 0000000..dae6908
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/NamingUtils.java
@@ -0,0 +1,15 @@
+package de.ids_mannheim.korap.utils;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Created by hanl on 14.05.16.
+ */
+public class NamingUtils {
+
+    private NamingUtils () {}
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/ParameterChecker.java b/src/main/java/de/ids_mannheim/korap/utils/ParameterChecker.java
new file mode 100644
index 0000000..9b98faa
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/ParameterChecker.java
@@ -0,0 +1,61 @@
+package de.ids_mannheim.korap.utils;
+
+import java.util.Collection;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+
+public class ParameterChecker {
+
+    public static void checkObjectValue (Object obj, String name)
+            throws KustvaktException {
+        if (obj == null) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " is null", name);
+        }
+    }
+
+    public static void checkCollection (Collection<?> collection, String name)
+            throws KustvaktException {
+        if (collection == null) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " is null", name);
+        }
+        else if (collection.isEmpty()) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " is empty", name);
+        }
+    }
+
+    public static void checkStringValue (String string, String name)
+            throws KustvaktException {
+        if (string == null) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " is null", name);
+        }
+        else if (string.isEmpty()) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " is empty", name);
+        }
+    }
+
+    public static void checkIntegerValue (int integer, String name)
+            throws KustvaktException {
+        if (integer == 0) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    name + " is missing", name);
+        }
+    }
+
+    public static void checkNameValue (String value, String name)
+            throws KustvaktException {
+        if (value == null) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " is null", name);
+        }
+        else if (value.length() < 3) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    name + " must contain at least 3 characters", name);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/PrefixTreeMap.java b/src/main/java/de/ids_mannheim/korap/utils/PrefixTreeMap.java
new file mode 100644
index 0000000..7cfcb17
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/PrefixTreeMap.java
@@ -0,0 +1,44 @@
+package de.ids_mannheim.korap.utils;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * @author hanl
+ * @date 01/07/2014
+ */
+public class PrefixTreeMap<V> extends TreeMap<String, V> {
+
+    public SortedMap<String, V> getPrefixSubMap (String prefix) {
+        if (prefix != null && prefix.length() > 0) {
+            SortedMap d = this.subMap(prefix, getEnd(prefix));
+            if (d.isEmpty())
+                return null;
+            return d;
+        }
+        return null;
+    }
+
+    private String getEnd (String prefix) {
+        char nextLetter = (char) (prefix.charAt(prefix.length() - 1) + 1);
+        return prefix.substring(0, prefix.length() - 1) + nextLetter;
+
+    }
+
+    public V getFirstValue (String prefix) {
+        if (prefix.length() > 0) {
+            String first = this.subMap(prefix, getEnd(prefix)).firstKey();
+            return this.get(first);
+        }
+        return null;
+    }
+
+    public V getLastValue (String prefix) {
+        if (prefix.length() > 0) {
+            String last = this.subMap(prefix, getEnd(prefix)).lastKey();
+            return this.get(last);
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/PropertyReader.java b/src/main/java/de/ids_mannheim/korap/utils/PropertyReader.java
new file mode 100644
index 0000000..b377cd1
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/PropertyReader.java
@@ -0,0 +1,36 @@
+package de.ids_mannheim.korap.utils;
+
+import de.ids_mannheim.korap.config.BeanInjectable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author hanl
+ * @date 27/09/2014
+ */
+public abstract class PropertyReader {
+
+    protected Map<String, Properties> read (String path) throws IOException {
+        Map<String, Properties> res = new HashMap<>();
+        Properties s = new Properties();
+        s.load(new FileInputStream(new File(path)));
+        for (Map.Entry<Object, Object> e : s.entrySet()) {
+            String key = e.getKey().toString().split("\\.")[0];
+            Properties in = res.get(key);
+            if (in == null) {
+                in = new Properties();
+                res.put(key, in);
+            }
+            in.setProperty(e.getKey().toString(), e.getValue().toString());
+        }
+        return res;
+    }
+
+    public abstract void load ();
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/ServiceInfo.java b/src/main/java/de/ids_mannheim/korap/utils/ServiceInfo.java
new file mode 100644
index 0000000..7d6f8a3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/ServiceInfo.java
@@ -0,0 +1,88 @@
+package de.ids_mannheim.korap.utils;
+
+import de.ids_mannheim.korap.config.ConfigLoader;
+import de.ids_mannheim.korap.query.serialize.QuerySerializer;
+import lombok.Getter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * @author hanl
+ * @date 23/01/2014
+ */
+public class ServiceInfo {
+
+    private static final ServiceInfo info = new ServiceInfo();
+
+    public static final String UNKNOWN = "UNKNOWN";
+
+    @Getter
+    private String name;
+    @Getter
+    private String version;
+    @Getter
+    private String config;
+    @Getter
+    private String logger;
+    @Getter
+    private Boolean cacheable;
+    @Getter
+    private String cache_store;
+
+    @Getter
+    private String krillVersion;
+    @Getter
+    private String koralVersion;
+
+    private ServiceInfo () {
+        load();
+    }
+
+    private void load () {
+        Properties props = new Properties();
+        try {
+
+            InputStream stream = getStream();
+            props.load(stream);
+            stream.close();
+            this.version = (String) props.get("kustvakt.version");
+            this.name = (String) props.get("kustvakt.name");
+            this.config = (String) props.get("kustvakt.properties");
+            this.logger = (String) props.get("kustvakt.logging");
+            this.cacheable = Boolean
+                    .valueOf((String) props.get("kustvakt.cache"));
+            this.cache_store = (String) props.get("kustvakt.cache_store");
+
+            this.krillVersion = (String) props.get("krill.version");
+
+            QuerySerializer s = new QuerySerializer();
+            this.koralVersion = s.getVersion();
+        }
+        catch (IOException e) {
+            this.version = UNKNOWN;
+            this.name = UNKNOWN;
+            this.logger = UNKNOWN;
+            this.config = UNKNOWN;
+            this.cacheable = false;
+            this.cache_store = UNKNOWN;
+
+            this.koralVersion = UNKNOWN;
+            this.krillVersion = UNKNOWN;
+        }
+    }
+
+    private static InputStream getStream () throws IOException {
+        String path = "service.properties";
+        InputStream stream = ConfigLoader.loadConfigStream(path);
+        if (stream == null)
+            throw new IOException(
+                    "stream for resource " + path + " could not be found...");
+        return stream;
+    }
+
+    public static ServiceInfo getInfo () {
+        return info;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/SqlBuilder.java b/src/main/java/de/ids_mannheim/korap/utils/SqlBuilder.java
new file mode 100644
index 0000000..5b31f51
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/SqlBuilder.java
@@ -0,0 +1,95 @@
+package de.ids_mannheim.korap.utils;
+
+/**
+ * @author hanl
+ * @date 26/11/2015
+ */
+public class SqlBuilder {
+
+    private StringBuffer buffer;
+    private String table;
+    private String[] fields;
+    private String where;
+
+    public SqlBuilder (String table) {
+        this.buffer = new StringBuffer();
+        this.table = table;
+    }
+
+    public SqlBuilder select (String ... fields) {
+        this.buffer.append("SELECT ");
+        if (fields.length > 0) {
+            for (int i = 0; i < fields.length; i++) {
+                if (i > 0)
+                    this.buffer.append(", ");
+                this.buffer.append(fields[i]);
+            }
+        }
+        else
+            this.buffer.append("*");
+        this.buffer.append(" FROM ").append(table);
+        return this;
+    }
+
+    public SqlBuilder update (String ... fields) {
+        this.buffer.append("UPDATE ").append(table);
+        this.fields = fields;
+        return this;
+    }
+
+    public SqlBuilder insert (String ... fields) {
+        this.buffer.append("INSERT INTO ").append(table);
+        this.fields = fields;
+        return this;
+    }
+
+    public SqlBuilder delete () {
+        this.buffer.append("DELETE FROM ").append(table);
+        return this;
+    }
+
+    public SqlBuilder params (String ... values) {
+        if (values.length != fields.length)
+            return this;
+        if (this.buffer.lastIndexOf("INSERT INTO") != -1) {
+            this.buffer.append(" (");
+            for (int i = 0; i < this.fields.length; i++) {
+                if (i > 0)
+                    this.buffer.append(", ");
+                this.buffer.append(fields[i]);
+            }
+
+            StringBuffer b = new StringBuffer();
+            for (int i = 0; i < values.length; i++) {
+                if (i > 0)
+                    b.append(", ");
+                b.append(values[i]);
+            }
+            this.buffer.append(") VALUES (").append(b.toString()).append(")");
+        }
+        if (this.buffer.lastIndexOf("UPDATE") != -1) {
+            this.buffer.append(" SET ");
+            for (int i = 0; i < this.fields.length; i++) {
+                if (i > 0)
+                    this.buffer.append(", ");
+                this.buffer.append(fields[i]).append("=").append(values[i]);
+            }
+        }
+        return this;
+    }
+
+    public SqlBuilder where (String where) {
+        this.where = where;
+        return this;
+    }
+
+    @Override
+    public String toString () {
+        StringBuffer b = new StringBuffer(this.buffer);
+        //exclude where clauses from insert statements
+        if (this.where != null && this.buffer.lastIndexOf("INSERT INTO") == -1)
+            b.append(" WHERE ").append(where);
+        return b.append(";").toString();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/StringUtils.java b/src/main/java/de/ids_mannheim/korap/utils/StringUtils.java
new file mode 100644
index 0000000..dd6259a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/StringUtils.java
@@ -0,0 +1,205 @@
+package de.ids_mannheim.korap.utils;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+public class StringUtils {
+    private final static Logger jlog = LogManager.getLogger(StringUtils.class);
+
+    private static final String SEP = ";";
+    private static final String SLASH = "/";
+
+    public static Collection<UUID> stringToUUIDList (String s) {
+        String[] array = s.split(SEP);
+        List<UUID> list = new LinkedList<>();
+        for (String att : array) {
+            list.add(UUID.fromString(att));
+        }
+        return list;
+    }
+
+    public static List<String> toList (String values) {
+        List<String> list = new ArrayList<>();
+        StringTokenizer tokenizer = new StringTokenizer(values, SEP);
+        while (tokenizer.hasMoreTokens())
+            list.add(tokenizer.nextToken());
+        return list;
+    }
+
+    public static Set<String> toSet (String values, String sep) {
+        Set<String> set = new HashSet<>();
+        if (values != null && !values.isEmpty()) {
+            StringTokenizer tokenizer = new StringTokenizer(values, sep);
+            while (tokenizer.hasMoreTokens())
+                set.add(tokenizer.nextToken());
+        }
+        return set;
+    }
+
+    public static Set<String> toSet (String values) {
+        return toSet(values, SEP);
+    }
+
+    public static String toString (Collection<String> values) {
+        return StringUtils.toString(values, SEP);
+    }
+
+    public static String toString (Collection<String> values, String sep) {
+        StringBuffer b = new StringBuffer();
+        for (String s : values)
+            b.append(s).append(sep);
+
+        if (b.length() > 0)
+            b.deleteCharAt(b.length() - 1);
+        return b.toString();
+    }
+
+    public static String orderedToString (Collection<String> hash) {
+        Set<String> orderedSet = new TreeSet<>();
+        orderedSet.addAll(hash);
+        if (orderedSet.isEmpty()) {
+            return "";
+        }
+        else {
+            StringBuilder builder = new StringBuilder();
+            for (String s : orderedSet) {
+                builder.append(s);
+                builder.append(SEP);
+            }
+            builder.deleteCharAt(builder.length() - 1);
+            return builder.toString();
+        }
+    }
+
+    public static String UUIDsetToString (Collection<UUID> hash) {
+        Set<UUID> orderedSet = new TreeSet<>();
+        orderedSet.addAll(hash);
+        if (orderedSet.isEmpty()) {
+            return "";
+        }
+        else {
+            StringBuilder builder = new StringBuilder();
+            for (UUID s : orderedSet) {
+                builder.append(s);
+                builder.append(SEP);
+            }
+            builder.deleteCharAt(builder.length() - 1);
+            return builder.toString();
+        }
+    }
+
+    public static String buildSQLRegex (String path) {
+        StringBuilder b = new StringBuilder();
+        String[] match = path.split("/");
+        b.append(match[0]);
+        b.append("(" + "/" + match[1] + ")");
+        b.append("*$");
+        return b.toString();
+    }
+
+    // todo: move to parameter utils
+    public static boolean isInteger (String value) {
+        try {
+            Integer.valueOf(value);
+            return true;
+        }
+        catch (IllegalArgumentException e) {
+            // do nothing!
+            return false;
+        }
+    }
+
+    public static String normalize (String value) {
+        return value.trim().toLowerCase();
+    }
+
+    public static String normalizeHTML (String value) {
+        return StringEscapeUtils.escapeHtml(value);
+    }
+
+    public static String decodeHTML (String value) {
+        return StringEscapeUtils.unescapeHtml(value);
+    }
+
+    public static String getDocSigle (String textSigle) {
+        if (textSigle != null)
+            return textSigle.split("\\.")[0];
+        return null;
+    }
+
+    public static String getCorpusSigle (String textSigle) {
+        //WPD_SSS.07367
+        if (textSigle != null)
+            return textSigle.split("_")[0];
+        return null;
+    }
+
+    public static Collection<String> joinStringSet (Collection<String> source,
+            String other) {
+        Set<String> set = new HashSet<>(source);
+        set.add(other);
+        return set;
+    }
+
+    public static Collection<UUID> joinUUIDSet (Collection<UUID> source,
+            UUID other) {
+        Set<UUID> set = new HashSet<>(source);
+        set.add(other);
+        return set;
+    }
+
+    public static String joinResources (String first, String second) {
+        String res;
+        if (first != null && !first.isEmpty())
+            res = first + SLASH + second;
+        else
+            res = second;
+        return res.replaceAll("\\s", "");
+    }
+
+    public static String[] splitAnnotations (String joined) {
+        String[] spl = joined.split(SLASH);
+        if (spl.length == 2)
+            return spl;
+        else
+            return null;
+    }
+
+    public static String stripTokenType (String token) {
+        int idx = token.lastIndexOf(" ");
+        if (idx == -1)
+            return token;
+        return token.substring(idx).replaceAll("\\s", "");
+    }
+
+    public static String getTokenType (String token) {
+        if (token.contains(" "))
+            return token.substring(0, token.lastIndexOf(" "))
+                    .replaceAll("\\s", "").toLowerCase();
+        else
+            return null;
+    }
+
+    public static String toSHAHash (String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            md.update(input.getBytes());
+            byte[] mdbytes = md.digest();
+
+            StringBuffer sb = new StringBuffer();
+            for (int i = 0; i < mdbytes.length; i++)
+                sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16)
+                        .substring(1));
+            return sb.toString();
+        }
+        catch (NoSuchAlgorithmException e) {
+            return null;
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/utils/TimeUtils.java b/src/main/java/de/ids_mannheim/korap/utils/TimeUtils.java
new file mode 100644
index 0000000..0a5f5e3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/utils/TimeUtils.java
@@ -0,0 +1,213 @@
+package de.ids_mannheim.korap.utils;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+import de.ids_mannheim.korap.config.Attributes;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * @author hanl
+ *         <p/>
+ *         calculates current, expiration and inactive time for
+ *         security
+ *         purposes.
+ * @return
+ */
+public class TimeUtils {
+
+    private static DecimalFormat df = new DecimalFormat("#.#############");
+    private static DateTimeFormatter dtf = DateTimeFormat
+            .forPattern("dd/MM/yyyy");
+    private static final DateTimeZone dtz = DateTimeZone
+            .forID(Attributes.DEFAULT_TIME_ZONE);
+    private static final boolean DEBUG = false;
+    private static Logger jlog = LogManager.getLogger(TimeUtils.class);
+
+    public static int convertTimeToSeconds (String expirationVal) {
+        expirationVal = expirationVal.trim();
+        int finIndex = expirationVal.length() - 1;
+        char entity = expirationVal.charAt(finIndex);
+        int returnSec = Integer.valueOf(expirationVal.substring(0, finIndex));
+        if (DEBUG) {
+            jlog.debug("setting time value to " + returnSec + " with time in "
+                    + entity);
+        }
+        switch (entity) {
+            case 'D':
+                return returnSec * 60 * 60 * 24;
+            case 'H':
+                return returnSec * 60 * 60;
+            case 'M':
+                return returnSec * 60;
+            case 'S':
+                return returnSec;
+            default:
+                if (DEBUG) {
+                    jlog.debug(
+                            "no time unit specified. Trying to read from default (minutes)");
+                }
+                return Integer.valueOf(expirationVal) * 60;
+        }
+
+    }
+
+    public static DateTime getNow () {
+        return DateTime.now(dtz);
+    }
+
+    public static DateTime getTime (String time) {
+        return DateTime.parse(time).withZone(dtz);
+    }
+
+    public static DateTime getTime (long time) {
+        return new DateTime(time).withZone(dtz);
+    }
+
+    //returns difference in milliseconds
+    public static long calcDiff (DateTime now, DateTime future) {
+        long diff = (future.withZone(dtz).getMillis()
+                - now.withZone(dtz).getMillis());
+        return diff;
+    }
+
+    public static boolean isExpired (long time) {
+        return getNow().isAfter(time);
+
+    }
+
+    // returns difference in seconds in floating number
+    public static float floating (DateTime past, DateTime now) {
+        long diff = (now.withZone(dtz).getMillis()
+                - past.withZone(dtz).getMillis());
+        double fin = diff / 1000.0;
+        BigDecimal bd = new BigDecimal(fin).setScale(8, RoundingMode.HALF_EVEN);
+        return bd.floatValue();
+    }
+
+    public static DateTime fromCosmas (String date) {
+        int idx = date.length();
+        try {
+            Integer sec = Integer.valueOf(
+                    date.substring((idx = idx - 2), date.length()).trim());
+            Integer min = Integer
+                    .valueOf(date.substring((idx = idx - 2), idx + 2).trim());
+            Integer hours = Integer
+                    .valueOf(date.substring((idx = idx - 2), idx + 2).trim());
+            Integer day = Integer
+                    .valueOf(date.substring((idx = idx - 2), idx + 2).trim());
+            Integer month = Integer
+                    .valueOf(date.substring((idx = idx - 2), idx + 2).trim());
+            Integer year = Integer
+                    .valueOf(date.substring((idx = idx - 4), idx + 4).trim());
+            return new DateTime(year, month, day, hours, min, sec);
+        }
+        catch (NumberFormatException e) {
+            return getNow().toDateTime();
+        }
+    }
+
+    public static String formatDiff (DateTime now, DateTime after) {
+        return df.format(calcDiff(now, after));
+    }
+
+    /**
+     * converts time to the ISO8601 standard.
+     * 
+     * @param time
+     * @return
+     */
+    public static String format (DateTime time) {
+        DateTimeFormatter fmt = ISODateTimeFormat.dateTime();
+        return fmt.withZone(dtz).print(time);
+    }
+
+    public static String format (long time) {
+        DateTimeFormatter fmt = ISODateTimeFormat.dateTime();
+        return fmt.withZone(dtz).print(time);
+    }
+
+    /**
+     * calculate expiration time
+     * 
+     * @param creation
+     * @param plus
+     *            time in seconds
+     * @return
+     */
+    public static DateTime plusSeconds (long creation, int plus) {
+        return new DateTime(creation).withZone(dtz).plusSeconds(plus);
+    }
+
+    public static DateTime getExpiration (long now, int exp) {
+        return new DateTime(now).withZone(dtz).plusSeconds(exp);
+    }
+
+    /**
+     * @param plus
+     * @return
+     */
+    public static DateTime plusSeconds (int plus) {
+        return getNow().withZone(dtz).plusSeconds(plus);
+    }
+
+    public static DateTime plusHours (int hours) {
+        return getNow().withZone(dtz).plusHours(hours);
+    }
+
+    public static DateTime plusMinutes (int minutes) {
+        return getNow().withZone(dtz).plusMinutes(minutes);
+    }
+
+    /**
+     * create time stamp from long value
+     * 
+     * @param t
+     *            time
+     * @return Timestamp
+     */
+    public static LocalDate getTimeStamp (long t) {
+        return new DateTime(t).withZone(dtz).toLocalDate();
+    }
+
+    public static DateTime getDate (int day, int month, int year) {
+        DateTime date = new DateTime().withZone(dtz);
+        return date.withDate(year, month, day);
+    }
+
+    public static String toString (long val, Locale locale) {
+        if (locale == Locale.GERMAN)
+            return new DateTime(val).toString("dd. MMMM yyyy, HH:mm",
+                    Locale.GERMAN);
+        else
+            return new DateTime(val).toString("MM-dd-yyyy, hh:mm",
+                    Locale.ENGLISH);
+
+    }
+
+    public static String dateToString (long val, int i) {
+        switch (i) {
+            case 1:
+                return new DateTime(val).toString("yyyy-MM");
+            case 2:
+                return new DateTime(val).toString("yyyy-MM-dd");
+            default:
+                return new DateTime(val).toString("yyyy");
+        }
+    }
+
+    private static final List<DateTime> times = new ArrayList<>();
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/validator/ApacheValidator.java b/src/main/java/de/ids_mannheim/korap/validator/ApacheValidator.java
new file mode 100644
index 0000000..cbf7ffc
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/validator/ApacheValidator.java
@@ -0,0 +1,128 @@
+package de.ids_mannheim.korap.validator;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.validator.routines.DateValidator;
+import org.apache.commons.validator.routines.EmailValidator;
+import org.apache.commons.validator.routines.RegexValidator;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.ConfigLoader;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.web.utils.KustvaktMap;
+
+/**
+ * Created by hanl on 09.06.16.
+ * 
+ */
+public class ApacheValidator implements Validator {
+
+    private static Logger jlog = LogManager.getLogger(ApacheValidator.class);
+
+    private static final String STRING_PATTERN = "^[\\.;:,&\\|@\\[\\]\\=\\*\\/\\/_()\\-0-9\\p{L}\\p{Space}]{0,1024}$";
+
+    private static final boolean DEBUG = false;
+
+    private Map<String, RegexValidator> validators;
+
+    public ApacheValidator () throws IOException {
+        this.validators = load();
+    }
+
+    private static Map<String, RegexValidator> load () throws IOException {
+        Map<String, RegexValidator> validatorMap = new HashMap<>();
+        Properties p = ConfigLoader.loadProperties("validation.properties");
+
+        for (String property : p.stringPropertyNames()) {
+            if (property.startsWith("Validator")) {
+                String name = property.replace("Validator.", "");
+                RegexValidator v = new RegexValidator(
+                        p.get(property).toString());
+                validatorMap.put(name, v);
+            }
+        }
+        return validatorMap;
+    }
+
+    @Override
+    public Map<String, Object> validateMap (Map<String, Object> map)
+            throws KustvaktException {
+        Map<String, Object> safeMap = new HashMap<>();
+        KustvaktMap kmap = new KustvaktMap(map);
+
+        if (map != null) {
+            loop: for (String key : kmap.keySet()) {
+                Object value = kmap.getRaw(key);
+                if (value instanceof List) {
+                    List list = (List) value;
+                    for (int i = 0; i < list.size(); i++) {
+                        if (!isValid(String.valueOf(list.get(i)), key)) {
+                            //                                list.remove(i);
+                            throw new KustvaktException(
+                                    StatusCodes.ILLEGAL_ARGUMENT,
+                                    "The value for the parameter " + key
+                                            + " is not valid or acceptable.");
+                        }
+                    }
+
+                    if (list.size() == 1)
+                        value = list.get(0);
+                    else
+                        value = list;
+                }
+                else {
+                    if (!isValid(kmap.get(key), key))
+                        continue loop;
+                }
+                safeMap.put(key, value);
+            }
+        }
+        return safeMap;
+    }
+
+    @Override
+    public String validateEntry (String input, String type)
+            throws KustvaktException {
+        if (!isValid(input, type))
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "Entry did not validate for type '" + type + "'", input);
+        return input;
+    }
+
+    @Override
+    public boolean isValid (String input, String type) {
+        boolean valid = false;
+        RegexValidator validator = this.validators.get(type);
+        if (validator != null) {
+            valid = validator.isValid(input);
+        }
+        else {
+            if (Attributes.EMAIL.equals(type)) {
+                valid = EmailValidator.getInstance().isValid(input);
+            }
+            else if ("date".equals(type)) {
+                valid = DateValidator.getInstance().isValid(input);
+            }
+            else if ("string".equals(type)
+                    && !this.validators.containsKey("string")) {
+                RegexValidator regex = new RegexValidator(STRING_PATTERN);
+                valid = regex.isValid(input);
+            }
+            else
+                return this.isValid(input, "string");
+        }
+        if (DEBUG) {
+            jlog.debug("validating entry " + input + " of type " + type + ": "
+                    + (valid ? "Is valid!" : "Is not valid!"));
+        }
+
+        return valid;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/validator/Validator.java b/src/main/java/de/ids_mannheim/korap/validator/Validator.java
new file mode 100644
index 0000000..3bd42a5
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/validator/Validator.java
@@ -0,0 +1,23 @@
+package de.ids_mannheim.korap.validator;
+
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+
+/**
+ * EM: made this as a spring component
+ * 
+ * Created by hanl on 08.06.16.
+ */
+@Component
+public interface Validator {
+
+    Map<String, Object> validateMap (Map<String, Object> map)
+            throws KustvaktException;
+
+    String validateEntry (String input, String type) throws KustvaktException;
+
+    boolean isValid (String input, String type);
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/ClientsHandler.java b/src/main/java/de/ids_mannheim/korap/web/ClientsHandler.java
new file mode 100644
index 0000000..16cfcd1
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/ClientsHandler.java
@@ -0,0 +1,57 @@
+package de.ids_mannheim.korap.web;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.MultivaluedMap;
+
+/**
+ * @author hanl
+ * @date 10/12/2013
+ */
+// use for Piotr Ps. rest api connection
+public class ClientsHandler {
+
+    private WebTarget service;
+
+    public ClientsHandler (URI address) {
+        Client client = ClientBuilder.newClient();
+        this.service = client.target(address);
+    }
+
+    public String getResponse (String path, String key, Object value)
+            throws KustvaktException {
+        try {
+            return service.path(path).queryParam(key, value).request()
+                    .get(String.class);
+        }
+        catch (WebApplicationException e) {
+            throw new KustvaktException(StatusCodes.INVALID_REQUEST);
+        }
+    }
+
+    public String getResponse (MultivaluedMap<String, String> map,
+            String ... paths) throws KustvaktException {
+        try {
+            WebTarget resource = service;
+            for (String p : paths)
+                resource = resource.path(p);
+            for (Map.Entry<String, List<String>> e : map.entrySet()) {
+                for (String value : e.getValue())
+                    resource = resource.queryParam(e.getKey(), value);
+            }
+            return resource.request().get(String.class);
+        }
+        catch (WebApplicationException e) {
+            throw new KustvaktException(StatusCodes.INVALID_REQUEST);
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/CoreResponseHandler.java b/src/main/java/de/ids_mannheim/korap/web/CoreResponseHandler.java
new file mode 100644
index 0000000..920d38b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/CoreResponseHandler.java
@@ -0,0 +1,86 @@
+package de.ids_mannheim.korap.web;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.response.Notifications;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Response;
+
+/**
+ * @author hanl, margaretha
+ * @date 29/01/2014
+ * @last 04/12/2017
+ */
+public class CoreResponseHandler {
+
+    public WebApplicationException throwit (KustvaktException e) {
+        Response s;
+        if (e.hasNotification()) {
+            if (e.getStatusCode() != null) {
+                s = Response.status(getStatus(e.getStatusCode()))
+                        .entity(e.getNotification()).build();
+            }
+            // KustvaktException just wraps another exception 
+            else {
+                s = Response.status(Response.Status.BAD_REQUEST)
+                        .entity(e.getNotification()).build();
+            }
+        }
+        else {
+            s = Response.status(getStatus(e.getStatusCode()))
+                    .entity(buildNotification(e)).build();
+        }
+        return new WebApplicationException(s);
+    }
+
+    public WebApplicationException throwit (int code) {
+        return new WebApplicationException(Response.status(getStatus(code))
+                .entity(buildNotification(code, "", "")).build());
+    }
+
+    public WebApplicationException throwit (int code, String message,
+            String entity) {
+        return new WebApplicationException(Response.status(getStatus(code))
+                .entity(buildNotification(code, message, entity)).build());
+    }
+
+    public WebApplicationException throwit (int code, String notification) {
+        return new WebApplicationException(
+                Response.status(getStatus(code)).entity(notification).build());
+    }
+
+    protected String buildNotification (KustvaktException e) {
+        return buildNotification(e.getStatusCode(), e.getMessage(),
+                e.getEntity());
+    }
+
+    public static String buildNotification (int code, String message,
+            String entity) {
+        Notifications notif = new Notifications();
+        notif.addError(code, message, entity);
+        return notif.toJsonString() + "\n";
+    }
+
+    protected Response.Status getStatus (int code) {
+        Response.Status status = Response.Status.BAD_REQUEST;
+        switch (code) {
+            // case StatusCodes.NO_VALUE_FOUND:
+            // status = Response.Status.NO_CONTENT;
+            // break;
+            case StatusCodes.ILLEGAL_ARGUMENT:
+                status = Response.Status.NOT_ACCEPTABLE;
+                break;
+            case StatusCodes.STATUS_OK:
+                status = Response.Status.OK;
+                break;
+            // EM: Added 
+            case StatusCodes.NO_RESOURCE_FOUND:
+                status = Response.Status.NOT_FOUND;
+                break;
+            case StatusCodes.CACHING_VC:
+                status = Response.Status.SERVICE_UNAVAILABLE;
+                break;
+        }
+        return status;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java b/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
new file mode 100644
index 0000000..1118b26
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
@@ -0,0 +1,65 @@
+package de.ids_mannheim.korap.web;
+
+import java.util.EnumSet;
+
+import de.ids_mannheim.korap.constant.AuthenticationScheme;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.ResponseBuilder;
+
+/**
+ * KustvaktResponseHandler includes exceptions regarding
+ * authorization.
+ * 
+ * @author margaretha
+ *
+ */
+public class KustvaktResponseHandler extends CoreResponseHandler {
+
+    @Override
+    public WebApplicationException throwit (KustvaktException e) {
+        Response r;
+
+        // KustvaktException just wraps another exception
+        if (e.getStatusCode() == null && e.hasNotification()) {
+            r = Response.status(Response.Status.BAD_REQUEST)
+                    .entity(e.getNotification()).build();
+        }
+        else if (e.getStatusCode() == StatusCodes.USER_REAUTHENTICATION_REQUIRED
+                || e.getStatusCode() == StatusCodes.AUTHORIZATION_FAILED
+                || e.getStatusCode() >= StatusCodes.AUTHENTICATION_FAILED) {
+            String notification = buildNotification(e.getStatusCode(),
+                    e.getMessage(), e.getEntity());
+            r = createUnauthenticatedResponse(notification);
+        }
+        else if (e.hasNotification()) {
+            r = Response.status(getStatus(e.getStatusCode()))
+                    .entity(e.getNotification()).build();
+        }
+        else {
+            String notification = buildNotification(e.getStatusCode(),
+                    e.getMessage(), e.getEntity());
+            r = Response.status(getStatus(e.getStatusCode()))
+                    .entity(notification).build();
+        }
+        return new WebApplicationException(r);
+    }
+
+    public Response createUnauthenticatedResponse (String notification) {
+        ResponseBuilder builder = Response.status(Response.Status.UNAUTHORIZED);
+
+        EnumSet<AuthenticationScheme> schemes = EnumSet
+                .allOf(AuthenticationScheme.class);
+        schemes.remove(AuthenticationScheme.API);
+
+        for (AuthenticationScheme s : schemes) {
+            builder = builder.header(HttpHeaders.WWW_AUTHENTICATE,
+                    s.displayName() + " realm=\"Kustvakt\"");
+        }
+
+        return builder.entity(notification).build();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/OAuth2ResponseHandler.java b/src/main/java/de/ids_mannheim/korap/web/OAuth2ResponseHandler.java
new file mode 100644
index 0000000..826a625
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/OAuth2ResponseHandler.java
@@ -0,0 +1,117 @@
+package de.ids_mannheim.korap.web;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.http.HttpHeaders;
+
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.ResponseBuilder;
+import jakarta.ws.rs.core.Response.Status;
+
+/**
+ * OAuth2ResponseHandler builds {@link Response}s and handles
+ * exceptions by building
+ * OAuth error responses accordingly.
+ * 
+ * <br/><br/>
+ * 
+ * OAuth2 error response consists of error (required),
+ * error_description (optional) and error_uri (optional).
+ * 
+ * @see OAuth2Error
+ * 
+ * @author margaretha
+ *
+ */
+public class OAuth2ResponseHandler extends KustvaktResponseHandler {
+    @Override
+    public WebApplicationException throwit (KustvaktException e) {
+        return throwit(e, null);
+    }
+
+    public WebApplicationException throwit (KustvaktException e, String state) {
+        String errorCode = e.getEntity();
+        int responseStatus = e.getResponseStatus();
+
+        Response r = null;
+        if (responseStatus > 0) {
+            r = createResponse(e, Status.fromStatusCode(responseStatus), state);
+        }
+        else if (errorCode == null) {
+            return super.throwit(e);
+        }
+        else if (errorCode.equals(OAuth2Error.INVALID_CLIENT.getCode())
+                || errorCode.equals(OAuth2Error.UNAUTHORIZED_CLIENT.getCode())
+                || errorCode.equals(
+                        de.ids_mannheim.korap.oauth2.constant.OAuth2Error.INVALID_TOKEN)) {
+            r = createResponse(e, Status.UNAUTHORIZED, state);
+        }
+        else if (errorCode.equals(OAuth2Error.INVALID_GRANT.getCode())
+                || errorCode.equals(OAuth2Error.INVALID_REQUEST.getCode())
+                || errorCode.equals(OAuth2Error.INVALID_SCOPE.getCode())
+                || errorCode
+                        .equals(OAuth2Error.UNSUPPORTED_GRANT_TYPE.getCode())
+                || errorCode
+                        .equals(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE.getCode())
+                || errorCode.equals(OAuth2Error.ACCESS_DENIED.getCode())) {
+            r = createResponse(e, Status.BAD_REQUEST, state);
+        }
+        else if (errorCode.equals(
+                de.ids_mannheim.korap.oauth2.constant.OAuth2Error.INSUFFICIENT_SCOPE)) {
+            r = createResponse(e, Status.FORBIDDEN, state);
+        }
+        else if (errorCode.equals(OAuth2Error.SERVER_ERROR.getCode())) {
+            r = createResponse(e, Status.INTERNAL_SERVER_ERROR, state);
+        }
+        else if (errorCode
+                .equals(OAuth2Error.TEMPORARILY_UNAVAILABLE.getCode())) {
+            r = createResponse(e, Status.SERVICE_UNAVAILABLE, state);
+        }
+        else {
+            return super.throwit(e);
+        }
+        return new WebApplicationException(r);
+    }
+
+    public Response sendRedirect (URI locationUri) {
+        ResponseBuilder builder = Response.temporaryRedirect(locationUri);
+        return builder.build();
+    }
+
+    private Response createResponse (KustvaktException e, Status statusCode,
+            String state) {
+        ErrorObject eo = new ErrorObject(e.getEntity(), e.getMessage());
+        if (state != null && !state.isEmpty()) {
+            Map<String, String> map = new HashMap<String, String>();
+            map.put("state", state);
+            eo = eo.setCustomParams(map);
+        }
+        return createResponse(statusCode, eo.toJSONObject().toJSONString());
+    }
+
+    public Response createResponse (AccessTokenResponse tokenResponse) {
+        String jsonString = tokenResponse.toJSONObject().toJSONString();
+        return createResponse(Status.OK, jsonString);
+    }
+
+    private Response createResponse (Status status, String entity) {
+        ResponseBuilder builder = Response.status(status);
+        builder.entity(entity);
+        builder.header(HttpHeaders.CACHE_CONTROL, "no-store");
+        builder.header(HttpHeaders.PRAGMA, "no-store");
+
+        if (status == Status.UNAUTHORIZED) {
+            builder.header(HttpHeaders.WWW_AUTHENTICATE,
+                    "Basic realm=\"Kustvakt\"");
+        }
+        return builder.build();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java b/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java
new file mode 100644
index 0000000..38e9c9a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/SearchKrill.java
@@ -0,0 +1,377 @@
+// Connector to the Lucene Backend
+package de.ids_mannheim.korap.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.lucene.store.MMapDirectory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.Krill;
+import de.ids_mannheim.korap.KrillCollection;
+import de.ids_mannheim.korap.KrillIndex;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.response.Match;
+import de.ids_mannheim.korap.response.MetaFields;
+import de.ids_mannheim.korap.response.Result;
+import de.ids_mannheim.korap.util.QueryException;
+
+/**
+ * The SearchKrill class allows for searching in the
+ * Lucene based Krill backend by applying KoralQuery.
+ * 
+ * @author Nils Diewald
+ */
+public class SearchKrill {
+    private final static Logger jlog = LogManager.getLogger(SearchKrill.class);
+
+    private static final boolean DEBUG = false;
+
+    public static KrillIndex index;
+
+    /**
+     * Constructor
+     */
+    // todo: use korap.config to get index location
+    public SearchKrill (String path) {
+
+        try {
+            if (path.equals(":temp:")) {
+                index = new KrillIndex();
+            }
+            else {
+                File f = new File(path);
+                jlog.info("Loading index from " + path);
+                if (!f.exists()) {
+                    jlog.error("Index not found: " + path + "!");
+                    System.exit(-1);
+                }
+                index = new KrillIndex(new MMapDirectory(Paths.get(path)));
+            };
+        }
+        catch (IOException e) {
+            jlog.error("Unable to loadSubTypes index:" + e.getMessage());
+        };
+    };
+
+    public KrillIndex getIndex () {
+        return index;
+    };
+
+    public void closeIndexReader () throws KustvaktException {
+        try {
+            index.closeReader();
+        }
+        catch (IOException e) {
+            throw new KustvaktException(500, "Failed closing index reader");
+        }
+    }
+
+    /**
+     * Search in the Lucene index.
+     * 
+     * @param json
+     *            JSON-LD string with search and potential meta
+     *            filters.
+     */
+    public String search (String json) {
+        if (DEBUG) {
+            jlog.debug(json);
+        }
+        if (index != null) {
+            String result = new Krill(json).apply(index).toJsonString();
+            if (DEBUG) {
+                jlog.debug(result);
+            }
+            return result;
+        }
+        Result kr = new Result();
+        kr.addError(601, "Unable to find index");
+        return kr.toJsonString();
+    };
+
+    /**
+     * Search in the Lucene index and return matches as token lists.
+     * 
+     * @param json
+     *            JSON-LD string with search and potential meta
+     *            filters.
+     */
+    @Deprecated
+    public String searchTokenList (String json) {
+        if (DEBUG) {
+            jlog.debug(json);
+        }
+        if (index != null)
+            return new Krill(json).apply(index).toTokenListJsonString();
+        Result kr = new Result();
+        kr.addError(601, "Unable to find index");
+        return kr.toJsonString();
+    };
+
+    /**
+     * Get info on a match - by means of a richly annotated html
+     * snippet.
+     * 
+     * @param id
+     *            match id
+     * @param availabilityList
+     * @throws KustvaktException
+     */
+    public String getMatch (String id, Pattern licensePattern)
+            throws KustvaktException {
+        Match km;
+        if (index != null) {
+            try {
+                km = index.getMatch(id);
+                String availability = km.getAvailability();
+                checkAvailability(licensePattern, availability, id);
+            }
+            catch (QueryException qe) {
+                km = new Match();
+                km.addError(qe.getErrorCode(), qe.getMessage());
+            }
+        }
+        else {
+            km = new Match();
+            km.addError(601, "Unable to find index");
+        }
+        return km.toJsonString();
+    };
+
+    private void checkAvailability (Pattern licensePattern, String availability,
+            String id) throws KustvaktException {
+        if (DEBUG) {
+            jlog.debug("pattern: " + licensePattern.toString()
+                    + ", availability: " + availability);
+        }
+        if (licensePattern != null && availability != null) {
+            Matcher m = licensePattern.matcher(availability);
+            if (!m.matches()) {
+                if (availability.isEmpty()) {
+                    throw new KustvaktException(StatusCodes.MISSING_ATTRIBUTE,
+                            "Availability for " + id + "is empty.", id);
+                }
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Retrieving resource with ID " + id
+                                + " is not allowed.",
+                        id);
+            }
+        }
+
+    }
+
+    /*
+     * Retrieve the meta fields for a certain document
+     */
+    public String getFields (String id, List<String> fields,
+            Pattern licensePattern) throws KustvaktException {
+        MetaFields meta;
+
+        // No index found
+        if (index == null) {
+            meta = new MetaFields(id);
+            meta.addError(601, "Unable to find index");
+        }
+
+        // Index available
+        else if (fields != null) {
+            // Get fields
+            meta = index.getFields(id, fields);
+        }
+        else {
+            // Get fields
+            meta = index.getFields(id);
+        }
+
+        // EM: this approach forbids the whole metadata
+        // this should be refined by filtering out only the restricted
+        // metadata fields
+        // String availability = meta.getFieldValue("availability");
+        // checkAvailability(licensePattern, availability, id);
+
+        return meta.toJsonString();
+    };
+
+    public String getMatch (String id, boolean info, List<String> foundries,
+            List<String> layers, boolean includeSpans, boolean includeSnippet,
+            boolean includeTokens, boolean includeHighlights,
+            boolean sentenceExpansion, Pattern licensePattern)
+            throws KustvaktException {
+        Match km;
+        if (index != null) {
+            try {
+                km = index.getMatchInfo(id, "tokens", info, foundries, layers,
+                        includeSpans, includeSnippet, includeTokens,
+                        includeHighlights, sentenceExpansion);
+                String availability = km.getAvailability();
+                checkAvailability(licensePattern, availability, id);
+            }
+            catch (QueryException qe) {
+                km = new Match();
+                km.addError(qe.getErrorCode(), qe.getMessage());
+            }
+        }
+        else {
+            km = new Match();
+            km.addError(601, "Unable to find index");
+        }
+        return km.toJsonString();
+    };
+
+    /**
+     * Get info on a match - by means of a richly annotated html
+     * snippet.
+     * 
+     * @param id
+     *            match id
+     * @param foundry
+     *            the foundry of interest - may be null
+     * @param layer
+     *            the layer of interest - may be null
+     * @param includeSpans
+     *            Should spans be included (or only token infos)?
+     * @param includeHighlights
+     *            Should highlight markup be included?
+     */
+    public String getMatch (String id, String foundry, String layer,
+            boolean includeSpans, boolean includeHighlights,
+            boolean sentenceExpansion) {
+
+        if (index != null) {
+            try {
+                /*
+                  For multiple foundries/layers use
+                  String idString,
+                  "tokens",
+                  true,
+                  ArrayList<String> foundry,
+                  ArrayList<String> layer,
+                  boolean includeSpans,
+                  boolean includeHighlights,
+                  boolean extendToSentence
+                */
+                return index.getMatchInfo(id, "tokens", foundry, layer,
+                        includeSpans, includeHighlights, sentenceExpansion)
+                        .toJsonString();
+            }
+            catch (QueryException qe) {
+                Match km = new Match();
+                km.addError(qe.getErrorCode(), qe.getMessage());
+                return km.toJsonString();
+            }
+        };
+        Match km = new Match();
+        km.addError(601, "Unable to find index");
+        return km.toJsonString();
+    };
+
+    /**
+     * Get statistics on (virtual) collections.
+     * 
+     * EM: might be changed later
+     * 
+     * @param json
+     *            JSON-LD string with potential meta filters.
+     * @throws KustvaktException
+     */
+    public String getStatistics (String json) throws KustvaktException {
+        if (index == null) {
+            return "{\"documents\" : -1, error\" : \"No index given\" }";
+        };
+
+        // Define a virtual corpus
+        KrillCollection kc;
+        if (json != null && !json.equals("")) {
+            if (DEBUG) {
+                jlog.debug(json);
+            }
+
+            // Create Virtual collection from json search
+            kc = new KrillCollection(json);
+        }
+
+        // There is no json string defined
+        else {
+
+            // Create Virtual collection of everything
+            kc = new KrillCollection();
+        };
+
+        // Set index
+        kc.setIndex(index);
+        long docs = 0, tokens = 0, sentences = 0, paragraphs = 0;
+        // Get numbers from index (currently slow)
+        try {
+            docs = kc.numberOf("documents");
+            if (docs > 0) {
+                tokens = kc.numberOf("tokens");
+                sentences = kc.numberOf("base/sentences");
+                paragraphs = kc.numberOf("base/paragraphs");
+            };
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        };
+
+        if (kc.hasErrors()) {
+            throw new KustvaktException(
+                    "{\"errors\":" + kc.getErrors().toJsonString() + "}");
+        }
+        // Build json response
+        StringBuilder sb = new StringBuilder("{");
+        sb.append("\"documents\":").append(docs).append(",\"tokens\":")
+                .append(tokens).append(",\"sentences\":").append(sentences)
+                .append(",\"paragraphs\":").append(paragraphs).append("}");
+        return sb.toString();
+    };
+
+    /**
+     * Return the match identifier as a string.
+     * This is a convenient method to deal with legacy instantiation
+     * of the
+     * code.
+     */
+    public String getMatchId (String corpusID, String docID, String textID,
+            String matchID) {
+        // Create a string representation of the match
+        StringBuilder sb = new StringBuilder();
+        sb.append("match-").append(corpusID).append('/').append(docID)
+                .append('/').append(textID).append('-').append(matchID);
+        return sb.toString();
+    };
+
+    /**
+     * Return the text sigle as a string.
+     */
+    public String getTextSigle (String corpusID, String docID, String textID) {
+        // Create a string representation of the match
+        StringBuilder sb = new StringBuilder();
+        sb.append(corpusID).append('/').append(docID).append('/')
+                .append(textID);
+        return sb.toString();
+    };
+
+    /**
+     * Return the fingerprint of the latest index revision.
+     */
+    public String getIndexFingerprint () {
+        if (index != null) {
+            return index.getFingerprint();
+        };
+        return "null";
+    }
+
+    public JsonNode getFieldValuesForVC (String koralQuery, String fieldName) {
+        return new Krill().retrieveFieldValues(koralQuery, index, fieldName);
+    }
+
+};
diff --git a/src/main/java/de/ids_mannheim/korap/web/TRACE.java b/src/main/java/de/ids_mannheim/korap/web/TRACE.java
new file mode 100644
index 0000000..80ae087
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/TRACE.java
@@ -0,0 +1,17 @@
+package de.ids_mannheim.korap.web;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import jakarta.ws.rs.HttpMethod;
+
+/**
+ * @author hanl
+ * @date 03/07/2014
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@HttpMethod("TRACE")
+public @interface TRACE {}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java b/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
new file mode 100644
index 0000000..0b40e33
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
@@ -0,0 +1,430 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.authentication.AuthenticationManager;
+import de.ids_mannheim.korap.authentication.http.AuthorizationData;
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.config.BeansFactory;
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
+import de.ids_mannheim.korap.constant.AuthenticationScheme;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+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;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.filter.DemoUserFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+// import com.sun.xml.internal.messaging.saaj.util.Base64;
+
+/**
+ * @author hanl
+ * @date 24/01/2014
+ * 
+ * @author margaretha
+ * @last-update 01/07/2019
+ * 
+ *              - added user authentication time in token context
+ *              - added api version filter
+ *              - changed the response media-type
+ */
+@Controller
+@Path("/{version}/auth")
+@ResourceFilters({ APIVersionFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class AuthenticationController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Autowired
+    private HttpAuthorizationHandler authorizationHandler;
+
+    private static Boolean DEBUG_LOG = true;
+
+    //todo: bootstrap function to transmit certain default configuration settings and examples (example user queries,
+    // default usersettings, etc.)
+    private static Logger jlog = LogManager
+            .getLogger(AuthenticationController.class);
+
+    @Autowired
+    private AuthenticationManager controller;
+
+    //    private SendMail mail;
+
+    /**
+     * represents json string with data. All GUI clients can access
+     * this method to get certain default values
+     * --> security checks?
+     * 
+     * @return String
+     */
+    @Deprecated
+    @GET
+    @Path("bootstrap")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response bootstrap () {
+        Map m = new HashMap();
+        //        m.put("settings", new UserSettings().toObjectMap());
+        m.put("ql", BeansFactory.getKustvaktContext().getConfiguration()
+                .getQueryLanguages());
+        m.put("SortTypes", null); // types of sorting that are supported!
+        m.put("version", ServiceInfo.getInfo().getVersion());
+        try {
+            return Response.ok(JsonUtils.toJSON(m)).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    // fixme: moved to user
+    @GET
+    @Path("status")
+    @ResourceFilters({ AuthenticationFilter.class, DemoUserFilter.class,
+            BlockingFilter.class })
+    public Response getStatus (@Context SecurityContext context,
+            @HeaderParam(ContainerRequest.USER_AGENT) String agent,
+            @HeaderParam(ContainerRequest.HOST) String host,
+            @Context Locale locale) {
+        TokenContext ctx = (TokenContext) context.getUserPrincipal();
+        try {
+            return Response.ok(ctx.toJson()).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    // EM: testing using spring security authentication manager
+    //    @Deprecated
+    //    @GET
+    //    @Path("ldap/token")
+    //    public Response requestToken (@Context HttpHeaders headers,
+    //            @Context Locale locale,
+    //            @HeaderParam(ContainerRequest.USER_AGENT) String agent,
+    //            @HeaderParam(ContainerRequest.HOST) String host,
+    //            @HeaderParam("referer-url") String referer,
+    //            @QueryParam("scope") String scopes,
+    //            //   @Context WebServiceContext wsContext, // FB
+    //            @Context SecurityContext securityContext) {
+    //        
+    //        Map<String, Object> attr = new HashMap<>();
+    //        if (scopes != null && !scopes.isEmpty())
+    //            attr.put(Attributes.SCOPES, scopes);
+    //        attr.put(Attributes.HOST, host);
+    //        attr.put(Attributes.USER_AGENT, agent);
+    //        
+    //        User user = new KorAPUser();
+    //        user.setUsername(securityContext.getUserPrincipal().getName());
+    //        controller.setAccessAndLocation(user, headers);
+    //        if (DEBUG_LOG == true) System.out.printf(
+    //                "Debug: /token/: location=%s, access='%s'.\n",
+    //                user.locationtoString(), user.accesstoString());
+    //        attr.put(Attributes.LOCATION, user.getLocation());
+    //        attr.put(Attributes.CORPUS_ACCESS, user.getCorpusAccess());
+    //        
+    //        try {
+    //            TokenContext context = controller.createTokenContext(user, attr,
+    //                    TokenType.API);
+    //            return Response.ok(context.toJson()).build();
+    //        }
+    //        catch (KustvaktException e) {
+    //            throw kustvaktResponseHandler.throwit(e);
+    //        }
+    //    }
+
+    @Deprecated
+    @GET
+    @Path("apiToken")
+    //@ResourceFilters({HeaderFilter.class})
+    public Response requestAPIToken (@Context HttpHeaders headers,
+            @Context Locale locale,
+            @HeaderParam(ContainerRequest.USER_AGENT) String agent,
+            @HeaderParam(ContainerRequest.HOST) String host,
+            @HeaderParam("referer-url") String referer,
+            @QueryParam("scope") String scopes,
+            //   @Context WebServiceContext wsContext, // FB
+            @Context SecurityContext secCtx) {
+
+        if (DEBUG_LOG == true) {
+            String warning = "Access to API token (JWT) web service";
+
+            List<String> auth = headers
+                    .getRequestHeader(ContainerRequest.AUTHORIZATION);
+            if (auth != null && !auth.isEmpty()) {
+                try {
+                    AuthorizationData authorizationData = authorizationHandler
+                            .parseAuthorizationHeaderValue(auth.get(0));
+                    if (authorizationData.getAuthenticationScheme()
+                            .equals(AuthenticationScheme.BASIC)) {
+                        authorizationData = authorizationHandler
+                                .parseBasicToken(authorizationData);
+                        jlog.warn(warning + " with username:"
+                                + authorizationData.getUsername());
+                    }
+                }
+                catch (KustvaktException e) {}
+            }
+            else {
+                jlog.warn(warning);
+            }
+        }
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.DEPRECATED,
+                "API token is no longer supported. Please use OAuth2 procedure instead."));
+    }
+
+    //        List<String> auth =
+    //                headers.getRequestHeader(ContainerRequest.AUTHORIZATION);
+    //        if (auth == null || auth.isEmpty()) {
+    //            throw kustvaktResponseHandler
+    //                    .throwit(new KustvaktException(StatusCodes.MISSING_PARAMETER,
+    //                            "Authorization header is missing.",
+    //                            "Authorization header"));
+    //        }
+    //        
+    //        AuthorizationData authorizationData;
+    //        try {
+    //            authorizationData = authorizationHandler.
+    //                    parseAuthorizationHeaderValue(auth.get(0));
+    //            if (authorizationData.getAuthenticationScheme().equals(AuthenticationScheme.BASIC)){
+    //                authorizationData = authorizationHandler.parseBasicToken(authorizationData);
+    //            }
+    //            else {
+    //                // EM: throw exception that auth scheme is not supported?
+    //            }
+    //           
+    //        }
+    //        catch (KustvaktException e) {
+    //            throw kustvaktResponseHandler.throwit(e);
+    //        }
+    //
+    //        if (DEBUG_LOG == true) {
+    //            System.out.printf("Debug: AuthService.requestAPIToken...:\n");
+    //            System.out.printf("Debug: auth.size=%d\n", auth.size());
+    //            System.out.printf("auth.get(0)='%s'\n", auth.get(0));
+    //            /* hide password etc. - FB
+    //             if( auth.size() > 0 )
+    //            	{
+    //            	Iterator it = auth.iterator();
+    //            	while( it.hasNext() )
+    //            		System.out.printf(" header '%s'\n",  it.next());
+    //            	}
+    //            if( values.length > 0 )
+    //            	{
+    //            	for(int i=0; i< values.length; i++)
+    //            		{
+    //            		System.out.printf(" values[%d]='%s'\n",  i, values[i]);
+    //            		}
+    //            	}
+    //             */
+    //            MultivaluedMap<String, String> headerMap =
+    //                    headers.getRequestHeaders();
+    //            if (headerMap != null && headerMap.size() > 0) {
+    //                Iterator<String> it = headerMap.keySet().iterator();
+    //                while (it.hasNext()) {
+    //                    String key = (String) it.next();
+    //                    List<String> vals = headerMap.get(key);
+    ////                    System.out.printf("Debug: requestAPIToken: '%s' = '%s'\n",
+    ////                            key, vals);
+    //                }
+    //
+    //            }
+    ////            System.out.printf("Debug: requestAPIToken: isSecure = %s.\n",
+    ////                    secCtx.isSecure() ? "yes" : "no");
+    //        } // DEBUG_LOG        
+    //
+    //        if (authorizationData.getUsername() == null || 
+    //                authorizationData.getUsername().isEmpty() || 
+    //                authorizationData.getPassword()== null || 
+    //                authorizationData.getPassword().isEmpty())
+    //            // is actual an invalid request
+    //            throw kustvaktResponseHandler.throwit(StatusCodes.REQUEST_INVALID);
+    //
+    //        Map<String, Object> attr = new HashMap<>();
+    //        if (scopes != null && !scopes.isEmpty())
+    //            attr.put(Attributes.SCOPE, scopes);
+    //        attr.put(Attributes.HOST, host);
+    //        attr.put(Attributes.USER_AGENT, agent);
+    //
+    //        TokenContext context;
+    //        try {
+    //            // User user = controller.authenticate(0, values[0], values[1], attr); Implementation by Hanl
+    //            User user = controller.authenticate(AuthenticationMethod.LDAP,
+    //                    authorizationData.getUsername(), authorizationData.getPassword(), attr); // Implementation with IdM/LDAP
+    //            // Userdata data = this.controller.getUserData(user, UserDetails.class); // Implem. by Hanl
+    //            // todo: is this necessary?
+    //            //            attr.putAll(data.fields());
+    //            
+    //            // EM: add authentication time
+    //            Date authenticationTime = TimeUtils.getNow().toDate();
+    //            attr.put(Attributes.AUTHENTICATION_TIME, authenticationTime);
+    //            // -- EM
+    //            
+    //            controller.setAccessAndLocation(user, headers);
+    //            if (DEBUG_LOG == true) System.out.printf(
+    //                    "Debug: /apiToken/: location=%s, access='%s'.\n",
+    //                    user.locationtoString(), user.accesstoString());
+    //            attr.put(Attributes.LOCATION, user.getLocation());
+    //            attr.put(Attributes.CORPUS_ACCESS, user.getCorpusAccess());
+    //            context = controller.createTokenContext(user, attr,
+    //                  TokenType.API);
+    ////            context = controller.createTokenContext(user, attr,
+    ////                    Attributes.API_AUTHENTICATION);
+    //        }
+    //        catch (KustvaktException e) {
+    //            throw kustvaktResponseHandler.throwit(e);
+    //        }
+    //
+    //        try {
+    //            return Response.ok(context.toJson()).build();
+    //        }
+    //        catch (KustvaktException e) {
+    //            throw kustvaktResponseHandler.throwit(e);
+    //        }
+    //    }
+
+    // todo:
+    @Deprecated
+    @GET
+    @Path("refresh")
+    public Response refresh (@Context SecurityContext context,
+            @Context Locale locale) {
+        TokenContext ctx = (TokenContext) context.getUserPrincipal();
+        TokenContext newContext;
+
+        //        try {
+        //            newContext = controller.refresh(ctx);
+        //        }catch (KorAPException e) {
+        //            KorAPLogger.ERROR_LOGGER.error("Exception encountered!", e);
+        //            throw KustvaktResponseHandler.throwit(e);
+        //        }
+        //        return Response.ok().entity(newContext.getToken()).build();
+        return null;
+    }
+
+    @GET
+    @Path("sessionToken")
+    //@ResourceFilters({HeaderFilter.class})
+    public Response requestSession (@Context HttpHeaders headers,
+            @Context Locale locale,
+            @HeaderParam(ContainerRequest.USER_AGENT) String agent,
+            @HeaderParam(ContainerRequest.HOST) String host) {
+        List<String> auth = headers
+                .getRequestHeader(ContainerRequest.AUTHORIZATION);
+
+        AuthorizationData authorizationData;
+        try {
+            authorizationData = authorizationHandler
+                    .parseAuthorizationHeaderValue(auth.get(0));
+            authorizationData = authorizationHandler
+                    .parseBasicToken(authorizationData);
+
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+        // Implementation Hanl mit '|'. 16.02.17/FB
+        //if (values[0].equalsIgnoreCase("null")
+        //        | values[1].equalsIgnoreCase("null"))
+        if (authorizationData.getUsername() == null
+                || authorizationData.getUsername().isEmpty()
+                || authorizationData.getPassword() == null
+                || authorizationData.getPassword().isEmpty())
+            // is actual an invalid request
+            throw kustvaktResponseHandler.throwit(StatusCodes.INVALID_REQUEST);
+
+        Map<String, Object> attr = new HashMap<>();
+        attr.put(Attributes.HOST, host);
+        attr.put(Attributes.USER_AGENT, agent);
+        TokenContext context;
+        String contextJson;
+        try {
+            //EM: authentication scheme default
+            User user = controller.authenticate(AuthenticationMethod.DATABASE,
+                    authorizationData.getUsername(),
+                    authorizationData.getPassword(), attr);
+            context = controller.createTokenContext(user, attr,
+                    TokenType.SESSION);
+            //            context = controller.createTokenContext(user, attr,
+            //                    Attributes.SESSION_AUTHENTICATION);
+            contextJson = context.toJson();
+            jlog.debug(contextJson);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok().entity(contextJson).build();
+    }
+
+    // fixme: security issues: setup shibboleth compatible authentication system
+    // todo: will be purged with token authentication --> shib is client side
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces("application/json")
+    @Path("shibboleth")
+    public Response loginshib (@Context HttpHeaders headers,
+            @Context Locale locale,
+            @HeaderParam(ContainerRequest.USER_AGENT) String agent,
+            @HeaderParam(ContainerRequest.HOST) String host,
+            @QueryParam("client_id") String client_id) {
+
+        // the shibfilter decrypted the values
+        // define default provider for returned access token strategy?!
+
+        Map<String, Object> attr = new HashMap<>();
+        attr.put(Attributes.HOST, host);
+        attr.put(Attributes.USER_AGENT, agent);
+
+        TokenContext context;
+
+        try {
+            // todo: distinguish type KorAP/Shibusers
+            User user = controller.authenticate(AuthenticationMethod.SHIBBOLETH,
+                    null, null, attr);
+            context = controller.createTokenContext(user, attr, null);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        try {
+            return Response.ok().entity(context.toJson()).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2AdminController.java b/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2AdminController.java
new file mode 100644
index 0000000..b76a049
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2AdminController.java
@@ -0,0 +1,75 @@
+package de.ids_mannheim.korap.web.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.service.OAuth2AdminService;
+import de.ids_mannheim.korap.web.OAuth2ResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AdminFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+@Controller
+@Path("{version}/admin/oauth2")
+@ResourceFilters({ APIVersionFilter.class, AdminFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class OAuth2AdminController {
+
+    @Autowired
+    private OAuth2AdminService adminService;
+    @Autowired
+    private OAuth2ResponseHandler responseHandler;
+
+    /**
+     * Removes expired or invalid access and refresh tokens from
+     * database and cache
+     * 
+     * @return Response status OK, if successful
+     */
+    @POST
+    @Path("token/clean")
+    public Response cleanExpiredInvalidToken () {
+        adminService.cleanTokens();
+        return Response.ok().build();
+    }
+
+    /**
+     * Facilitates editing client privileges for admin purposes, e.g.
+     * setting a specific client to be a super client.
+     * Only confidential clients are allowed to be super clients.
+     * 
+     * When upgrading clients to super clients, existing access tokens
+     * and authorization codes retain their scopes.
+     * 
+     * When degrading super clients, all existing tokens and
+     * authorization codes are invalidated.
+     * 
+     * @param clientId
+     *            OAuth2 client id
+     * @param super
+     *            true indicating super client, false otherwise
+     * @return Response status OK, if successful
+     */
+    @POST
+    @Path("client/privilege")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response updateClientPrivilege (
+            @FormParam("client_id") String clientId,
+            @FormParam("super") String isSuper) {
+        try {
+            adminService.updatePrivilege(clientId, Boolean.valueOf(isSuper));
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java b/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
new file mode 100644
index 0000000..8ab3e2a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
@@ -0,0 +1,472 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationErrorResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2TokenDto;
+import de.ids_mannheim.korap.oauth2.service.OAuth2AuthorizationService;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import de.ids_mannheim.korap.web.OAuth2ResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.UriBuilder;
+
+/**
+ * OAuth2Controller describes OAuth2 web API for authorization
+ * for both internal (e.g Kalamar) and external (third party) clients.
+ * 
+ * Possible authorization scopes are listed in {@link OAuth2Scope} For
+ * more information about OAuth2 authorization mechanisms, see RFC
+ * 6749.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("{version}/oauth2")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class OAuth2Controller {
+
+    @Autowired
+    private OAuth2ResponseHandler responseHandler;
+
+    @Autowired
+    private OAuth2TokenService tokenService;
+    @Autowired
+    private OAuth2AuthorizationService authorizationService;
+
+    @Autowired
+    private OAuth2ScopeService scopeService;
+
+    /**
+     * Requests an authorization code.
+     * 
+     * Kustvakt supports authorization only with Kalamar as the
+     * authorization web-frontend or user interface. Thus
+     * authorization code request requires user authentication
+     * using authorization header.
+     * 
+     * <br /><br />
+     * RFC 6749:
+     * If the client omits the scope parameter when requesting
+     * authorization, the authorization server MUST either process the
+     * request using a pre-defined default value or fail the request
+     * indicating an invalid scope.
+     * 
+     * @param request
+     *            HttpServletRequest
+     * @param form
+     *            form parameters
+     * @return a redirect URL
+     */
+    @Deprecated
+    @POST
+    @Path("authorize")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response requestAuthorizationCode (
+            @Context HttpServletRequest request,
+            @Context SecurityContext context, @FormParam("scope") String scope,
+            @FormParam("state") String state,
+            @FormParam("client_id") String clientId,
+            @FormParam("redirect_uri") String redirectUri,
+            MultivaluedMap<String, String> form) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+        ZonedDateTime authTime = tokenContext.getAuthenticationTime();
+
+        URI requestURI;
+        UriBuilder builder = UriBuilder.fromPath(request.getRequestURI());
+        for (String key : form.keySet()) {
+            builder.queryParam(key, form.get(key).toArray());
+        }
+        requestURI = builder.build();
+
+        try {
+            scopeService.verifyScope(tokenContext, OAuth2Scope.AUTHORIZE);
+            URI uri = authorizationService.requestAuthorizationCode(requestURI,
+                    clientId, redirectUri, scope, state, username, authTime);
+            return responseHandler.sendRedirect(uri);
+        }
+        catch (KustvaktException e) {
+            e = authorizationService.checkRedirectUri(e, clientId, redirectUri);
+            if (e.getRedirectUri() != null) {
+                AuthorizationErrorResponse errorResponse = authorizationService
+                        .createAuthorizationError(e, state);
+                return responseHandler.sendRedirect(errorResponse.toURI());
+            }
+            else {
+                throw responseHandler.throwit(e, state);
+            }
+        }
+    }
+
+    @GET
+    @Path("authorize")
+    public Response requestAuthorizationCode (
+            @Context HttpServletRequest request,
+            @Context SecurityContext context,
+            @QueryParam("response_type") String responseType,
+            @QueryParam("client_id") String clientId,
+            @QueryParam("redirect_uri") String redirectUri,
+            @QueryParam("scope") String scope,
+            @QueryParam("state") String state) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+        ZonedDateTime authTime = tokenContext.getAuthenticationTime();
+
+        URI requestURI;
+        try {
+            requestURI = new URI(
+                    request.getRequestURI() + "?" + request.getQueryString());
+        }
+        catch (URISyntaxException e) {
+            KustvaktException ke = new KustvaktException(
+                    StatusCodes.INVALID_REQUEST, "Failed parsing request URI.",
+                    OAuth2Error.INVALID_REQUEST_URI);
+            throw responseHandler.throwit(ke, state);
+        }
+
+        try {
+            scopeService.verifyScope(tokenContext, OAuth2Scope.AUTHORIZE);
+            URI uri = authorizationService.requestAuthorizationCode(requestURI,
+                    clientId, redirectUri, scope, state, username, authTime);
+            return responseHandler.sendRedirect(uri);
+        }
+        catch (KustvaktException e) {
+            e = authorizationService.checkRedirectUri(e, clientId, redirectUri);
+            if (e.getRedirectUri() != null) {
+                AuthorizationErrorResponse errorResponse = authorizationService
+                        .createAuthorizationError(e, state);
+                return responseHandler.sendRedirect(errorResponse.toURI());
+            }
+            else {
+                throw responseHandler.throwit(e, state);
+            }
+        }
+    }
+
+    /**
+     * Grants a client an access token, namely a string used in
+     * authenticated requests representing user authorization for
+     * the client to access user resources. An additional refresh
+     * token strictly associated to the access token is also granted
+     * for confidential clients. Both public and confidential clients
+     * may issue multiple access tokens.
+     * 
+     * <br /><br />
+     * 
+     * Confidential clients may request refresh access token using
+     * this endpoint. This request will grant a new access token.
+     * 
+     * Usually the given refresh token is not changed and can be used
+     * until it expires. However, currently there is a limitation of
+     * one access token per one refresh token. Thus, the given refresh
+     * token will be revoked, and a new access token and a new refresh
+     * token will be returned.
+     * 
+     * <br /><br />
+     * 
+     * Client credentials for authentication can be provided either as
+     * an authorization header with Basic authentication scheme or as
+     * form parameters in the request body.
+     * 
+     * <br /><br />
+     * 
+     * OAuth2 specification describes various ways of requesting an
+     * access token. Kustvakt supports:
+     * <ul>
+     * <li> Authorization code grant: obtains authorization from a
+     * third party application. Required parameters: grant_type,
+     * code, client_id, redirect_uri (if specified in the
+     * authorization request), client_secret (if the client is
+     * confidential or issued a secret).
+     * </li>
+     * <li> Resource owner password grant: strictly for clients that
+     * are parts of KorAP. Clients use user credentials, e.g. Kalamar
+     * (front-end) with login form. Required parameters: grant_type,
+     * username, password, client_id, client_secret (if the client is
+     * confidential or issued a secret). Optional parameters: scope.
+     * </li>
+     * <li> Client credentials grant: strictly for clients that are
+     * parts of KorAP. Clients access their own resources, not on
+     * behalf of a user. Required parameters: grant_type, client_id,
+     * client_secret. Optional parameters: scope.
+     * </li>
+     * </ul>
+     * 
+     * RFC 6749: The value of the scope parameter is expressed as a
+     * list of space-delimited, case-sensitive strings defined by the
+     * authorization server.
+     * 
+     * @param request
+     *            the request
+     * @param form
+     *            form parameters in a map
+     * @return a JSON object containing an access token, a refresh
+     *         token, a token type and the token expiration in seconds
+     *         if successful, an error code and an error description
+     *         otherwise.
+     */
+    @POST
+    @Path("token")
+    @ResourceFilters({ APIVersionFilter.class })
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response requestAccessToken (@Context HttpServletRequest request,
+            @NotEmpty @FormParam("grant_type") String grantType,
+            @NotEmpty @FormParam("client_id") String clientId,
+            @FormParam("client_secret") String clientSecret,
+            MultivaluedMap<String, String> form) {
+
+        try {
+            URI requestURI;
+            UriBuilder builder = UriBuilder
+                    .fromPath(request.getRequestURL().toString());
+            for (String key : form.keySet()) {
+                builder.queryParam(key, form.get(key).toArray());
+            }
+            requestURI = builder.build();
+
+            try {
+                AuthorizationGrant authGrant = AuthorizationGrant.parse(form);
+
+                ClientAuthentication clientAuth = null;
+                String authorizationHeader = request.getHeader("Authorization");
+                if (authorizationHeader != null
+                        && !authorizationHeader.isEmpty()) {
+                    clientAuth = ClientSecretBasic.parse(authorizationHeader);
+                }
+                else if (authGrant instanceof ClientCredentialsGrant) {
+                    // this doesn't allow public clients
+                    clientAuth = ClientSecretPost.parse(form);
+                }
+
+                TokenRequest tokenRequest = null;
+                if (clientAuth != null) {
+                    ClientAuthenticationMethod method = clientAuth.getMethod();
+                    if (method.equals(
+                            ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+                        ClientSecretBasic basic = (ClientSecretBasic) clientAuth;
+                        clientSecret = basic.getClientSecret().getValue();
+                        clientId = basic.getClientID().getValue();
+                    }
+                    else if (method.equals(
+                            ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
+                        ClientSecretPost post = (ClientSecretPost) clientAuth;
+                        clientSecret = post.getClientSecret().getValue();
+                        clientId = post.getClientID().getValue();
+                    }
+
+                    tokenRequest = new TokenRequest(requestURI, clientAuth,
+                            AuthorizationGrant.parse(form),
+                            Scope.parse(form.getFirst("scope")));
+                }
+                else {
+                    // requires ClientAuthentication for client_credentials grant
+                    tokenRequest = new TokenRequest(requestURI,
+                            new ClientID(clientId),
+                            AuthorizationGrant.parse(form),
+                            Scope.parse(form.getFirst("scope")));
+                }
+
+                AccessTokenResponse r = tokenService.requestAccessToken(
+                        tokenRequest, clientId, clientSecret);
+                return responseHandler.createResponse(r);
+            }
+            catch (ParseException | IllegalArgumentException e) {
+                throw new KustvaktException(StatusCodes.INVALID_REQUEST,
+                        e.getMessage(), OAuth2Error.INVALID_REQUEST);
+            }
+
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Revokes either an access token or a refresh token. Revoking a
+     * refresh token also revokes all access token associated with the
+     * refresh token.
+     * 
+     * RFC 7009
+     * Client authentication for confidential client
+     * 
+     * @param request
+     * @param form
+     *            containing
+     *            client_id,
+     *            client_secret (required for confidential clients),
+     *            token,
+     *            token_type (optional)
+     * @return 200 if token invalidation is successful or the given
+     *         token is invalid
+     */
+    @POST
+    @Path("revoke")
+    @ResourceFilters({ APIVersionFilter.class })
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response revokeAccessToken (@Context HttpServletRequest request,
+            @FormParam("client_id") String clientId,
+            @FormParam("client_secret") String clientSecret,
+            @FormParam("token") String token,
+            @FormParam("token_type") String tokenType) {
+
+        try {
+            ParameterChecker.checkStringValue("client_id", clientId);
+            ParameterChecker.checkStringValue("token", token);
+            tokenService.revokeToken(clientId, clientSecret, token, tokenType);
+
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Path("revoke/super")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response revokeTokenViaSuperClient (@Context SecurityContext context,
+            @Context HttpServletRequest request,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("token") String token) {
+
+        try {
+            ParameterChecker.checkStringValue("super_client_id", superClientId);
+            ParameterChecker.checkStringValue("super_client_secret",
+                    superClientSecret);
+            ParameterChecker.checkStringValue("token", token);
+
+            TokenContext tokenContext = (TokenContext) context
+                    .getUserPrincipal();
+            String username = tokenContext.getUsername();
+
+            tokenService.revokeTokensViaSuperClient(username, superClientId,
+                    superClientSecret, token);
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Revokes all tokens of a client for the authenticated user from
+     * a super client. This service is not part of the OAUTH2
+     * specification. It requires user authentication via
+     * authorization header, and super client
+     * via URL-encoded form parameters.
+     * 
+     * @param request
+     * @param form
+     *            containing client_id, super_client_id,
+     *            super_client_secret
+     * @return 200 if token invalidation is successful or the given
+     *         token is invalid
+     */
+    @POST
+    @Path("revoke/super/all")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response revokeAllClientTokensViaSuperClient (
+            @Context SecurityContext context,
+            @Context HttpServletRequest request,
+            @FormParam("client_id") String clientId,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            ParameterChecker.checkStringValue("super_client_id", superClientId);
+            ParameterChecker.checkStringValue("super_client_secret",
+                    superClientSecret);
+
+            tokenService.revokeAllClientTokensViaSuperClient(username,
+                    superClientId, superClientSecret, clientId);
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Path("token/list")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public List<OAuth2TokenDto> listUserToken (@Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("client_id") String clientId, // optional
+            @FormParam("token_type") String tokenType) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            if (tokenType.equals("access_token")) {
+                return tokenService.listUserAccessToken(username, superClientId,
+                        superClientSecret, clientId);
+            }
+            else if (tokenType.equals("refresh_token")) {
+                return tokenService.listUserRefreshToken(username,
+                        superClientId, superClientSecret, clientId);
+            }
+            else {
+                throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                        "Missing token_type parameter value",
+                        OAuth2Error.INVALID_REQUEST);
+            }
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java b/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
new file mode 100644
index 0000000..b530e19
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -0,0 +1,225 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ClientService;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.web.OAuth2ResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * Defines controllers for OAuth2 clients, namely applications
+ * performing actions such as searching and retrieving match
+ * information on behalf of users.
+ * 
+ * <br /><br />
+ * According to its privileges, clients are categorized into super and
+ * normal clients. Super clients are intended only for clients that
+ * are part of KorAP. They has special privileges to use controllers
+ * that usually are not allowed for normal clients, for instance using
+ * OAuth2 password grant to obtain access tokens.
+ * 
+ * <br /><br />
+ * By default, clients are set as normal clients. Super clients has to
+ * be set manually by an admin, e.g by using
+ * {@link #updateClientPrivilege(SecurityContext, String, boolean)}
+ * controller. Only confidential clients are allowed to be super
+ * clients.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("{version}/oauth2/client")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class OAuthClientController {
+
+    @Autowired
+    private OAuth2ClientService clientService;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
+    private OAuth2ResponseHandler responseHandler;
+
+    /**
+     * Registers a client application. Before starting an OAuth
+     * process, client applications have to be registered first. Only
+     * registered users are allowed to register client applications.
+     * 
+     * After registration, the client receives a client_id and a
+     * client_secret, if the client is confidential (capable of
+     * storing the client_secret), that are needed in the
+     * authorization process.
+     * 
+     * From RFC 6749:
+     * The authorization server SHOULD document the size of any
+     * identifier it issues.
+     * 
+     * @param context
+     * @param clientJson
+     *            a JSON object describing the client
+     * @return client_id and client_secret if the client type is
+     *         confidential
+     * 
+     * @see OAuth2ClientJson
+     */
+    @POST
+    @Path("register")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public OAuth2ClientDto registerClient (
+            @Context SecurityContext securityContext,
+            OAuth2ClientJson clientJson) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.REGISTER_CLIENT);
+            return clientService.registerClient(clientJson,
+                    context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Deregisters a client requires client owner authentication.
+     * 
+     * 
+     * @param securityContext
+     * @param clientId
+     *            the client id
+     * @return HTTP Response OK if successful.
+     */
+    @DELETE
+    @Path("deregister/{client_id}")
+    public Response deregisterClient (@Context SecurityContext securityContext,
+            @PathParam("client_id") String clientId) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.DEREGISTER_CLIENT);
+            clientService.deregisterClient(clientId, context.getUsername());
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Resets client secret of the given client. This controller
+     * requires client owner and client authentication. Only
+     * confidential clients are issued client secrets.
+     * 
+     * @param securityContext
+     * @param clientId
+     * @param clientSecret
+     * @return a new client secret
+     */
+    @POST
+    @Path("reset")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public OAuth2ClientDto resetClientSecret (
+            @Context SecurityContext securityContext,
+            @FormParam("client_id") String clientId) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.RESET_CLIENT_SECRET);
+            return clientService.resetSecret(clientId, context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Path("{client_id}")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @ResourceFilters({ APIVersionFilter.class })
+    public OAuth2ClientInfoDto retrieveClientInfo (
+            @PathParam("client_id") String clientId,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret) {
+        try {
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            return clientService.retrieveClientInfo(clientId);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Lists user clients having active refresh tokens (not revoked,
+     * not expired), except super clients.
+     * 
+     * This service is not part of the OAuth2 specification. It is
+     * intended to facilitate users revoking any suspicious and
+     * misused access or refresh tokens.
+     * 
+     * Only super clients are allowed to use this service. It requires
+     * user and client authentications.
+     * 
+     * @param context
+     * @param superClientId
+     *            the client id of the super client
+     * @param superClientSecret
+     *            the client secret of the super client
+     * @return a list of clients having refresh tokens of the
+     *         given user
+     */
+    @POST
+    @Path("/list")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public List<OAuth2ClientInfoDto> listUserClients (
+            @Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("authorized_only") boolean authorizedOnly) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.LIST_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            if (authorizedOnly) {
+                return clientService.listUserAuthorizedClients(username);
+            }
+            else {
+                return clientService.listUserRegisteredClients(username);
+            }
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/PluginController.java b/src/main/java/de/ids_mannheim/korap/web/controller/PluginController.java
new file mode 100644
index 0000000..0afda45
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/PluginController.java
@@ -0,0 +1,136 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.dto.InstalledPluginDto;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ClientService;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.web.OAuth2ResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+@Controller
+@Path("{version}/plugins")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class PluginController {
+
+    @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
+    private OAuth2ClientService clientService;
+    @Autowired
+    private OAuth2ResponseHandler responseHandler;
+
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public List<OAuth2ClientInfoDto> listPlugins (
+            @Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("permitted_only") boolean permittedOnly) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.LIST_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            return clientService.listPlugins(permittedOnly);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Path("/install")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public InstalledPluginDto installPlugin (@Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("client_id") String clientId) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.INSTALL_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            return clientService.installPlugin(superClientId, clientId,
+                    username);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Path("/installed")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public List<InstalledPluginDto> listInstalledPlugins (
+            @Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.LIST_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            return clientService.listInstalledPlugins(superClientId,
+                    tokenContext.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    @POST
+    @Path("/uninstall")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response uninstallPlugin (@Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("client_id") String clientId) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.UNINSTALL_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            clientService.uninstallPlugin(superClientId, clientId, username);
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java b/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java
new file mode 100644
index 0000000..af9c395
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/QueryReferenceController.java
@@ -0,0 +1,237 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.dto.QueryDto;
+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.service.QueryService;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.filter.DemoUserFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.input.QueryJson;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * QueryReferenceController defines web APIs related to the
+ * management of query references.
+ *
+ * This controller is based on VirtualCorpusController.
+ *
+ * @author diewald, margaretha
+ *
+ */
+@Controller
+@Path("{version}/query")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class QueryReferenceController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private QueryService service;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+
+    /**
+     * Creates a query reference according to the given Json.
+     * The query reference creator must be the same as the
+     * 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.
+     *
+     * @param securityContext
+     * @param qCreator
+     *            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 HTTP Status 201 Created when creating a new query, or
+     *         204
+     *         No Content when updating an existing query.
+     * @throws KustvaktException
+     */
+    @PUT
+    @Path("/~{qCreator}/{qName}")
+    @Consumes(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response createQuery (@Context SecurityContext securityContext,
+            @PathParam("qCreator") String qCreator,
+            @PathParam("qName") String qName, QueryJson query)
+            throws KustvaktException {
+
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.EDIT_VC);
+            ParameterChecker.checkObjectValue(query, "request entity");
+            if (query.getQueryType() == null) {
+                query.setQueryType(QueryType.QUERY);
+            }
+            Status status = service.handlePutRequest(context.getUsername(),
+                    qCreator, qName, query);
+            return Response.status(status).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+    }
+
+    /**
+     * Returns the query with the given name and creator.
+     * 
+     * @param securityContext
+     * @param createdBy
+     *            query creator
+     * @param qName
+     *            query name
+     * @return the query with the given name and creator.
+     */
+    @GET
+    @Path("~{createdBy}/{qName}")
+    @ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+            DemoUserFilter.class, PiwikFilter.class })
+    public QueryDto retrieveQueryByName (
+            @Context SecurityContext securityContext,
+            @PathParam("createdBy") String createdBy,
+            @PathParam("qName") String qName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
+            return service.retrieveQueryByName(context.getUsername(), qName,
+                    createdBy, QueryType.QUERY);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * 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
+     *            query creator
+     * @param qName
+     *            query name
+     * @return HTTP status 200, if successful
+     */
+
+    @DELETE
+    @Path("~{createdBy}/{qName}")
+    public Response deleteQueryByName (@Context SecurityContext securityContext,
+            @PathParam("createdBy") String createdBy,
+            @PathParam("qName") String qName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.DELETE_VC);
+            service.deleteQueryByName(context.getUsername(), qName, createdBy,
+                    QueryType.QUERY);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    };
+
+    /**
+     * Lists all queries available to the authenticated user.
+     *
+     * System-admins can list available queries for a specific user by
+     * specifiying the username parameter.
+     * 
+     * Normal users cannot list queries available for other users.
+     * Thus, username parameter is optional
+     * and must be identical to the authenticated username.
+     * 
+     * @param securityContext
+     * @param username
+     *            a username (optional)
+     * @return a list of queries
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public List<QueryDto> listAvailableQuery (
+            @Context SecurityContext securityContext,
+            @QueryParam("username") String username) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
+            List<QueryDto> dtos = service.listAvailableQueryForUser(
+                    context.getUsername(), QueryType.QUERY);
+            return dtos;
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    //    // TODO: List all queries of a sepcific user
+    //    /**
+    //     * Lists all queries created by a user. This list is only
+    //     * available to the owner of the queries. Users, except system-admins, 
+    //     * are not allowed to list queries created by other users. 
+    //     * 
+    //     * Thus, the path parameter "createdBy" must be the same as the
+    //     * authenticated username. 
+    //     * 
+    //     * @param securityContext
+    //     * @return a list of queries created by the user
+    //     *         in the security context.
+    //     */
+    //    @GET
+    //    @Path("~{createdBy}")
+    //    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    //    public List<VirtualCorpusDto> listUserVC (
+    //            @PathParam("createdBy") String createdBy,
+    //            @Context SecurityContext securityContext) {
+    //        TokenContext context =
+    //                (TokenContext) securityContext.getUserPrincipal();
+    //        try {
+    //            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
+    //            return service.listOwnerVC(context.getUsername(), createdBy,
+    //                    QueryType.QUERY);
+    //        }
+    //        catch (KustvaktException e) {
+    //            throw kustvaktResponseHandler.throwit(e);
+    //        }
+    //    }
+
+    // TODO: Some admin routes missing.
+    // TODO: Some sharing routes missing
+};
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java b/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java
new file mode 100644
index 0000000..986f621
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java
@@ -0,0 +1,44 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.dto.ResourceDto;
+import de.ids_mannheim.korap.service.ResourceService;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+/**
+ * Provides information about free resources.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("{version}/resource")
+@ResourceFilters({ APIVersionFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class ResourceController {
+
+    @Autowired
+    private ResourceService resourceService;
+
+    /**
+     * Returns descriptions of all free resources stored in
+     * the database.
+     * 
+     * @return a json description of all free resources stored in
+     *         the database.
+     */
+    @GET
+    public List<ResourceDto> getAllResourceInfo () {
+        return resourceService.getResourceDtos();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/UserController.java b/src/main/java/de/ids_mannheim/korap/web/controller/UserController.java
new file mode 100644
index 0000000..09de4c2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/UserController.java
@@ -0,0 +1,50 @@
+package de.ids_mannheim.korap.web.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+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.service.UserService;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.SecurityContext;
+
+@Controller
+@Path("{version}/user")
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+@ResourceFilters({ AuthenticationFilter.class, APIVersionFilter.class })
+public class UserController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
+    private UserService userService;
+
+    @GET
+    @Path("/info")
+    public JsonNode getUsername (@Context SecurityContext securityContext) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.USER_INFO);
+            return userService.retrieveUserInfo(context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java
new file mode 100644
index 0000000..4ed6266
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java
@@ -0,0 +1,85 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.dto.UserGroupDto;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.service.UserGroupService;
+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.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Controller
+@Path("{version}/admin/group")
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+@ResourceFilters({ APIVersionFilter.class, AdminFilter.class })
+public class UserGroupAdminController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private UserGroupService service;
+
+    /**
+     * Lists user-groups for system-admin purposes. If username is
+     * specified, lists user-groups of the given user, otherwise list
+     * user-groups of all users. If status specified, list only
+     * user-groups with the given status, otherwise list user-groups
+     * regardless of their status.
+     * 
+     * @param securityContext
+     * @param username
+     *            a username
+     * @param status
+     *            {@link UserGroupStatus}
+     * @return a list of user-groups
+     */
+    @POST
+    @Path("list")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public List<UserGroupDto> listUserGroupBySystemAdmin (
+            @FormParam("username") String username,
+            @FormParam("status") UserGroupStatus status) {
+        try {
+            return service.retrieveUserGroupByStatus(username, status);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Retrieves a specific user-group. Only system admins are
+     * allowed.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            group name
+     * @return a user-group
+     */
+    @POST
+    @Path("@{groupName}")
+    public UserGroupDto retrieveUserGroup (
+            @PathParam("groupName") String groupName) {
+        try {
+            return service.searchByName(groupName);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
new file mode 100644
index 0000000..b54269a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
@@ -0,0 +1,383 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.apache.http.HttpStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.dto.UserGroupDto;
+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.service.UserGroupService;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * UserGroupController defines web APIs related to user groups,
+ * such as creating a user group, listing groups of a user,
+ * adding members to a group and subscribing (confirming an
+ * invitation) to a group.
+ * 
+ * These APIs are only available to logged-in users and not available
+ * via third-party apps.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("{version}/group")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class UserGroupController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private UserGroupService service;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+
+    /**
+     * Returns all user-groups in which a user is an active or a
+     * pending member.
+     * 
+     * Not suitable for system-admin, instead use
+     * {@link UserGroupController#
+     * getUserGroupBySystemAdmin(SecurityContext, String,
+     * UserGroupStatus)}
+     * 
+     * @param securityContext
+     * @return a list of user-groups
+     * 
+     */
+    @GET
+    public List<UserGroupDto> listUserGroups (
+            @Context SecurityContext securityContext) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.USER_GROUP_INFO);
+            return service.retrieveUserGroupDto(context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Creates a user group with the group owner as the only group
+     * member. The group owner is the authenticated user in the token
+     * context.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            the name of the group
+     * @return if a new group created, HTTP response status 201
+     *         Created, otherwise 204 No Content.
+     */
+    @PUT
+    @Path("@{groupName}")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response createUpdateUserGroup (
+            @Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @FormParam("description") String description) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.CREATE_USER_GROUP);
+            boolean groupExists = service.createUpdateUserGroup(groupName,
+                    description, context.getUsername());
+            if (groupExists) {
+                return Response.noContent().build();
+            }
+            else {
+                return Response.status(HttpStatus.SC_CREATED).build();
+            }
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Deletes a user-group specified by the group name. Only group
+     * owner and system admins can delete groups.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            the name of the group to delete
+     * @return HTTP 200, if successful.
+     */
+    @DELETE
+    @Path("@{groupName}")
+    public Response deleteUserGroup (@Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.DELETE_USER_GROUP);
+            service.deleteGroup(groupName, context.getUsername());
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Removes a user-group member. Group owner cannot be removed.
+     * Only group admins, system admins and the member himself can
+     * remove a member.
+     * 
+     * @param securityContext
+     * @param memberUsername
+     *            a username of a group member
+     * @param groupName
+     *            a group name
+     * @return if successful, HTTP response status OK
+     */
+    @DELETE
+    @Path("@{groupName}/~{memberUsername}")
+    public Response removeUserFromGroup (
+            @Context SecurityContext securityContext,
+            @PathParam("memberUsername") String memberUsername,
+            @PathParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.DELETE_USER_GROUP_MEMBER);
+            service.deleteGroupMember(memberUsername, groupName,
+                    context.getUsername());
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Invites users to join a user-group specified by the
+     * groupName. Only user-group admins and system admins are
+     * allowed to use this service.
+     * 
+     * The invited users are added as group members with status
+     * GroupMemberStatus.PENDING.
+     * 
+     * If a user accepts the invitation by using the service:
+     * {@link UserGroupController#subscribeToGroup(SecurityContext, String)},
+     * his GroupMemberStatus will be updated to
+     * GroupMemberStatus.ACTIVE.
+     * 
+     * If a user rejects the invitation by using the service:
+     * {@link UserGroupController#unsubscribeFromGroup(SecurityContext, String)},
+     * his GroupMemberStatus will be updated to
+     * GroupMemberStatus.DELETED.
+     * 
+     * @param securityContext
+     * @param members
+     *            usernames separated by comma
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("@{groupName}/invite")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response inviteGroupMembers (
+            @Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @FormParam("members") String members) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.ADD_USER_GROUP_MEMBER);
+            service.inviteGroupMembers(groupName, members,
+                    context.getUsername());
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Very similar to addMemberRoles web-service, but allows deletion
+     * as well.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            the group name
+     * @param memberUsername
+     *            the username of a group-member
+     * @param roleId
+     *            a role id or multiple role ids
+     * @return
+     */
+    @POST
+    @Path("@{groupName}/role/edit")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response editMemberRoles (@Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @FormParam("memberUsername") String memberUsername,
+            @FormParam("roleId") List<Integer> roleIds) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.EDIT_USER_GROUP_MEMBER_ROLE);
+            service.editMemberRoles(context.getUsername(), groupName,
+                    memberUsername, roleIds);
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Adds roles of an active member of a user-group. Only user-group
+     * admins and system admins are allowed.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            a group name
+     * @param memberUsername
+     *            a username of a group member
+     * @param roleId
+     *            a role id or multiple role ids
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("@{groupName}/role/add")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response addMemberRoles (@Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @FormParam("memberUsername") String memberUsername,
+            @FormParam("roleId") List<Integer> roleIds) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.ADD_USER_GROUP_MEMBER_ROLE);
+            service.addMemberRoles(context.getUsername(), groupName,
+                    memberUsername, roleIds);
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Updates the roles of a member of a user-group by removing the
+     * given roles. Only user-group admins and system admins are
+     * allowed.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            a group name
+     * @param memberUsername
+     *            a username of a group member
+     * @param roleId
+     *            a role id or multiple role ids
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("@{groupName}/role/delete")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response deleteMemberRoles (@Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @FormParam("memberUsername") String memberUsername,
+            @FormParam("roleId") List<Integer> roleIds) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.DELETE_USER_GROUP_MEMBER_ROLE);
+            service.deleteMemberRoles(context.getUsername(), groupName,
+                    memberUsername, roleIds);
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Handles requests to accept membership invitation. Only invited
+     * users can subscribe to the corresponding user-group.
+     * 
+     * @param securityContext
+     * @param groupName
+     *            a group name
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("@{groupName}/subscribe")
+    public Response subscribeToGroup (@Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.ADD_USER_GROUP_MEMBER);
+            service.acceptInvitation(groupName, context.getUsername());
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Handles requests to reject membership invitation. A member can
+     * only unsubscribe him/herself from a group.
+     * 
+     * Implemented identical to
+     * {@link #removeUserFromGroup(SecurityContext, String, String)}.
+     * 
+     * @param securityContext
+     * @param groupName
+     * @return if successful, HTTP response status OK
+     */
+    @DELETE
+    @Path("@{groupName}/unsubscribe")
+    public Response unsubscribeFromGroup (
+            @Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.DELETE_USER_GROUP_MEMBER);
+            service.deleteGroupMember(context.getUsername(), groupName,
+                    context.getUsername());
+            return Response.ok("SUCCESS").build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/UserSettingController.java b/src/main/java/de/ids_mannheim/korap/web/controller/UserSettingController.java
new file mode 100644
index 0000000..af0199f
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/UserSettingController.java
@@ -0,0 +1,188 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.service.DefaultSettingService;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * UserSettingController defines web APIs related to user default
+ * setting.
+ * 
+ * All the APIs in this class are only available to logged-in users.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("{version}/{username: ~[a-zA-Z0-9_.]+}/setting")
+@ResourceFilters({ AuthenticationFilter.class, APIVersionFilter.class,
+        PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class UserSettingController {
+
+    @Autowired
+    private DefaultSettingService settingService;
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+
+    /**
+     * Creates a default setting of the given username.
+     * The setting inputs should be represented as pairs of keys and
+     * values (a map). The keys must only contains alphabets, numbers,
+     * hypens or underscores.
+     * 
+     * 
+     * @param context
+     *            security context
+     * @param username
+     *            username
+     * @param map
+     *            the default setting
+     * @return status code 201 if a new resource is created, or 200 if
+     *         an existing resource is edited.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @ResourceFilters({ AuthenticationFilter.class, PiwikFilter.class,
+            BlockingFilter.class })
+    public Response createDefaultSetting (@Context SecurityContext context,
+            @PathParam("username") String username, Map<String, Object> map) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.CREATE_DEFAULT_SETTING);
+            int statusCode = settingService.handlePutRequest(username, map,
+                    tokenContext.getUsername());
+            return Response.status(statusCode).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+    }
+
+    /**
+     * Retrieves the default setting of the given username.
+     * 
+     * @param context
+     *            a security context
+     * @param username
+     *            a username
+     * @return the default setting of the given username
+     */
+    @GET
+    @ResourceFilters({ AuthenticationFilter.class, PiwikFilter.class,
+            BlockingFilter.class })
+    public Response retrieveDefaultSetting (@Context SecurityContext context,
+            @PathParam("username") String username) {
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.READ_DEFAULT_SETTING);
+            String settings = settingService.retrieveDefaultSettings(username,
+                    tokenContext.getUsername());
+            if (settings == null) {
+                username = tokenContext.getUsername();
+                throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                        "No default setting for username: " + username
+                                + " is found",
+                        username);
+            }
+            return Response.ok(settings).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Deletes an entry of a default setting of a user by the given
+     * key.
+     * 
+     * @param context
+     *            a security context
+     * @param username
+     *            a username
+     * @param key
+     *            the key of the default setting entry to be deleted
+     * @return
+     */
+    @DELETE
+    @Path("{key}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @ResourceFilters({ AuthenticationFilter.class, PiwikFilter.class,
+            BlockingFilter.class })
+    public Response deleteDefaultSettingEntry (@Context SecurityContext context,
+            @PathParam("username") String username,
+            @PathParam("key") String key) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.DELETE_DEFAULT_SETTING);
+            settingService.deleteKey(username, tokenContext.getUsername(), key);
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Deletes the default setting of the given username. If such a
+     * setting does not exists, no error will be thrown and response
+     * status 200 will be returned since the purpose of the request
+     * has been achieved.
+     * 
+     * @param context
+     * @param username
+     *            a username
+     * @return 200 if the request is successful
+     */
+    @DELETE
+    @ResourceFilters({ AuthenticationFilter.class, PiwikFilter.class,
+            BlockingFilter.class })
+    public Response deleteDefaultSetting (@Context SecurityContext context,
+            @PathParam("username") String username) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.DELETE_DEFAULT_SETTING);
+            settingService.deleteSetting(username, tokenContext.getUsername());
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusAdminController.java b/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusAdminController.java
new file mode 100644
index 0000000..b4946fb
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusAdminController.java
@@ -0,0 +1,79 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import de.ids_mannheim.korap.config.NamedVCLoader;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.dto.QueryDto;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.service.QueryService;
+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.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+@Controller
+@Path("{version}/admin/vc")
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+@ResourceFilters({ APIVersionFilter.class, AdminFilter.class })
+public class VirtualCorpusAdminController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private QueryService service;
+
+    @Autowired
+    private NamedVCLoader vcLoader;
+
+    @POST
+    @Path("load-cache")
+    public Response loadAndCacheSystemVC () {
+        Thread t = new Thread(vcLoader);
+        t.start();
+
+        return Response.status(Status.OK).build();
+    }
+
+    /**
+     * Lists virtual corpora by creator and type. This is a controller
+     * for system admin requiring valid system admin authentication.
+     * 
+     * If type is not specified, retrieves virtual corpora of all
+     * types. If createdBy is not specified, retrieves virtual corpora
+     * of all users.
+     * 
+     * @param securityContext
+     * @param createdBy
+     *            username of virtual corpus creator (optional)
+     * @param type
+     *            {@link ResourceType}
+     * @return a list of virtual corpora
+     */
+    @POST
+    @Path("list")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public List<QueryDto> listVCByType (
+            @FormParam("createdBy") String createdBy,
+            @FormParam("type") ResourceType type) {
+        try {
+            return service.listQueryByType(createdBy, type,
+                    QueryType.VIRTUAL_CORPUS);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java b/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
new file mode 100644
index 0000000..b69b09f
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
@@ -0,0 +1,410 @@
+package de.ids_mannheim.korap.web.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.dto.QueryAccessDto;
+import de.ids_mannheim.korap.dto.QueryDto;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.service.QueryService;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+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.BlockingFilter;
+import de.ids_mannheim.korap.web.filter.DemoUserFilter;
+import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.input.QueryJson;
+import de.ids_mannheim.korap.web.utils.ResourceFilters;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * VirtualCorpusController defines web APIs related to virtual corpus
+ * (VC) such as creating, deleting and listing user virtual corpora.
+ * All the APIs in this class are available to logged-in users, except
+ * retrieving info of system VC.
+ * 
+ * This class also includes APIs related to virtual corpus access
+ * (VCA) such as sharing and publishing VC. When a VC is published,
+ * it is shared with all users, but not always listed like system
+ * VC. It is listed for a user, once when he/she have searched for the
+ * VC. A VC can be published by creating or editing the VC.
+ * 
+ * VC name must follow the following regex [a-zA-Z_0-9-.], other
+ * characters are not allowed.
+ * 
+ * @author margaretha
+ *
+ */
+@Controller
+@Path("{version}/vc")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class, PiwikFilter.class })
+@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+public class VirtualCorpusController {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+    @Autowired
+    private QueryService service;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+
+    /**
+     * 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 system admins. System admins can create or update
+     * system VC and any VC for any users.
+     * 
+     * 
+     * @param securityContext
+     * @param vcCreator
+     *            the username of the vc creator, must be the same
+     *            as the authenticated username, except admins
+     * @param vcName
+     *            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
+    @Path("/~{vcCreator}/{vcName}")
+    @Consumes(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response createUpdateVC (@Context SecurityContext securityContext,
+            @PathParam("vcCreator") String vcCreator,
+            @PathParam("vcName") String vcName, QueryJson vc)
+            throws KustvaktException {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.EDIT_VC);
+            ParameterChecker.checkObjectValue(vc, "request entity");
+            if (vc.getQueryType() == null) {
+                vc.setQueryType(QueryType.VIRTUAL_CORPUS);
+            }
+            Status status = service.handlePutRequest(context.getUsername(),
+                    vcCreator, vcName, vc);
+            return Response.status(status).build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+    }
+
+    /**
+     * Returns the virtual corpus with the given name and creator.
+     * This web-service is also available for guests.
+     * 
+     * System admin can retrieve private or project vc of any users.
+     * 
+     * @param securityContext
+     * @param createdBy
+     *            vc creator
+     * @param vcName
+     *            vc name
+     * @return the virtual corpus with the given name and creator.
+     */
+    @GET
+    @Path("~{createdBy}/{vcName}")
+    @ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+            DemoUserFilter.class, PiwikFilter.class })
+    public QueryDto retrieveVCByName (@Context SecurityContext securityContext,
+            @PathParam("createdBy") String createdBy,
+            @PathParam("vcName") String vcName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
+            return service.retrieveQueryByName(context.getUsername(), vcName,
+                    createdBy, QueryType.VIRTUAL_CORPUS);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    @GET
+    @Path("/koralQuery/~{createdBy}/{vcName}")
+    public JsonNode retrieveVCKoralQuery (
+            @Context SecurityContext securityContext,
+            @PathParam("createdBy") String createdBy,
+            @PathParam("vcName") String vcName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
+            return service.retrieveKoralQuery(context.getUsername(), vcName,
+                    createdBy, QueryType.VIRTUAL_CORPUS);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Retrieves field values of a virtual corpus, e.g. corpus sigle.
+     * 
+     * This service is restricted to system admin only.
+     * 
+     * @param securityContext
+     * @param createdBy
+     * @param vcName
+     * @param fieldName
+     * @return
+     */
+    @GET
+    @Path("/field/~{createdBy}/{vcName}")
+    @ResourceFilters({ APIVersionFilter.class, AdminFilter.class })
+    public JsonNode retrieveVCField (@Context SecurityContext securityContext,
+            @PathParam("createdBy") String createdBy,
+            @PathParam("vcName") String vcName,
+            @QueryParam("fieldName") String fieldName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            return service.retrieveFieldValues(context.getUsername(), vcName,
+                    createdBy, QueryType.VIRTUAL_CORPUS, fieldName);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Lists all virtual corpora available to the user.
+     *
+     * System-admins can list available vc for a specific user by
+     * specifiying the username parameter.
+     * 
+     * Normal users cannot list virtual corpora
+     * available for other users. Thus, username parameter is optional
+     * and must be identical to the authenticated username.
+     * 
+     * @param securityContext
+     * @param username
+     *            a username (optional)
+     * @return a list of virtual corpora
+     */
+    @GET
+    public List<QueryDto> listAvailableVC (
+            @Context SecurityContext securityContext,
+            @QueryParam("filter-by") String filter) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
+            if (filter != null && !filter.isEmpty()) {
+                filter = filter.toLowerCase();
+                if (filter.equals("system")) {
+                    return service.listSystemQuery(QueryType.VIRTUAL_CORPUS);
+                }
+                else if (filter.equals("own")) {
+                    return service.listOwnerQuery(context.getUsername(),
+                            QueryType.VIRTUAL_CORPUS);
+                }
+                else {
+                    throw new KustvaktException(StatusCodes.UNSUPPORTED_VALUE,
+                            "The given filter is unknown or not supported.");
+                }
+            }
+            else {
+                return service.listAvailableQueryForUser(context.getUsername(),
+                        QueryType.VIRTUAL_CORPUS);
+            }
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+    /**
+     * Lists all system virtual corpora, if PathParam
+     * <em>createdBy</em> is specified to system or SYSTEM.
+     * Otherwise, lists all virtual corpora created by the given user.
+     * 
+     * This web-service is only available to the owner of the vc.
+     * Users, except system-admins, are not allowed to list vc created
+     * by other users.
+     * 
+     * Beside "system or SYSTEM', the path parameter "createdBy" must
+     * be the same as the
+     * authenticated username.
+     * 
+     * @param createdBy
+     *            system or username
+     * @param securityContext
+     * @return all system VC, if createdBy=system, otherwise a list of
+     *         virtual corpora created by the authorized user.
+     */
+    @Deprecated
+    @GET
+    @Path("~{createdBy}")
+    public List<QueryDto> listUserOrSystemVC (
+            @PathParam("createdBy") String createdBy,
+            @Context SecurityContext securityContext) {
+
+        KustvaktException e = new KustvaktException(StatusCodes.DEPRECATED,
+                "This service has been deprecated. Please use Virtual Corpus List "
+                        + "web-service.");
+        throw kustvaktResponseHandler.throwit(e);
+    }
+
+    /**
+     * 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.
+     * 
+     * @param securityContext
+     * @param createdBy
+     *            vc creator
+     * @param vcName
+     *            vc name
+     * @return HTTP status 200, if successful
+     */
+    @DELETE
+    @Path("~{createdBy}/{vcName}")
+    public Response deleteVCByName (@Context SecurityContext securityContext,
+            @PathParam("createdBy") String createdBy,
+            @PathParam("vcName") String vcName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.DELETE_VC);
+            service.deleteQueryByName(context.getUsername(), vcName, createdBy,
+                    QueryType.VIRTUAL_CORPUS);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+
+    /**
+     * VC can only be shared with a group, not individuals.
+     * Only VCA admins are allowed to share VC and the VC must have
+     * been created by themselves.
+     * 
+     * <br /><br />
+     * Not allowed via third-party apps.
+     * 
+     * @param securityContext
+     * @param vcCreator
+     *            the username of the vc creator
+     * @param vcName
+     *            the name of the vc
+     * @param groupName
+     *            the name of the group to share
+     * @return HTTP status 200, if successful
+     */
+    @POST
+    @Path("~{vcCreator}/{vcName}/share/@{groupName}")
+    public Response shareVC (@Context SecurityContext securityContext,
+            @PathParam("vcCreator") String vcCreator,
+            @PathParam("vcName") String vcName,
+            @PathParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.SHARE_VC);
+            service.shareQuery(context.getUsername(), vcCreator, vcName,
+                    groupName);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok("SUCCESS").build();
+    }
+
+    /**
+     * Only VCA Admins and system admins are allowed to delete a
+     * VC-access.
+     * 
+     * <br /><br />
+     * Not allowed via third-party apps.
+     * 
+     * @param securityContext
+     * @param accessId
+     * @return
+     */
+    @DELETE
+    @Path("access/{accessId}")
+    public Response deleteVCAccessById (
+            @Context SecurityContext securityContext,
+            @PathParam("accessId") int accessId) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.DELETE_VC_ACCESS);
+            service.deleteQueryAccess(accessId, context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+
+    /**
+     * Lists active VC-accesses available to user.
+     * 
+     * Only available to VCA and system admins.
+     * For system admins, list all VCA regardless of status.
+     * 
+     * @param securityContext
+     * @return a list of VC accesses
+     */
+    @GET
+    @Path("access")
+    public List<QueryAccessDto> listVCAccesses (
+            @Context SecurityContext securityContext,
+            @QueryParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_ACCESS_INFO);
+            if (groupName != null && !groupName.isEmpty()) {
+                return service.listQueryAccessByGroup(context.getUsername(),
+                        groupName);
+            }
+            else {
+                return service.listQueryAccessByUsername(context.getUsername());
+            }
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/APIVersionFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/APIVersionFilter.java
new file mode 100644
index 0000000..e42de93
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/APIVersionFilter.java
@@ -0,0 +1,41 @@
+package de.ids_mannheim.korap.web.filter;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.PathSegment;
+
+/**
+ * Checks API version in URL path.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+@Priority(Integer.MIN_VALUE)
+public class APIVersionFilter implements ContainerRequestFilter {
+
+    @Autowired
+    private KustvaktConfiguration config;
+
+    public void filter (ContainerRequestContext request) {
+        List<PathSegment> pathSegments = request.getUriInfo().getPathSegments();
+        String version = pathSegments.get(0).getPath();
+
+        if (!config.getSupportedVersions().contains(version)) {
+            throw new NotFoundException();
+            // throw kustvaktResponseHandler.throwit(
+            // new
+            // KustvaktException(StatusCodes.UNSUPPORTED_API_VERSION,
+            // "API " + version + " is unsupported.", version));
+        }
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/AdminFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/AdminFilter.java
new file mode 100644
index 0000000..e09ebd5
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/AdminFilter.java
@@ -0,0 +1,75 @@
+package de.ids_mannheim.korap.web.filter;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.utils.JerseyUtils;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import jakarta.annotation.Priority;
+import jakarta.servlet.ServletContext;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * Verifies admin credentials or token before allowing access to
+ * administrative services
+ * 
+ * @author hanl, margaretha
+ * 
+ * @see {@link AuthenticationFilter}
+ */
+@Component
+@Priority(Priorities.AUTHENTICATION)
+public class AdminFilter extends AuthenticationFilter {
+
+    private @Context ServletContext servletContext;
+    @Autowired
+    private AdminDao adminDao;
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Override
+    public void filter (ContainerRequestContext context) {
+        super.filter(context);
+        String username = "guest";
+        String adminToken = JerseyUtils.getFormParameters(context).asMap()
+                .getFirst("token");
+        if (!checkAdminToken(adminToken)) {
+            SecurityContext securityContext = context.getSecurityContext();
+            TokenContext tokenContext = (TokenContext) securityContext
+                    .getUserPrincipal();
+            checkAdminCredentials(tokenContext, username);
+        }
+    }
+
+    private boolean checkAdminToken (String adminToken) {
+        if (adminToken != null && !adminToken.isEmpty()) {
+            if (adminToken
+                    .equals(servletContext.getInitParameter("adminToken"))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void checkAdminCredentials (TokenContext tokenContext,
+            String username) {
+        if (tokenContext != null) {
+            username = tokenContext.getUsername();
+            if (adminDao.isAdmin(username)) {
+                return;
+            }
+        }
+
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.AUTHORIZATION_FAILED,
+                "Unauthorized operation for user: " + username, username));
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java
new file mode 100644
index 0000000..1fb9360
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java
@@ -0,0 +1,127 @@
+package de.ids_mannheim.korap.web.filter;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.authentication.AuthenticationManager;
+import de.ids_mannheim.korap.authentication.http.AuthorizationData;
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.KustvaktContext;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+
+/**
+ * Authentication filter extracts an authentication token from
+ * authorization header and uses an authentication provider
+ * with respect to the token type to create a token context as
+ * a security context.
+ * 
+ * @author hanl, margaretha
+ * @date 28/01/2014
+ * @last update 12/2017
+ */
+@Component
+@Priority(Priorities.AUTHENTICATION)
+public class AuthenticationFilter implements ContainerRequestFilter {
+
+    private static Logger jlog = LogManager
+            .getLogger(AuthenticationFilter.class);
+
+    @Autowired
+    private HttpAuthorizationHandler authorizationHandler;
+
+    @Autowired
+    private AuthenticationManager authenticationManager;
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Override
+    public void filter (ContainerRequestContext request) {
+        String host = request.getHeaderString(ContainerRequest.HOST);
+        String ua = request.getHeaderString(ContainerRequest.USER_AGENT);
+
+        String authorization = request
+                .getHeaderString(ContainerRequest.AUTHORIZATION);
+
+        if (authorization != null && !authorization.isEmpty()) {
+            TokenContext context = null;
+            AuthorizationData authData;
+            try {
+                authData = authorizationHandler
+                        .parseAuthorizationHeaderValue(authorization);
+
+                switch (authData.getAuthenticationScheme()) {
+                    // EM: For testing only, must be disabled for
+                    // production
+                    case BASIC:
+                        context = authenticationManager.getTokenContext(
+                                TokenType.BASIC, authData.getToken(), host, ua);
+                        break;
+                    // EM: has not been tested yet
+                    // case SESSION:
+                    // context =
+                    // authenticationManager.getTokenContext(
+                    // TokenType.SESSION, authData.getToken(), host,
+                    // ua);
+                    // break;
+
+                    // OAuth2 authentication scheme
+                    case BEARER:
+                        context = authenticationManager.getTokenContext(
+                                TokenType.BEARER, authData.getToken(), host,
+                                ua);
+                        break;
+                    // EM: JWT token-based authentication scheme
+                    case API:
+                        jlog.warn("Authentication filter using token API");
+                        throw new KustvaktException(
+                                StatusCodes.AUTHENTICATION_FAILED,
+                                "Authentication API is no longer supported.");
+                    default:
+                        throw new KustvaktException(
+                                StatusCodes.AUTHENTICATION_FAILED,
+                                "Authentication scheme is not supported.");
+                }
+                checkContext(context, request);
+                request.setSecurityContext(new KustvaktContext(context));
+            }
+            catch (KustvaktException e) {
+                throw kustvaktResponseHandler.throwit(e);
+            }
+        }
+    }
+
+    private void checkContext (TokenContext context,
+            ContainerRequestContext request) throws KustvaktException {
+        if (context == null) {
+            throw new KustvaktException(StatusCodes.AUTHENTICATION_FAILED,
+                    "Context is null.");
+        }
+        else if (!context.isValid()) {
+            throw new KustvaktException(StatusCodes.AUTHENTICATION_FAILED,
+                    "Context is not valid: "
+                            + "missing username, password or authentication scheme.");
+        }
+        else if (context.isSecureRequired()
+                && !request.getSecurityContext().isSecure()) {
+            throw new KustvaktException(StatusCodes.AUTHENTICATION_FAILED,
+                    "Request is not secure.");
+        }
+        else if (TimeUtils.isExpired(context.getExpirationTime())) {
+            throw new KustvaktException(StatusCodes.EXPIRED,
+                    "Access token is expired");
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/BlockingFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/BlockingFilter.java
new file mode 100644
index 0000000..e33f57f
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/BlockingFilter.java
@@ -0,0 +1,50 @@
+package de.ids_mannheim.korap.web.filter;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * @author hanl
+ * @date 11/12/2014
+ *       <p/>
+ *       endpoint filter to block access to an endpoint, in case no
+ *       anonymous access should be allowed!
+ */
+
+@Component
+@Priority(Priorities.AUTHORIZATION)
+public class BlockingFilter implements ContainerRequestFilter {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Override
+    public void filter (ContainerRequestContext request) {
+        TokenContext context;
+
+        SecurityContext securityContext = request.getSecurityContext();
+        if (securityContext != null) {
+            context = (TokenContext) securityContext.getUserPrincipal();
+        }
+        else {
+            throw kustvaktResponseHandler.throwit(
+                    new KustvaktException(StatusCodes.UNSUPPORTED_OPERATION));
+        }
+
+        if (context == null || context.isDemo()) {
+            throw kustvaktResponseHandler.throwit(
+                    new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                            "Unauthorized operation for user: guest", "guest"));
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/DemoFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/DemoFilter.java
new file mode 100644
index 0000000..2a3cdf0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/DemoFilter.java
@@ -0,0 +1,49 @@
+package de.ids_mannheim.korap.web.filter;
+
+import org.glassfish.jersey.server.ContainerRequest;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.security.context.KustvaktContext;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * @author hanl
+ * @date 08/02/2016
+ */
+@Priority(Priorities.AUTHENTICATION)
+public class DemoFilter implements ContainerRequestFilter {
+
+    @Override
+    public void filter (ContainerRequestContext request) {
+        String authentication = request
+                .getHeaderString(ContainerRequest.AUTHORIZATION);
+        if (authentication == null || authentication.isEmpty()) {
+            if (request.getSecurityContext() == null) {
+                request.setSecurityContext(createContext());
+            }
+        }
+    }
+
+    private SecurityContext createContext () {
+        TokenContext context = new TokenContext();
+        String token = null;
+        try {
+            token = HttpAuthorizationHandler
+                    .createBasicAuthorizationHeaderValue("demo", "demo2015");
+        }
+        catch (KustvaktException e) {
+            e.printStackTrace();
+        }
+        context.setToken(token);
+        context.setTokenType(TokenType.BASIC);
+        context.setUsername("demo");
+        return new KustvaktContext(context);
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java
new file mode 100644
index 0000000..300c3da
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java
@@ -0,0 +1,66 @@
+package de.ids_mannheim.korap.web.filter;
+
+import java.security.Principal;
+
+import org.glassfish.jersey.server.ContainerRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.security.context.KustvaktContext;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.UriInfo;
+
+/**
+ * Created by hanl on 7/15/14.
+ */
+@Component
+@Priority(Priorities.AUTHENTICATION)
+public class DemoUserFilter implements ContainerRequestFilter {
+
+    @Context
+    UriInfo info;
+    @Autowired
+    private KustvaktConfiguration config;
+
+    @Override
+    public void filter (ContainerRequestContext request) {
+        String host = request.getHeaderString(ContainerRequest.HOST);
+        String ua = request.getHeaderString(ContainerRequest.USER_AGENT);
+        String authentication = request
+                .getHeaderString(ContainerRequest.AUTHORIZATION);
+
+        // means that this is the public service
+        if (authentication == null || authentication.isEmpty()) {
+            Principal pr = null;
+            SecurityContext securityContext = request.getSecurityContext();
+            if (securityContext != null) {
+                pr = securityContext.getUserPrincipal();
+            }
+            if (pr == null)
+                request.setSecurityContext(
+                        new KustvaktContext(createShorterToken(host, ua)));
+        }
+    }
+
+    private TokenContext createShorterToken (String host, String agent) {
+        User demo = User.UserFactory.getDemoUser();
+        TokenContext c = new TokenContext();
+        c.setUsername(demo.getUsername());
+        c.setHostAddress(host);
+        c.setUserAgent(agent);
+        c.setExpirationTime(
+                TimeUtils.plusSeconds(config.getShortTokenTTL()).getMillis());
+        c.setTokenType(TokenType.BASIC);
+        return c;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/NonDemoBlockingFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/NonDemoBlockingFilter.java
new file mode 100644
index 0000000..1e3a05c
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/NonDemoBlockingFilter.java
@@ -0,0 +1,51 @@
+package de.ids_mannheim.korap.web.filter;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.SecurityContext;
+
+/**
+ * EM: pretty much identical to {@link BlockingFilter}, should be
+ * deleted?
+ * 
+ * @author hanl
+ * @date 11/12/2014
+ *       <p/>
+ *       endpoint filter to block access to an endpoint, in case no
+ *       anonymous access should be allowed!
+ */
+@Component
+@Priority(Priorities.AUTHORIZATION)
+public class NonDemoBlockingFilter implements ContainerRequestFilter {
+
+    @Autowired
+    private KustvaktResponseHandler kustvaktResponseHandler;
+
+    @Override
+    public void filter (ContainerRequestContext request) {
+        TokenContext context;
+        SecurityContext securityContext = request.getSecurityContext();
+        if (securityContext != null) {
+            context = (TokenContext) securityContext.getUserPrincipal();
+        }
+        else {
+            throw kustvaktResponseHandler.throwit(
+                    new KustvaktException(StatusCodes.UNSUPPORTED_OPERATION));
+        }
+
+        if (context == null || context.isDemo()) {
+            throw kustvaktResponseHandler.throwit(
+                    new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                            "Operation is not permitted for guest users"));
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/PiwikFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/PiwikFilter.java
new file mode 100644
index 0000000..42ede2a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/PiwikFilter.java
@@ -0,0 +1,129 @@
+package de.ids_mannheim.korap.web.filter;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.authentication.AuthenticationManager;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.UriBuilder;
+import net.minidev.json.JSONArray;
+
+/**
+ * @author hanl
+ * @date 13/05/2014
+ */
+@Deprecated
+//@Component
+//@Priority(Priorities.AUTHORIZATION)
+public class PiwikFilter implements ContainerRequestFilter {
+
+    private WebTarget service;
+    //    private static final String SERVICE = "http://localhost:8888";
+    private static final String SERVICE = "http://10.0.10.13";
+    private static Logger jlog = LogManager.getLogger(PiwikFilter.class);
+    public static boolean ENABLED = false;
+    private Map<String, String> customVars;
+    @Autowired
+    private AuthenticationManager authenticationManager;
+
+    public PiwikFilter () {
+        //        controller = BeansFactory.getKustvaktContext()
+        //                .getAuthenticationManager();
+        ClientConfig clientConfig = new ClientConfig();
+        if (jlog.isDebugEnabled())
+            clientConfig.register(LoggingFeature.class);
+        Client client = ClientBuilder.newClient(clientConfig);
+        UriBuilder b = UriBuilder.fromUri(SERVICE);
+        service = client.target(b.build());
+        this.customVars = new HashMap<>();
+    }
+
+    private void send (ContainerRequestContext request) {
+        Random random = new SecureRandom();
+        Locale l = null;
+        if (request.getAcceptableLanguages() != null)
+            l = request.getAcceptableLanguages().get(0);
+        try {
+            service.path("piwik/piwik.php").queryParam("idsite", "2")
+                    .queryParam("rec", "1")
+                    //todo check for empty container
+                    .queryParam("_cvar", translateCustomData())
+                    .queryParam("cip", request.getHeaderString("Host"))
+                    .queryParam("cookie", "false")
+                    .queryParam("r", String.valueOf(random.nextDouble()))
+                    .queryParam("action_name",
+                            request.getUriInfo().getRequestUri()
+                                    .toASCIIString())
+                    .request().accept("text/html")
+                    .header("Host", request.getHeaderString("Host"))
+                    .header("User-Agent", request.getHeaderString("User-Agent"))
+                    .acceptLanguage(l).method("GET");
+        }
+        catch (Exception e) {
+            // do nothing if piwik not available!
+        }
+    }
+
+    private String translateCustomData () {
+        final Map<String, List<String>> customVariables = new HashMap<String, List<String>>();
+        int i = 0;
+        for (final Map.Entry<String, String> entry : this.customVars
+                .entrySet()) {
+            i++;
+            final List<String> list = new ArrayList<String>();
+            list.add(entry.getKey());
+            list.add(entry.getValue());
+            customVariables.put(Integer.toString(i), list);
+        }
+
+        final JSONArray json = new JSONArray();
+        json.add(customVariables);
+
+        // remove unnecessary parent square brackets from JSON-string
+        String jsonString = json.toString().substring(1,
+                json.toString().length() - 1);
+        customVars.clear();
+        return jsonString;
+    }
+
+    @Override
+    public void filter (ContainerRequestContext request) {
+        if (ENABLED) {
+            //            try {
+            //                TokenContext context;
+            //                SecurityContext securityContext = request.getSecurityContext();
+            //                if (securityContext != null) {
+            //                    context = (TokenContext) securityContext.getUserPrincipal();
+            //
+            //                    if (context.getUsername() != null){
+            //                        // since this is cached, not very expensive!
+            //                        User user = authenticationManager.getUser(context.getUsername());
+            //                        Userdata data = authenticationManager
+            //                                .getUserData(user, UserSettingProcessor.class);
+            //                        if ((Boolean) data.get(Attributes.COLLECT_AUDITING_DATA))
+            //                            customVars.put("username", context.getUsername());
+            //                    }
+            //                }
+            //            }
+            //            catch (KustvaktException e) {
+            //                //do nothing
+            //            }
+            send(request);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java b/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
new file mode 100644
index 0000000..9630a0e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
@@ -0,0 +1,95 @@
+package de.ids_mannheim.korap.web.input;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+
+/**
+ * Defines attributes to register an OAuth2 client. Application name,
+ * client type and description are required attributes.
+ * 
+ * To accommodate desktop applications such as R, url and redirectURI
+ * are not compulsory.
+ * 
+ * Source is json description of a plugin.
+ * 
+ * @author margaretha
+ *
+ */
+public class OAuth2ClientJson {
+
+    // required
+    private String name;
+    private OAuth2ClientType type;
+    private String description;
+
+    // optional
+    private String url;
+    // redirect URI determines where the OAuth 2.0 service will return
+    // the user to after they have authorized a client.
+    @JsonProperty("redirect_uri")
+    private String redirectURI;
+    // Default 365 days
+    @JsonProperty("refresh_token_expiry")
+    private int refreshTokenExpiry; // in seconds
+
+    // plugins
+    private JsonNode source;
+
+    public String getName () {
+        return name;
+    }
+
+    public void setName (String name) {
+        this.name = name;
+    }
+
+    public OAuth2ClientType getType () {
+        return type;
+    }
+
+    public void setType (OAuth2ClientType type) {
+        this.type = type;
+    }
+
+    public String getUrl () {
+        return url;
+    }
+
+    public void setUrl (String url) {
+        this.url = url;
+    }
+
+    public String getRedirectURI () {
+        return redirectURI;
+    }
+
+    public void setRedirectURI (String redirectURI) {
+        this.redirectURI = redirectURI;
+    }
+
+    public String getDescription () {
+        return description;
+    }
+
+    public void setDescription (String description) {
+        this.description = description;
+    }
+
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
+
+    public JsonNode getSource () {
+        return source;
+    }
+
+    public void setSource (JsonNode source2) {
+        this.source = source2;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/input/QueryJson.java b/src/main/java/de/ids_mannheim/korap/web/input/QueryJson.java
new file mode 100644
index 0000000..dcc88e3
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/input/QueryJson.java
@@ -0,0 +1,40 @@
+package de.ids_mannheim.korap.web.input;
+
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.service.QueryService;
+import de.ids_mannheim.korap.web.controller.QueryReferenceController;
+import de.ids_mannheim.korap.web.controller.VirtualCorpusController;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Java POJO of JSON input of the virtual corpus and query controllers
+ * for creating and editing virtual corpora and query references.
+ * 
+ * @author margaretha
+ * @see VirtualCorpusController
+ * @see QueryReferenceController
+ * @see QueryService
+ */
+@Getter
+@Setter
+public class QueryJson {
+    // default false
+    private boolean isCached;
+
+    // required
+    private ResourceType type;
+    // required for queryType="VIRTUAL_CORPUS"
+    private String corpusQuery;
+    // required for queryType="QUERY"
+    private String query;
+    private String queryLanguage;
+
+    // optional
+    private String definition;
+    private String description;
+    private String status;
+    private String queryVersion;
+    private QueryType queryType;
+}
\ No newline at end of file
diff --git a/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java b/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java
new file mode 100644
index 0000000..0cc4922
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java
@@ -0,0 +1,22 @@
+package de.ids_mannheim.korap.web.input;
+
+import de.ids_mannheim.korap.web.controller.UserGroupController;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Java POJO of JSON input used in the user group controller for
+ * creating user group and managing group members.
+ * 
+ * @author margaretha
+ * @see UserGroupController
+ */
+@Deprecated
+@Getter
+@Setter
+public class UserGroupJson {
+
+    private int id;
+    private String name;
+    private String[] members;
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/FormRequestWrapper.java b/src/main/java/de/ids_mannheim/korap/web/utils/FormRequestWrapper.java
new file mode 100644
index 0000000..959ed80
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/FormRequestWrapper.java
@@ -0,0 +1,87 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.ws.rs.core.MultivaluedMap;
+
+/**
+ * Helper class to wrapp multivaluedmap into a hashmap. Depending on
+ * the strict parameter,
+ * list values are retained in the resulting wrapper map.
+ * 
+ * @author hanl
+ * @date 25/04/2015
+ */
+public class FormRequestWrapper extends HttpServletRequestWrapper {
+
+    private MultivaluedMap<String, String> form;
+
+    /**
+     * Constructs a request object wrapping the given request.
+     * 
+     * @param request
+     * @throws IllegalArgumentException
+     *             if the request is null
+     */
+    public FormRequestWrapper (HttpServletRequest request,
+                               MultivaluedMap<String, String> form) {
+        super(request);
+        this.form = form;
+    }
+
+    @Override
+    public String getParameter (String name) {
+        String value = super.getParameter(name);
+        if (value == null) {
+            value = form.getFirst(name);
+        }
+        return value;
+    }
+
+    @Override
+    public String[] getParameterValues (String name) {
+        String[] values = super.getParameterValues(name);
+        if (values == null && form.get(name) != null) {
+            values = new String[form.get(name).size()];
+            values = form.get(name).toArray(values);
+        }
+        return values;
+    }
+
+    public HashMap<String, Object> singleValueMap () {
+        return toMap(this.form, false);
+    }
+
+    /**
+     * @param strict
+     *            returns only values with size equal to one. If false
+     *            pairs key to first value
+     *            in value list and returns the result
+     * @return key/value map
+     */
+    public static HashMap<String, Object> toMap (
+            MultivaluedMap<String, String> form, boolean strict) {
+        if (form == null)
+            return null;
+        HashMap<String, Object> map = new HashMap<>();
+        for (String key : form.keySet()) {
+            if (strict && form.get(key).size() > 1)
+                continue;
+            map.put(key, form.getFirst(key));
+
+        }
+        return map;
+    }
+
+    public void put (String key, String value) {
+        this.form.putSingle(key, value);
+    }
+
+    public void put (String key, String ... values) {
+        this.form.put(key, Arrays.<String> asList(values));
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/HTMLBuilder.java b/src/main/java/de/ids_mannheim/korap/web/utils/HTMLBuilder.java
new file mode 100644
index 0000000..30e3a5b
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/HTMLBuilder.java
@@ -0,0 +1,54 @@
+package de.ids_mannheim.korap.web.utils;
+
+/**
+ * @author hanl
+ * @date 12/04/2014
+ */
+public class HTMLBuilder {
+
+    private StringBuilder html;
+    private StringBuilder body;
+    private String bodyAttr;
+
+    public HTMLBuilder () {
+        html = new StringBuilder();
+        body = new StringBuilder();
+        bodyAttr = "";
+        html.append("<html>");
+    }
+
+    public void addHeader (String header, int h) {
+        html.append("<h" + h + ">");
+        html.append(header);
+        html.append("</h" + h + ">");
+    }
+
+    public void addToBody (String body) {
+        this.body.append(body);
+    }
+
+    public void addToBody (String body, String attributes) {
+        this.body.append(body);
+        bodyAttr = attributes;
+    }
+
+    public String build () {
+        if (bodyAttr.isEmpty())
+            html.append("<body>");
+        else {
+            html.append("<body ");
+            html.append(bodyAttr);
+            html.append(">");
+        }
+
+        html.append(body);
+        html.append("</body>");
+        html.append("</html>");
+        return html.toString();
+    }
+
+    @Override
+    public String toString () {
+        return build();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/JsonExceptionMapper.java b/src/main/java/de/ids_mannheim/korap/web/utils/JsonExceptionMapper.java
new file mode 100644
index 0000000..e5c28a2
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/JsonExceptionMapper.java
@@ -0,0 +1,35 @@
+package de.ids_mannheim.korap.web.utils;
+
+import com.fasterxml.jackson.databind.JsonMappingException;
+
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.web.CoreResponseHandler;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * Creates appropriate responses in case of incorrect JSON
+ * deserialization or JsonMappingException, for instance when a
+ * request parameter should be deserialized as an ENUM but the
+ * parameter value does not match any of the available ENUM values.
+ * 
+ * @author margaretha
+ *
+ */
+@Priority(value = 1)
+@Provider
+public class JsonExceptionMapper
+        implements ExceptionMapper<JsonMappingException> {
+
+    @Override
+    public Response toResponse (JsonMappingException exception) {
+        String entity = CoreResponseHandler.buildNotification(
+                StatusCodes.DESERIALIZATION_FAILED, exception.getMessage(),
+                null);
+        return Response.status(Response.Status.BAD_REQUEST).entity(entity)
+                .build();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/KustvaktMap.java b/src/main/java/de/ids_mannheim/korap/web/utils/KustvaktMap.java
new file mode 100644
index 0000000..6c5d5ea
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/KustvaktMap.java
@@ -0,0 +1,69 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author hanl
+ * @date 21/01/2016
+ */
+public class KustvaktMap {
+
+    private boolean monoTyped;
+    private Map<String, Object> values;
+
+    public KustvaktMap () {
+        this.values = new HashMap<>();
+        this.monoTyped = false;
+    }
+
+    public KustvaktMap (Map<String, Object> m) {
+        this();
+        setMap(m);
+    }
+
+    public void setMap (Map<String, Object> m) {
+        if (!isGeneric(m) | !this.monoTyped)
+            this.values.putAll(m);
+    }
+
+    public boolean isGeneric () {
+        return !this.monoTyped && isGeneric(this.values);
+    }
+
+    private static boolean isGeneric (Map<String, Object> map) {
+        int i = 0;
+        for (Object o : map.values()) {
+            if (o instanceof String)
+                i++;
+        }
+        return !(i == map.size());
+    }
+
+    public void setMonoValue (boolean monovalue) {
+        this.monoTyped = monovalue;
+    }
+
+    public String get (String key) {
+        Object o = this.values.get(key);
+        if (!isGeneric())
+            return (String) o;
+        return String.valueOf(o);
+    }
+
+    public Object getRaw (String key) {
+        return this.values.get(key);
+    }
+
+    public <T extends Object> Object get (String key, Class<T> cl) {
+        if (isGeneric())
+            return (T) this.values.get(key);
+        return get(key);
+    }
+
+    public Set<String> keySet () {
+        return this.values.keySet();
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/KustvaktResponseBuilder.java b/src/main/java/de/ids_mannheim/korap/web/utils/KustvaktResponseBuilder.java
new file mode 100644
index 0000000..5997787
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/KustvaktResponseBuilder.java
@@ -0,0 +1,29 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 20/01/2016
+ */
+public class KustvaktResponseBuilder {
+    Map<String, Object> _values;
+
+    public KustvaktResponseBuilder () {
+        this._values = new HashMap<>();
+    }
+
+    public KustvaktResponseBuilder addEntity (Object o) {
+        if (o instanceof Map && !((Map) o).isEmpty())
+            this._values.putAll((Map<? extends String, ?>) o);
+
+        return this;
+    }
+
+    @Override
+    public String toString () {
+        return "";
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/LocaleFactory.java b/src/main/java/de/ids_mannheim/korap/web/utils/LocaleFactory.java
new file mode 100644
index 0000000..f928701
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/LocaleFactory.java
@@ -0,0 +1,27 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.glassfish.hk2.api.Factory;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.ext.Provider;
+
+@Provider
+public class LocaleFactory implements Factory<Locale> {
+    @Context
+    private ContainerRequestContext context;
+
+    @Override
+    public Locale provide () {
+        final List<Locale> locales = context.getAcceptableLanguages();
+        if (locales.isEmpty())
+            return Locale.US;
+        return locales.get(0);
+    }
+
+    @Override
+    public void dispose (Locale instance) {}
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/MapUtils.java b/src/main/java/de/ids_mannheim/korap/web/utils/MapUtils.java
new file mode 100644
index 0000000..ec11de1
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/MapUtils.java
@@ -0,0 +1,42 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import jakarta.ws.rs.core.MultivaluedMap;
+
+/**
+ * Utility methods for maps
+ * 
+ * @author margaretha
+ *
+ */
+public class MapUtils {
+
+    /**
+     * Converts {@link MultivaluedMap} to {@link Map}
+     * 
+     * @param multivaluedMap
+     * @return
+     */
+    public static Map<String, String> toMap (
+            MultivaluedMap<String, String> multivaluedMap) {
+
+        if (multivaluedMap == null) {
+            return new HashMap<String, String>();
+        }
+
+        Set<String> keySet = multivaluedMap.keySet();
+        Map<String, String> map = new HashMap<String, String>(keySet.size());
+
+        for (String key : keySet) {
+            List<String> values = multivaluedMap.get(key);
+            String value = values.stream().collect(Collectors.joining(" "));
+            map.put(key, value);
+        }
+        return map;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/NotFoundMapper.java b/src/main/java/de/ids_mannheim/korap/web/utils/NotFoundMapper.java
new file mode 100644
index 0000000..b20d124
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/NotFoundMapper.java
@@ -0,0 +1,83 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.net.URI;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.eclipse.jetty.http.HttpStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ResourceContext;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * Handles not found API version by redirecting the request URI to a
+ * similar URI with current API version.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+@Provider
+public class NotFoundMapper implements ExceptionMapper<NotFoundException> {
+
+    private static Logger jlog = LogManager.getLogger(NotFoundMapper.class);
+    public static final Pattern VERSION_PATTERN = Pattern
+            .compile("/(v[0-9][^/]*)(/.*)");
+    private static final boolean DEBUG = false;
+
+    @Autowired
+    private KustvaktConfiguration config;
+
+    @Context
+    private ResourceContext resourceContext;
+
+    @Override
+    public Response toResponse (NotFoundException exception) {
+        ContainerRequestContext requestContext = resourceContext
+                .getResource(ContainerRequestContext.class);
+
+        URI notFoundUri = requestContext.getUriInfo().getRequestUri();
+
+        String path = notFoundUri.getPath();
+        String baseUrl = config.getBaseURL();
+        baseUrl = baseUrl.substring(0, baseUrl.length() - 2);
+
+        if (path.startsWith(baseUrl)) {
+            path = path.substring(baseUrl.length(), path.length());
+            Matcher matcher = VERSION_PATTERN.matcher(path);
+            if (!matcher.matches()) {
+                path = baseUrl + "/" + config.getCurrentVersion() + path;
+                URI redirectUri = UriBuilder.fromUri(notFoundUri)
+                        .replacePath(path).build();
+                if (DEBUG) {
+                    jlog.debug("REDIRECT: " + redirectUri.toString());
+                }
+                return Response.status(HttpStatus.PERMANENT_REDIRECT_308)
+                        .location(redirectUri).build();
+            }
+            else if (!matcher.group(1).equals(config.getCurrentVersion())) {
+                path = baseUrl + "/" + config.getCurrentVersion()
+                        + matcher.group(2);
+                URI redirectUri = UriBuilder.fromUri(notFoundUri)
+                        .replacePath(path).build();
+                if (DEBUG) {
+                    jlog.debug("REDIRECT replace: " + redirectUri.toString());
+                }
+                return Response.status(HttpStatus.PERMANENT_REDIRECT_308)
+                        .location(redirectUri).build();
+            }
+        }
+        return Response.status(HttpStatus.NOT_FOUND_404).build();
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/ResourceFilters.java b/src/main/java/de/ids_mannheim/korap/web/utils/ResourceFilters.java
new file mode 100644
index 0000000..fa68084
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/ResourceFilters.java
@@ -0,0 +1,28 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the list of
+ * {@link jakarta.ws.rs.container.ContainerRequestFilter}
+ * and {@link jakarta.ws.rs.container.ContainerResponseFilter}
+ * classes associated with a resource method.
+ * <p>
+ * This annotation can be specified on a class or on method(s).
+ * Specifying it
+ * at a class level means that it applies to all the methods in the
+ * class.
+ * Specifying it on a method means that it is applicable to that
+ * method only.
+ * If applied at both the class and methods level , the method value
+ * overrides
+ * the class value.
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ResourceFilters {
+    Class<?>[] value();
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/ResourceFiltersFeature.java b/src/main/java/de/ids_mannheim/korap/web/utils/ResourceFiltersFeature.java
new file mode 100644
index 0000000..9e4a0ce
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/ResourceFiltersFeature.java
@@ -0,0 +1,30 @@
+package de.ids_mannheim.korap.web.utils;
+
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * Registers {@link jakarta.ws.rs.container.ContainerRequestFilter}
+ * and {@link jakarta.ws.rs.container.ContainerResponseFilter}
+ * classes for a resource method annotated with
+ * {@link ResourceFilters}.
+ */
+@Provider
+public class ResourceFiltersFeature implements DynamicFeature {
+
+    @Override
+    public void configure (ResourceInfo resourceInfo, FeatureContext context) {
+        ResourceFilters filtersAnnotation = resourceInfo.getResourceMethod()
+                .getAnnotation(ResourceFilters.class);
+        if (filtersAnnotation == null)
+            filtersAnnotation = resourceInfo.getResourceClass()
+                    .getAnnotation(ResourceFilters.class);
+
+        if (filtersAnnotation != null) {
+            for (Class<?> filter : filtersAnnotation.value())
+                context.register(filter);
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFilters.java b/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFilters.java
new file mode 100644
index 0000000..60b226a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFilters.java
@@ -0,0 +1,26 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the list of
+ * {@link jakarta.ws.rs.container.ContainerRequestFilter}
+ * and {@link jakarta.ws.rs.container.ContainerResponseFilter}
+ * classes associated with a resource method.
+ * <p>
+ * This annotation can be specified on a class or on method(s).
+ * Specifying it
+ * at a class level means that it applies to all the methods in the
+ * class.
+ * Specifying it on a method means that it is applicable to that
+ * method only.
+ * If applied at both the class and methods level , the method value
+ * overrides
+ * the class value.
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SearchResourceFilters {}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFiltersFeature.java b/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFiltersFeature.java
new file mode 100644
index 0000000..6063aa0
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFiltersFeature.java
@@ -0,0 +1,61 @@
+package de.ids_mannheim.korap.web.utils;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.glassfish.jersey.model.internal.CommonConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * Registers {@link jakarta.ws.rs.container.ContainerRequestFilter}
+ * and {@link jakarta.ws.rs.container.ContainerResponseFilter}
+ * classes for a resource method annotated with
+ * {@link ResourceFilters}.
+ */
+@Provider
+@Component
+public class SearchResourceFiltersFeature implements DynamicFeature {
+
+    @Value("${search.resource.filters:AuthenticationFilter,DemoUserFilter}")
+    private String[] resourceFilters;
+
+    @Override
+    public void configure (ResourceInfo resourceInfo, FeatureContext context) {
+        SearchResourceFilters filters = resourceInfo.getResourceMethod()
+                .getAnnotation(SearchResourceFilters.class);
+        if (filters != null) {
+            CommonConfig con = (CommonConfig) context.getConfiguration();
+            con.getComponentBag().clear();
+        }
+        else {
+            filters = resourceInfo.getResourceClass()
+                    .getAnnotation(SearchResourceFilters.class);
+        }
+
+        if (filters != null) {
+            List<?> list = Arrays.asList(resourceFilters);
+            if (!list.contains("APIVersionFilter")) {
+                context.register(APIVersionFilter.class);
+            }
+
+            for (String c : resourceFilters) {
+                try {
+                    context.register(Class
+                            .forName("de.ids_mannheim.korap.web.filter." + c));
+                }
+                catch (ClassNotFoundException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/ShutdownHook.java b/src/main/java/de/ids_mannheim/korap/web/utils/ShutdownHook.java
new file mode 100644
index 0000000..de34e38
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/web/utils/ShutdownHook.java
@@ -0,0 +1,29 @@
+package de.ids_mannheim.korap.web.utils;
+
+/**
+ * @author hanl
+ * @date 18/02/2014
+ */
+public class ShutdownHook extends Thread {
+
+    @Override
+    public void run () {
+        //        Properties config = ExtensionBeans.getInstance().getConfiguration()
+        //                .getMailProperties();
+
+        //                Email e = new SimpleEmail();
+        //                try {
+        //                    e.setHostName(config.getProperty("mail.host"));
+        //                    e.setSmtpPort(587);
+        //                    e.setSubject("KorAP Rest service shutdown Notification");
+        //                    e.setFrom(config.getProperty("mail.from"));
+        //                    e.addTo("hanl@ids-mannheim.de");
+        //                    e.setMsg("The KorAP - REST application shut down unexpectedly!!");
+        //                    e.send();
+        //                } catch (EmailException e1) {
+        //                    e1.printStackTrace();
+        //                }
+
+    }
+
+}
diff --git a/src/main/resources/ESAPI.properties b/src/main/resources/ESAPI.properties
new file mode 100644
index 0000000..b822b21
--- /dev/null
+++ b/src/main/resources/ESAPI.properties
@@ -0,0 +1,188 @@
+#
+# OWASP Enterprise Security API (ESAPI) Properties file -- PRODUCTION Version
+# 
+# This file is part of the Open Web de.ids_mannheim.korap.news.Application Security Project (OWASP)
+# Enterprise Security API (ESAPI) project. For details, please see
+# http://www.owasp.org/index.php/ESAPI.
+#
+# Copyright (c) 2008,2009 - The OWASP Foundation
+#
+# DISCUSS: This may cause a major backwards compatibility issue, etc. but
+#		   from a name space perspective, we probably should have prefaced
+#		   all the property names with ESAPI or at least OWASP. Otherwise
+#		   there could be problems is someone loads this properties file into
+#		   the System properties.  We could also put this file into the
+#		   esapi.jar file (perhaps as a ResourceBundle) and then allow an external
+#		   ESAPI properties be defined that would overwrite these defaults.
+#		   That keeps the application's properties relatively simple as usually
+#		   they will only want to override a few properties. If looks like we
+#		   already support multiple override levels of this in the
+#		   DefaultSecurityConfiguration class, but I'm suggesting placing the
+#		   defaults in the esapi.jar itself. That way, if the jar is signed,
+#		   we could detect if those properties had been tampered with. (The
+#		   code to isSystem the jar signatures is pretty simple... maybe 70-90 LOC,
+#		   but off course there is an execution penalty (similar to the way
+#		   that the separate sunjce.jar used to be when a class from it was
+#		   first loaded). Thoughts?
+###############################################################################
+
+ESAPI.Randomizer=org.owasp.esapi.reference.DefaultRandomizer
+ESAPI.Validator=org.owasp.esapi.reference.DefaultValidator
+
+#===========================================================================
+# ESAPI Encoder
+#
+# ESAPI canonicalizes input before validation to prevent bypassing filters with encoded attacks.
+# Failure to canonicalize input is a very common mistake when implementing validation schemes.
+# Canonicalization is automatic when using the ESAPI Validator, but you can also use the
+# following code to canonicalize data.
+#
+#      ESAPI.Encoder().canonicalize( "%22hello world&#x22;" );
+#  
+# Multiple encoding is when a single encoding format is applied multiple times. Allowing
+# multiple encoding is strongly discouraged.
+Encoder.AllowMultipleEncoding=false
+
+# Mixed encoding is when multiple different encoding formats are applied, or when 
+# multiple formats are nested. Allowing multiple encoding is strongly discouraged.
+Encoder.AllowMixedEncoding=false
+
+# The default list of codecs to apply when canonicalizing untrusted data. The list should include the codecs
+# for all downstream interpreters or decoders. For example, if the data is likely to end up in a URL, HTML, or
+# inside JavaScript, then the list of codecs below is appropriate. The order of the list is not terribly important.
+Encoder.DefaultCodecList=HTMLEntityCodec,PercentCodec,JavaScriptCodec
+
+
+#===========================================================================
+# ESAPI Encryption
+#
+# The ESAPI Encryptor provides basic cryptographic functions with a simplified API.
+# To get started, generate a new key using java -classpath esapi.jar org.owasp.esapi.reference.crypto.JavaEncryptor
+# There is not currently any support for key rotation, so be careful when changing your key and salt as it
+# will invalidate all signed, encrypted, and hashed data.
+#
+# WARNING: Not all combinations of algorithms and key lengths are supported.
+# If you choose to use a key length greater than 128, you MUST download the
+# unlimited strength policy files and install in the lib directory of your JRE/JDK.
+# See http://java.sun.com/javase/downloads/index.jsp for more information.
+#
+# Backward compatibility with ESAPI Java 1.4 is supported by the two deprecated API
+# methods, Encryptor.encrypt(String) and Encryptor.decrypt(String). However, whenever
+# possible, these methods should be avoided as they use ECB cipher mode, which in almost
+# all circumstances a poor choice because of it's weakness. CBC cipher mode is the default
+# for the new Encryptor encrypt / decrypt methods for ESAPI Java 2.0.  In general, you
+# should only use this compatibility setting if you have persistent data encrypted with
+# version 1.4 and even then, you should ONLY set this compatibility mode UNTIL
+# you have decrypted all of your old encrypted data and then re-encrypted it with
+# ESAPI 2.0 using CBC mode. If you have some reason to mix the deprecated 1.4 mode
+# with the new 2.0 methods, make sure that you use the same cipher algorithm for both
+# (256-bit AES was the default for 1.4; 128-bit is the default for 2.0; see below for
+# more details.) Otherwise, you will have to use the new 2.0 encrypt / decrypt methods
+# where you can specify a SecretKey. (Note that if you are using the 256-bit AES,
+# that requires downloading the special jurisdiction policy files mentioned above.)
+#
+#		***** IMPORTANT: Do NOT forget to replace these with your own values! *****
+# To calculate these values, you can run:
+#		java -classpath esapi.jar org.owasp.esapi.reference.crypto.JavaEncryptor
+#
+#Encryptor.MasterKey=
+## default key
+#Encryptor.MasterSalt=434fsdferbs7sdf5sdf+d23=a
+
+#==============================================================
+Encryptor.MasterKey=Nowq7w96tBckpYCPkoBtjQ==
+Encryptor.MasterSalt=vRaKzzh7hLp9v3CXi7KDI/1yO3A=
+#==============================================================
+
+#===========================================================================
+# ESAPI Intrusion Detection
+#
+# Each event has a base to which .count, .interval, and .action are added
+# The IntrusionException will fire if we receive "count" events within "interval" seconds
+# The IntrusionDetector is configurable to take the following actions: log, logout, and disable
+#  (multiple actions separated by commas are allowed e.g. event.test.actions=log,disable
+#
+# Custom Events
+# Names must start with "event." as the base
+# Use IntrusionDetector.addEvent( "test" ) in your code to trigger "event.test" here
+# You can also disable intrusion detection completely by changing
+# the following parameter to true
+#
+IntrusionDetector.Disable=false
+#
+IntrusionDetector.event.test.count=2
+IntrusionDetector.event.test.interval=10
+IntrusionDetector.event.test.actions=disable,log
+
+# Exception Events
+# All EnterpriseSecurityExceptions are registered automatically
+# Call IntrusionDetector.getInstance().addException(e) for Exceptions that do not extend EnterpriseSecurityException
+# Use the fully qualified classname of the exception as the base
+
+# any intrusion is an attack
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.count=1
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.interval=1
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.actions=log,disable,logout
+
+# for test purposes
+# CHECKME: Shouldn't there be something in the property name itself that designates
+#		   that these are for testing???
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.count=10
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.interval=5
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.actions=log,disable,logout
+
+# rapid validation errors indicate scans or attacks in progress
+org.owasp.esapi.errors.ValidationException.count=10
+org.owasp.esapi.errors.ValidationException.interval=10
+org.owasp.esapi.errors.ValidationException.actions=log,logout
+
+# sessions jumping between hosts indicates session hijacking
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.count=2
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.interval=10
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.actions=log,logout
+
+
+#===========================================================================
+# ESAPI Validation
+#
+# The ESAPI Validator works on regular expressions with defined names. You can define names
+# either here, or you may define application specific patterns in a separate file defined below.
+# This allows enterprises to specify both organizational standards as well as application specific
+# validation rules.
+#
+Validator.ConfigurationFile=validation.properties
+
+# Validators used by ESAPI
+Validator.AccountName=^[a-zA-Z0-9]{3,20}$
+Validator.SystemCommand=^[a-zA-Z\\-\\/]{1,64}$
+Validator.RoleName=^[a-z]{1,20}$
+
+#the word TEST below should be changed to your application
+#name - only relative URL's are supported
+Validator.Redirect=^\\/test.*$
+
+# Global HTTP Validation Rules
+# Values with Base64 encoded data (e.g. encrypted state) will need at least [a-zA-Z0-9\/+=]
+Validator.HTTPScheme=^(http|https)$
+Validator.HTTPServerName=^[a-zA-Z0-9_.\\-]*$
+Validator.HTTPParameterName=^[a-zA-Z0-9_]{1,32}$
+Validator.HTTPParameterValue=^[a-zA-Z0-9.\\-\\/+=@_ ]*$
+Validator.HTTPCookieName=^[a-zA-Z0-9\\-_]{1,32}$
+Validator.HTTPCookieValue=^[a-zA-Z0-9\\-\\/+=_ ]*$
+Validator.HTTPHeaderName=^[a-zA-Z0-9\\-_]{1,32}$
+Validator.HTTPHeaderValue=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ ]*$
+Validator.HTTPContextPath=^\\/?[a-zA-Z0-9.\\-\\/_]*$
+Validator.HTTPServletPath=^[a-zA-Z0-9.\\-\\/_]*$
+Validator.HTTPPath=^[a-zA-Z0-9.\\-_]*$
+Validator.HTTPQueryString=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ %]*$
+Validator.HTTPURI=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ ]*$
+Validator.HTTPURL=^.*$
+Validator.HTTPJSESSIONID=^[A-Z0-9]{10,30}$
+
+# Validation of file related input
+Validator.FileName=^[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$
+Validator.DirectoryName=^[a-zA-Z0-9:/\\\\!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$
+
+# Validation of dates. Controls whether or not 'lenient' dates are accepted.
+# See DataFormat.setLenient(boolean flag) for further details.
+Validator.AcceptLenientDates=false
diff --git a/src/main/resources/annotation-scripts/foundries/base.js b/src/main/resources/annotation-scripts/foundries/base.js
new file mode 100644
index 0000000..a21cce6
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/base.js
@@ -0,0 +1,15 @@
+define(["hint/foundries"], function (ah) {
+  ah["-"].push(
+    ["Base Annotation", "base/", "Structure"]
+  );
+  
+  ah["base/"] = [
+	["Structure", "s="]
+  ];
+  
+  ah["base/s="] = [
+    ["s", "s", "Sentence"],
+    ["p", "p", "Paragraph"],
+    ["t", "t", "Text"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/cnx.js b/src/main/resources/annotation-scripts/foundries/cnx.js
new file mode 100644
index 0000000..9316cbd
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/cnx.js
@@ -0,0 +1,63 @@
+define(["hint/foundries"], function (ah) {
+  ah["-"].push(
+    ["Connexor", "cnx/", "Constituency, Lemma, Morphology, Part-of-Speech, Syntax"]
+  );
+
+  ah["cnx/"] = [
+    ["Constituency", "c="],
+    ["Lemma", "l="],
+    ["Morphology", "m="],
+    ["Part-of-Speech", "p="],
+    ["Syntax", "syn="]
+  ];
+
+  ah["cnx/c="] = [
+    ["np", "np ", "Nominal Phrase"]
+  ];
+
+  // http://www.ids-mannheim.de/cosmas2/projekt/referenz/connexor/morph.html
+  ah["cnx/m="] = [
+    ["Abbr","Abbr ", "Nouns: Abbreviation"],
+    ["CMP","CMP ", "Adjective: Comparative"],
+    ["IMP", "IMP ", "Mood: Imperative"],
+    ["IND", "IND ", "Mood: Indicative"],
+    ["INF", "INF ", "Infinitive"],
+    ["ORD","ORD ", "Numeral: Ordinal"],
+    ["PAST", "PAST ", "Tense: past"],
+    ["PCP", "PCP ", "Participle"],
+    ["PERF", "PERF ", "Perfective Participle"],
+    ["PL","PL ", "Nouns: Plural"],
+    ["PRES", "PRES ", "Tense: present"],
+    ["PROG", "PROG ", "Progressive Participle"],
+    ["Prop","Prop ", "Nouns: Proper Noun"],
+    ["SUB", "SUB ", "Mood: Subjunctive"],
+    ["SUP","SUP ", "Adjective: Superlative"]
+  ];
+
+  // http://www.ids-mannheim.de/cosmas2/projekt/referenz/connexor/morph.html
+  ah["cnx/p="] = [
+    ["A", "A ", "Adjective"],
+    ["ADV", "ADV ", "Adverb"],
+    ["CC", "CC ", "Coordination Marker"],
+    ["CS", "CS ", "Clause Marker"],
+    ["DET", "DET ", "Determiner"],
+    ["INTERJ", "INTERJ ", "Interjection"],
+    ["N", "N ", "Noun"],
+    ["NUM", "NUM ", "Numeral"],
+    ["PREP", "PREP ", "Preposition"],
+    ["PRON", "PRON ", "Pro-Nominal"],
+    ["V", "V ", "Verb"]
+  ];
+
+  // http://www.ids-mannheim.de/cosmas2/projekt/referenz/connexor/syntax.html
+  ah["cnx/syn="] = [
+    ["@ADVL", "@ADVL ", "Adverbial Head"],
+    ["@AUX", "@AUX ", "Auxiliary Verb"],
+    ["@CC", "@CC ", "Coordination"]
+    ["@MAIN", "@MAIN ", "Main Verb"],
+    ["@NH", "@NH ", "Nominal Head"],
+    ["@POSTMOD", "@POSTMOD ", "Postmodifier"],
+    ["@PREMARK", "@PREMARK ", "Preposed Marker"],
+    ["@PREMOD", "@PREMOD ", "Premodifier"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/corenlp.js b/src/main/resources/annotation-scripts/foundries/corenlp.js
new file mode 100644
index 0000000..ff3b44a
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/corenlp.js
@@ -0,0 +1,1374 @@
+define(["hint/foundries","hint/foundries/stts","hint/foundries/negranodes","hint/foundries/negraedges"],
+       function (ah, sttsArray, negraNodesArray, negraEdgesArray) {
+
+  ah["-"].push(
+    ["CoreNLP", "corenlp/", "Constituency, Named Entities, Part-of-Speech"]
+  );
+
+  ah["corenlp/"] = [
+    ["Constituency", "c="],
+    ["Named Entity", "ne=" , "Combined"],
+    ["Named Entity", "ne_dewac_175m_600=" , "ne_dewac_175m_600"],
+    ["Named Entity", "ne_hgc_175m_600=",    "ne_hgc_175m_600"],
+    ["Part-of-Speech", "p="]
+  ];
+
+  ah["corenlp/ne="] = [
+	["I-LOC",  "I-LOC ",  "Location"],
+	["I-MISC", "I-MISC ", "Miscellaneous"],
+	["I-ORG",  "I-ORG ",  "Organization"],
+	["I-PER",  "I-PER ",  "Person"]
+  ];
+  
+  ah["corenlp/ne_dewac_175m_600="] = [
+	["I-LOC",  "I-LOC ",  "Location"],
+	["I-MISC", "I-MISC ", "Miscellaneous"],
+	["I-ORG",  "I-ORG ",  "Organization"],
+	["I-PER",  "I-PER ",  "Person"]
+  ];
+	  
+  ah["corenlp/ne_hgc_175m_600="] = [
+	["I-LOC",  "I-LOC ",  "Location"],
+	["I-MISC", "I-MISC ", "Miscellaneous"],
+	["I-ORG",  "I-ORG ",  "Organization"],
+	["I-PER",  "I-PER ",  "Person"]
+  ];
+	  
+  ah["corenlp/p="] = [
+    ["ADJA","ADJA ", "Attributive Adjective"],
+    ["ADJD","ADJD ", "Predicative Adjective"],
+    ["ADV","ADV ", "Adverb"],
+    ["APPO","APPO ", "Postposition"],
+    ["APPR","APPR ", "Preposition"],
+    ["APPRART","APPRART ", "Preposition with Determiner"],
+    ["APZR","APZR ","Right Circumposition"],
+    ["ART","ART ", "Determiner"],
+    ["CARD","CARD ", "Cardinal Number"],
+    ["FM","FM ", "Foreign Material"],
+    ["ITJ","ITJ ", "Interjection"],
+    ["KOKOM","KOKOM ", "Comparison Particle"],
+    ["KON","KON ", "Coordinating Conjuncion"],
+    ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+    ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+    ["NE","NE ", "Named Entity"],
+    ["NN","NN ", "Normal Nomina"],
+    ["PAV", "PAV ", "Pronominal Adverb"],
+    ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+    ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+    ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+    ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+    ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+    ["PPER","PPER ", "Personal Pronoun"],
+    ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+    ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+    ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+    ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+    ["PRF","PRF ", "Reflexive Pronoun"],
+    ["PROAV","PROAV ", "Pronominal Adverb"],
+    ["PTKA","PTKA ","Particle with Adjective"],
+    ["PTKANT","PTKANT ", "Answering Particle"],
+    ["PTKNEG","PTKNEG ", "Negation Particle"],
+    ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+    ["PTKZU","PTKZU ", "'zu' Particle"],
+    ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+    ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+    ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+    ["TRUNC","TRUNC ","Truncated"],
+    ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+    ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+    ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+    ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+    ["VMFIN","VMFIN ", "Modal Finite Verb"],
+    ["VMINF","VMINF ", "Modal Infinite Verb"],
+    ["VMPP","VMPP ", "Modal Perfect Participle"],
+    ["VVFIN","VVFIN ","Finite Verb"],
+    ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+    ["VVINF","VVINF ", "Infinite Verb"],
+    ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+    ["VVPP","VVPP ", "Perfect Participle"],
+    ["XY", "XY ", "Non-Word"]
+  ];
+  
+  ah["corenlp/c="] = [
+    ["AA", "AA", "superlative phrase with 'am'"],
+    ["AP","AP", "adjektive phrase"],
+    ["AVP","AVP", "adverbial phrase"],
+    ["CAP","CAP", "coordinated adjektive phrase"],
+    ["CAVP","CAVP", "coordinated adverbial phrase"],
+    ["CAC","CAC", "coordinated adposition"],
+    ["CCP","CCP", "coordinated complementiser"],
+    ["CH","CH", "chunk"],
+    ["CNP","CNP", "coordinated noun phrase"],
+    ["CO","CO", "coordination"],
+    ["CPP","CPP", "coordinated adpositional phrase"],
+    ["CS","CS", "coordinated sentence"],
+    ["CVP","CVP", "coordinated verb phrase (non-finite)"],
+    ["CVZ","CVZ", "coordinated zu-marked infinitive"],
+    ["DL","DL", "discourse level constituent"],
+    ["ISU","ISU", "idiosyncratis unit"],
+    ["MPN","MPN", "multi-word proper noun"],
+    ["MTA","MTA", "multi-token adjective"],
+    ["NM","NM", "multi-token number"],
+    ["NP","NP", "noun phrase"],
+    ["PP","PP", "adpositional phrase"],
+    ["QL","QL", "quasi-languag"],
+    ["ROOT","ROOT", "root node"],
+    ["S","S", "sentence"],
+    ["VP","VP", "verb phrase (non-finite)"],
+    ["VZ","VZ", "zu-marked infinitive"]
+  ];
+  
+  ah["corenlp/c=AA-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=AP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=AVP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CAP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CAVP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CAC-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CCP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CH-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CNP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CO-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CPP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CS-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CVP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=CVZ-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=DL-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=ISU-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=MPN-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=MTA-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=NM-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=NP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=PP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=QL-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=ROOT-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=S-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=VP-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+	ah["corenlp/c=VZ-"] = [
+	  ["AC", "AC ", "adpositional case marker"],
+	  ["ADC", "ADC ", "adjective component"],
+	  ["AMS", "AMS ", "measure argument of adj"],
+	  ["APP", "APP ", "apposition"],
+	  ["AVC", "AVC ", "adverbial phrase component"],
+	  ["CC", "CC ", "comparative complement"],
+	  ["CD", "CD ", "coordinating conjunction"],
+	  ["CJ", "CJ ", "conjunct"],
+	  ["CM", "CM ", "comparative concjunction"],
+	  ["CP", "CP ", "complementizer"],
+	  ["DA", "DA ", "dative"],
+	  ["DH", "DH ", "discourse-level head"],
+	  ["DM", "DM ", "discourse marker"],
+	  ["GL", "GL ", "prenominal genitive"],
+	  ["GR", "GR ", "postnominal genitive"],
+	  ["HD", "HD ", "head"],
+	  ["JU", "JU ", "junctor"],
+	  ["MC", "MC ", "comitative"],
+	  ["MI", "MI ", "instrumental"],
+	  ["ML", "ML ", "locative"],
+	  ["MNR", "MNR ", "postnominal modifier"],
+	  ["MO", "MO ", "modifier"],
+	  ["MR", "MR ", "rhetorical modifier"],
+	  ["MW", "MW ", "way (directional modifier)"],
+	  ["NG", "NG ", "negation"],
+	  ["NK", "NK ", "noun kernel modifier"],
+	  ["NMC", "NMC ", "numerical component"],
+	  ["OA", "OA ", "accusative object"],
+	  ["OA2", "OA2 ", "second accusative object"],
+	  ["OC", "OC ", "clausal object"],
+	  ["OG", "OG ", "genitive object"],
+	  ["PD", "PD ", "predicate"],
+	  ["PG", "PG ", "pseudo-genitive"],
+	  ["PH", "PH ", "placeholder"],
+	  ["PM", "PM ", "morphological particle"],
+	  ["PNC", "PNC ", "proper noun component"],
+	  ["RC", "RC ", "relative clause"],
+	  ["RE", "RE ", "repeated element"],
+	  ["RS", "RS ", "reported speech"],
+	  ["SB", "SB ", "subject"],
+	  ["SBP", "SBP ", "passivised subject (PP)"],
+	  ["SP", "SP ", "subject or predicate"],
+	  ["SVP", "SVP ", "separable verb prefix"],
+	  ["UC", "UC ", "(idiosyncratic) unit component"],
+	  ["VO", "VO ", "vocative"]
+	];
+
+
+  
+//  for (var i in negraNodesArray) {
+//    ah["corenlp/c=" + negraNodesArray[i][0] + '-'] = negraEdgesArray;
+//  };
+});
diff --git a/src/main/resources/annotation-scripts/foundries/dereko.js b/src/main/resources/annotation-scripts/foundries/dereko.js
new file mode 100644
index 0000000..e31f42e
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/dereko.js
@@ -0,0 +1,9 @@
+define(["hint/foundries"], function (ah) {
+  ah["-"].push(
+    ["DeReKo", "dereko/", "Structure"]
+  );
+  
+  ah["dereko/"] = [
+	["Structure", "s="]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/drukola.js b/src/main/resources/annotation-scripts/foundries/drukola.js
new file mode 100644
index 0000000..b57f8a4
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/drukola.js
@@ -0,0 +1,104 @@
+define(["hint/foundries"], function (ah) {
+  ah["-"].push(
+    ["DRuKoLa", "drukola/", "Lemma, Morphology, Part-of-Speech"]
+  );
+
+  ah["drukola/"] = [
+    ["Lemma", "l="],
+    ["Morphology", "m="],
+    ["Part-of-Speech", "p="]      
+  ];
+
+  ah["drukola/m="] =  [
+    ["CTAG", "ctag:"]
+  ];
+
+  ah["drukola/m=ctag:"] = [
+    ["A","a ","Adjective"],
+    ["Y","y ","Abbreviation"],
+    ["AN","an ","Adjective, Indefinite"],
+    ["APRY","apry ","Adjective, Plural, Direct, Definite"],
+    ["APN","apn ","Adjective, Plural, Indefinite"],
+    ["APOY","apoy ","Adjective, Plural, Oblique, Definite"],
+    ["APON","apon ","Adjective, Plural, Oblique, Indefinite"],
+    ["ASRY","asry ","Adjective, Singular, Direct, Definite"],
+    ["ASN","asn ","Adjective, Singular, Indefinite"],
+    ["ASOY","asoy ","Adjective, Singular, Oblique, Definite"],
+    ["ASON","ason ","Adjective, Singular, Oblique, Indefinite"],
+    ["ASVY","asvy ","Adjective, Singular, Vocative, Definite"],
+    ["ASVN","asvn ","Adjective, Singular, Vocative, Indefinite"],
+    ["R","r ","Adverb"],
+    ["RC","rc ","Adverb, Portmanteau"],
+    ["TS","ts ","Article, Definite or Possessive, Singular"],
+    ["TP","tp ","Article, Indefinite or Possessive, Plural"],
+    ["TPR","tpr ","Article, non-Possessive, Plural, Direct"],
+    ["TPO","tpo ","Article, non-Possessive, Plural, Oblique"],
+    ["TSR","tsr ","Article, non-Possessive, Singular, Direct"],
+    ["TSO","tso ","Article, non-Possessive, Singular, Oblique"],
+    ["NPRY","npry ","Common Noun, Plural, Direct, Definite"],
+    ["NPN","npn ","Common Noun, Plural, Indefinite"],
+    ["NPOY","npoy ","Common Noun, Plural, Oblique, Definite"],
+    ["NPVY","npvy ","Common Noun, Plural, Vocative, Definite"],
+    ["NN","nn ","Common Noun, singular"],
+    ["NSY","nsy ","Common Noun, Singular, Definite"],
+    ["NSRY","nsry ","Common Noun, Singular, Direct, Definite"],
+    ["NSRN","nsrn ","Common Noun, Singular, Direct, Indefinite"],
+    ["NSN","nsn ","Common Noun, Singular, Indefinite"],
+    ["NSOY","nsoy ","Common Noun, Singular, Oblique, Definite"],
+    ["NSON","nson ","Common Noun, Singular, Oblique, Indefinite"],
+    ["NSVY","nsvy ","Common Noun, Singular, Vocative, Definite"],
+    ["NSVN","nsvn ","Common Noun, Singular, Vocative, Indefinite"],
+    ["CR","cr ","Conjunctio, portmanteau"],
+    ["C","c ","Conjunction"],
+    ["QF","qf ","Future Particle"],
+    ["QN","qn ","Infinitival Particle"],
+    ["I","i ","Interjection"],
+    ["QZ","qz ","Negative Particle"],
+    ["M","m ","Numeral"],
+    ["PP","pp ","Personal Pronoun"],
+    ["PPP","ppp ","Personal Pronoun, Plural"],
+    ["PPPA","pppa ","Personal Pronoun, Plural, Acc."],
+    ["PPPD","pppd ","Personal Pronoun, Plural, Dative"],
+    ["PPPR","pppr ","Personal Pronoun, Plural, Direct"],
+    ["PPPO","pppo ","Personal Pronoun, Plural, Oblique"],
+    ["PPS","pps ","Personal Pronoun, Singular"],
+    ["PPSA","ppsa ","Personal Pronoun, Singular, Accusative"],
+    ["PPSD","ppsd ","Personal Pronoun, Singular, Dative"],
+    ["PPSR","ppsr ","Personal Pronoun, Singular, Direct"],
+    ["PPSN","ppsn ","Personal Pronoun, Singular, Nominative"],
+    ["PPSO","ppso ","Personal Pronoun, Singular, Oblique"],
+    ["S","s ","Preposition"],
+    ["DMPR","dmpr ","Pronoun or Determiner, Demonstrative, Plural, Direct"],
+    ["DMPO","dmpo ","Pronoun or Determiner, Demonstrative, Plural, Oblique"],
+    ["DMSR","dmsr ","Pronoun or Determiner, Demonstrative, Singular, Direct"],
+    ["DMSO","dmso ","Pronoun or Determiner, Demonstrative, Singular, Oblique"],
+    ["PS","ps ","Pronoun or Determiner, Poss or Emph"],
+    ["PSS","pss ","Pronoun or Determiner, Poss or Emph, Singular"],
+    ["RELR","relr ","Pronoun or Determiner, Relative, Direct"],
+    ["RELO","relo ","Pronoun or Determiner, Relative, Oblique"],
+    ["NP","np ","Proper Noun"],
+    ["PI","pi ","Quantifier Pronoun or Determiner"],
+    ["PXA","pxa ","Reflexive Pronoun, Accusative"],
+    ["PXD","pxd ","Reflexive Pronoun, Dative"],
+    ["X","x ","Residual"],
+    ["QS","qs ","Subjunctive Particle"],
+    ["VA","va ","Verb, Auxiliary"],
+    ["VA1","va1 ","Verb, Auxiliary, 1st person"],
+    ["VA2P","va2p ","Verb, Auxiliary, 2nd person, Plural"],
+    ["VA2S","va2s ","Verb, Auxiliary, 2nd person, Singular"],
+    ["VA3","va3 ","Verb, Auxiliary, 3rd person"],
+    ["VA3P","va3p ","Verb, Auxiliary, 3rd person, Plural"],
+    ["VA3S","va3s ","Verb, Auxiliary, 3rd person, Singular"],
+    ["VA1P","va1p ","Verb, Auxiliary,1st person, Plural"],
+    ["VA1S","va1s ","Verb, Auxiliary,1st person, Singular"],
+    ["VG","vg ","Verb, Gerund"],
+    ["VN","vn ","Verb, Infinitive"],
+    ["V1","v1 ","Verb, Main, 1st person"],
+    ["V2","v2 ","Verb, Main, 2nd person"],
+    ["V3","v3 ","Verb, Main, 3rd person"],
+    ["VPPF","vppf ","Verb, Participle, Plural, Feminine"],
+    ["VPPM","vppm ","Verb, Participle, Plural, Masculine"],
+    ["VPSF","vpsf ","Verb, Participle, Singular, Feminine"],
+    ["VPSM","vpsm ","Verb, Participle, Singular, Masculine"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/lwc.js b/src/main/resources/annotation-scripts/foundries/lwc.js
new file mode 100644
index 0000000..600bc57
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/lwc.js
@@ -0,0 +1,57 @@
+define(["hint/foundries","hint/foundries/negraedges"], function (ah, negraEdgesArray) {
+  ah["-"].push(
+    ["LWC", "lwc/", "Dependency"]
+  );
+
+  ah["lwc/"] = [
+    ["Dependency", "d="]
+  ];
+
+  ah["lwc/d="] = [
+	["AC","AC ","adpositional case marker"],
+    ["ADC","ADC ","adjective component"],
+    ["AMS","AMS ","measure argument of adj"],
+    ["APP","APP ","apposition"],
+    ["AVC","AVC ","adverbial phrase component"],
+    ["CC","CC ","comparative complement"],
+    ["CD","CD ","coordinating conjunction"],
+    ["CJ","CJ ","conjunct"],
+    ["CM","CM ","comparative concjunction"],
+    ["CP","CP ","complementizer"],
+    ["DA","DA ","dative"],
+    ["DH","DH ","discourse-level head"],
+    ["DM","DM ","discourse marker"],
+    ["GL","GL ","prenominal genitive"],
+    ["GR","GR ","postnominal genitive"],
+    ["HD","HD ","head"],
+    ["JU","JU ","junctor"],
+    ["MC","MC ","comitative"],
+    ["MI","MI ","instrumental"],
+    ["ML","ML ","locative"],
+    ["MNR","MNR ","postnominal modifier"],
+    ["MO","MO ","modifier"],
+    ["MR","MR ","rhetorical modifier"],
+    ["MW","MW ","way (directional modifier)"],
+    ["NG","NG ","negation"],
+    ["NK","NK ","noun kernel modifier"],
+    ["NMC","NMC ","numerical component"], 
+    ["OA","OA ","accusative object"],
+    ["OA2","OA2 ","second accusative object"], 
+    ["OC","OC ","clausal object"],
+    ["OG","OG ","genitive object"], 
+    ["PD","PD ","predicate"],
+    ["PG","PG ","pseudo-genitive"],
+    ["PH","PH ","placeholder"],
+    ["PM","PM ","morphological particle"],
+    ["PNC","PNC ","proper noun component"], 
+    ["RC","RC ","relative clause"],
+    ["RE","RE ","repeated element"],
+    ["RS","RS ","reported speech"],
+    ["SB","SB ","subject"],
+    ["SBP","SBP ","passivised subject (PP)"], 
+    ["SP","SP ","subject or predicate"],
+    ["SVP","SVP ","separable verb prefix"],
+    ["UC","UC ","(idiosyncratic) unit component"], 
+    ["VO","VO ","vocative"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/malt.js b/src/main/resources/annotation-scripts/foundries/malt.js
new file mode 100644
index 0000000..c8c837b
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/malt.js
@@ -0,0 +1,47 @@
+define(["hint/foundries"], function (ah) {
+  ah["-"].push(
+    ["Malt", "malt/", "Dependency"]
+  );
+
+  ah["malt/"] = [
+    ["Dependency", "d="]
+  ];
+
+  ah["malt/d="] = [
+    ["-PUNCT-", "-PUNCT- ",""],
+    ["-UNKNOWN-","-UNKNOWN- ",""],
+    ["ADV","ADV ",""],
+    ["APP","APP ",""],
+    ["ATTR","ATTR ",""],
+    ["AUX","AUX ",""],
+    ["AVZ","AVZ ",""],
+    ["CJ","CJ ",""],
+    ["DET","DET ",""],
+    ["EXPL","EXPL ",""],
+    ["GMOD","GMOD ",""],
+    ["GRAD","GRAD ",""],
+    ["KOM","KOM ",""],
+    ["KON","KON ",""],
+    ["KONJ","KONJ ",""],
+    ["NEB","NEB ",""],
+    ["OBJA","OBJA ",""],
+    ["OBJC","OBJC ",""],
+    ["OBJD","OBJD ",""],
+    ["OBJG","OBJG ",""],
+    ["OBJI","OBJI ",""],
+    ["OBJP","OBJP ",""],
+    ["PAR","PAR ",""],
+    ["PART","PART ",""],
+    ["PN","PN ",""],
+    ["PP","PP ",""],
+    ["PRED","PRED ",""],
+    ["REL","REL ",""],
+    ["ROOT","ROOT ",""],
+    ["S","S ",""],
+    ["SUBJ","SUBJ ",""],
+    ["SUBJC","SUBJC ",""],
+    ["ZEIT","ZEIT ",""],
+    ["gmod-app","gmod-app ",""],
+    ["koord","koord ",""]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/marmot.js b/src/main/resources/annotation-scripts/foundries/marmot.js
new file mode 100644
index 0000000..a6e5b61
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/marmot.js
@@ -0,0 +1,120 @@
+define(["hint/foundries","hint/foundries/stts"], function (ah, sttsArray) {
+  ah["-"].push(
+    ["MarMoT", "marmot/", "Morphology, Part-of-Speech"]
+  );
+
+  ah["marmot/"] = [
+    ["Morphology", "m="],
+    ["Part-of-Speech", "p="]
+  ];
+
+  ah["marmot/p="] = [
+	["ADJA","ADJA ", "Attributive Adjective"],
+    ["ADJD","ADJD ", "Predicative Adjective"],
+    ["ADV","ADV ", "Adverb"],
+    ["APPO","APPO ", "Postposition"],
+    ["APPR","APPR ", "Preposition"],
+    ["APPRART","APPRART ", "Preposition with Determiner"],
+    ["APZR","APZR ","Right Circumposition"],
+    ["ART","ART ", "Determiner"],
+    ["CARD","CARD ", "Cardinal Number"],
+    ["FM","FM ", "Foreign Material"],
+    ["ITJ","ITJ ", "Interjection"],
+    ["KOKOM","KOKOM ", "Comparison Particle"],
+    ["KON","KON ", "Coordinating Conjuncion"],
+    ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+    ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+    ["NE","NE ", "Named Entity"],
+    ["NN","NN ", "Normal Nomina"],
+    ["PAV", "PAV ", "Pronominal Adverb"],
+    ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+    ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+    ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+    ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+    ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+    ["PPER","PPER ", "Personal Pronoun"],
+    ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+    ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+    ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+    ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+    ["PRF","PRF ", "Reflexive Pronoun"],
+    ["PROAV","PROAV ", "Pronominal Adverb"],
+    ["PTKA","PTKA ","Particle with Adjective"],
+    ["PTKANT","PTKANT ", "Answering Particle"],
+    ["PTKNEG","PTKNEG ", "Negation Particle"],
+    ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+    ["PTKZU","PTKZU ", "'zu' Particle"],
+    ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+    ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+    ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+    ["TRUNC","TRUNC ","Truncated"],
+    ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+    ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+    ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+    ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+    ["VMFIN","VMFIN ", "Modal Finite Verb"],
+    ["VMINF","VMINF ", "Modal Infinite Verb"],
+    ["VMPP","VMPP ", "Modal Perfect Participle"],
+    ["VVFIN","VVFIN ","Finite Verb"],
+    ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+    ["VVINF","VVINF ", "Infinite Verb"],
+    ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+    ["VVPP","VVPP ", "Perfect Participle"],
+    ["XY", "XY ", "Non-Word"]
+  ];	  
+
+  ah["marmot/m="] = [
+    ["Case", "case:"],
+    ["Degree", "degree:"],
+    ["Gender", "gender:"],
+    ["Mood", "mood:"],
+    ["Number", "number:"],
+    ["Person", "person:"],
+    ["Tense","tense:"],
+    ["No type", "<no-type> "]
+  ];
+
+  ah["marmot/m=case:"] = [
+    ["acc", "acc ", "Accusative"],
+    ["dat","dat ", "Dative"],
+    ["gen", "gen ","Genitive"],
+    ["nom","nom ", "Nominative"],
+    ["*","* ", "Undefined"]
+  ];
+
+  ah["marmot/m=degree:"] = [
+    ["comp","comp ", "Comparative"],
+    ["pos","pos ", "Positive"],
+    ["sup","sup ", "Superative"]
+  ];
+  
+  ah["marmot/m=gender:"] = [
+    ["fem", "fem ", "Feminium"],
+    ["masc", "masc ", "Masculinum"],
+    ["neut","neut ", "Neuter"],
+    ["*","* ","Undefined"]
+  ];
+
+  ah["marmot/m=mood:"] = [
+    ["imp","imp ", "Imperative"],
+    ["ind","ind ", "Indicative"],
+    ["subj","subj ", "Subjunctive"]
+  ];
+
+  ah["marmot/m=number:"] = [
+    ["pl","pl ","Plural"],
+    ["sg","sg ","Singular"],
+    ["*","* ","Undefined"]
+  ];
+
+  ah["marmot/m=person:"] = [
+    ["1","1 ", "First Person"],
+    ["2","2 ", "Second Person"],
+    ["3","3 ", "Third Person"]
+  ];
+
+  ah["marmot/m=tense:"] = [
+    ["past","past ", "Past"],
+    ["pres","pres ", "Present"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/mate.js b/src/main/resources/annotation-scripts/foundries/mate.js
new file mode 100644
index 0000000..f0d5c12
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/mate.js
@@ -0,0 +1,130 @@
+define(["hint/foundries","hint/foundries/stts"], function (ah, sttsArray) {
+//  var mateSttsArray = sttsArray.slice(0);
+//  mateSttsArray.push(
+//    ["<root-POS>","<root-POS>","Root Part of Speech"]
+//  );
+
+  var ah = KorAP.annotationHelper = KorAP.annotationHelper || { "-" : [] };
+
+  ah["-"].push(
+    ["Mate", "mate/", "Lemma, Morphology, Part-of-Speech"]
+  );
+
+  ah["mate/"] = [
+    // Inactive: "d" : ["d=", "Dependency"],
+    ["Lemma", "l="],
+    ["Morphology", "m="],
+    ["Part-of-Speech", "p="]
+  ];
+
+  // Inactive: mate/d=
+  ah["mate/p="] = [
+	["ADJA","ADJA ", "Attributive Adjective"],
+    ["ADJD","ADJD ", "Predicative Adjective"],
+    ["ADV","ADV ", "Adverb"],
+    ["APPO","APPO ", "Postposition"],
+    ["APPR","APPR ", "Preposition"],
+    ["APPRART","APPRART ", "Preposition with Determiner"],
+    ["APZR","APZR ","Right Circumposition"],
+    ["ART","ART ", "Determiner"],
+    ["CARD","CARD ", "Cardinal Number"],
+    ["FM","FM ", "Foreign Material"],
+    ["ITJ","ITJ ", "Interjection"],
+    ["KOKOM","KOKOM ", "Comparison Particle"],
+    ["KON","KON ", "Coordinating Conjuncion"],
+    ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+    ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+    ["NE","NE ", "Named Entity"],
+    ["NN","NN ", "Normal Nomina"],
+    ["PAV", "PAV ", "Pronominal Adverb"],
+    ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+    ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+    ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+    ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+    ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+    ["PPER","PPER ", "Personal Pronoun"],
+    ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+    ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+    ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+    ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+    ["PRF","PRF ", "Reflexive Pronoun"],
+    ["PROAV","PROAV ", "Pronominal Adverb"],
+    ["PTKA","PTKA ","Particle with Adjective"],
+    ["PTKANT","PTKANT ", "Answering Particle"],
+    ["PTKNEG","PTKNEG ", "Negation Particle"],
+    ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+    ["PTKZU","PTKZU ", "'zu' Particle"],
+    ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+    ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+    ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+    ["TRUNC","TRUNC ","Truncated"],
+    ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+    ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+    ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+    ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+    ["VMFIN","VMFIN ", "Modal Finite Verb"],
+    ["VMINF","VMINF ", "Modal Infinite Verb"],
+    ["VMPP","VMPP ", "Modal Perfect Participle"],
+    ["VVFIN","VVFIN ","Finite Verb"],
+    ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+    ["VVINF","VVINF ", "Infinite Verb"],
+    ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+    ["VVPP","VVPP ", "Perfect Participle"],
+    ["XY", "XY ", "Non-Word"],
+    ["<root-POS>","<root-POS>","Root Part of Speech"]
+  ];
+
+  ah["mate/m="] = [
+    ["Case", "case:"],
+    ["Degree", "degree:"],
+    ["Gender", "gender:"],
+    ["Mood", "mood:"],
+    ["Number", "number:"],
+    ["Person", "person:"],
+    ["Tense","tense:"],
+    ["No type", "<no-type> "]
+  ];
+
+  ah["mate/m=case:"] = [
+    ["acc", "acc ", "Accusative"],
+    ["dat","dat ", "Dative"],
+    ["gen", "gen ","Genitive"],
+    ["nom","nom ", "Nominative"],
+    ["*","* ", "Undefined"]
+  ];
+
+  ah["mate/m=degree:"] = [
+    ["comp","comp ", "Comparative"],
+    ["pos","pos ", "Positive"],
+    ["sup","sup ", "Superative"]
+  ];
+
+  ah["mate/m=gender:"] = [
+    ["fem", "fem ", "Feminium"],
+    ["masc", "masc ", "Masculinum"],
+    ["neut","neut ", "Neuter"],
+    ["*","* ","Undefined"]
+  ];
+
+  ah["mate/m=mood:"] = [
+    ["imp","imp ", "Imperative"],
+    ["ind","ind ", "Indicative"],
+    ["subj","subj ", "Subjunctive"]
+  ];
+
+  ah["mate/m=number:"] = [
+    ["pl","pl ","Plural"],
+    ["sg","sg ","Singular"],
+    ["*","* ","Undefined"]
+  ];
+
+  ah["mate/m=person:"] = [
+    ["1","1 ", "First Person"],
+    ["2","2 ", "Second Person"],
+    ["3","3 ", "Third Person"]
+  ];
+  ah["mate/m=tense:"] = [
+    ["past","past ", "Past"],
+    ["pres","pres ", "Present"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/opennlp.js b/src/main/resources/annotation-scripts/foundries/opennlp.js
new file mode 100644
index 0000000..d790eba
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/opennlp.js
@@ -0,0 +1,65 @@
+define(["hint/foundries","hint/foundries/stts"], function (ah, sttsArray) {
+  ah["-"].push(
+    ["OpenNLP", "opennlp/", "Part-of-Speech"]
+  );
+
+  ah["opennlp/"] = [
+    ["Part-of-Speech", "p="]
+  ];
+
+//  ah["opennlp/p="] = sttsArray;
+  ah["opennlp/p="] = [
+	  ["ADJA","ADJA ", "Attributive Adjective"],
+	  ["ADJD","ADJD ", "Predicative Adjective"],
+	  ["ADV","ADV ", "Adverb"],
+	  ["APPO","APPO ", "Postposition"],
+	  ["APPR","APPR ", "Preposition"],
+	  ["APPRART","APPRART ", "Preposition with Determiner"],
+	  ["APZR","APZR ","Right Circumposition"],
+	  ["ART","ART ", "Determiner"],
+	  ["CARD","CARD ", "Cardinal Number"],
+	  ["FM","FM ", "Foreign Material"],
+	  ["ITJ","ITJ ", "Interjection"],
+	  ["KOKOM","KOKOM ", "Comparison Particle"],
+	  ["KON","KON ", "Coordinating Conjuncion"],
+	  ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+	  ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+	  ["NE","NE ", "Named Entity"],
+	  ["NN","NN ", "Normal Nomina"],
+	  ["PAV", "PAV ", "Pronominal Adverb"],
+	  ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+	  ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+	  ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+	  ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+	  ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+	  ["PPER","PPER ", "Personal Pronoun"],
+	  ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+	  ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+	  ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+	  ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+	  ["PRF","PRF ", "Reflexive Pronoun"],
+	  ["PROAV","PROAV ", "Pronominal Adverb"],
+	  ["PTKA","PTKA ","Particle with Adjective"],
+	  ["PTKANT","PTKANT ", "Answering Particle"],
+	  ["PTKNEG","PTKNEG ", "Negation Particle"],
+	  ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+	  ["PTKZU","PTKZU ", "'zu' Particle"],
+	  ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+	  ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+	  ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+	  ["TRUNC","TRUNC ","Truncated"],
+	  ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+	  ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+	  ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+	  ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+	  ["VMFIN","VMFIN ", "Modal Finite Verb"],
+	  ["VMINF","VMINF ", "Modal Infinite Verb"],
+	  ["VMPP","VMPP ", "Modal Perfect Participle"],
+	  ["VVFIN","VVFIN ","Finite Verb"],
+	  ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+	  ["VVINF","VVINF ", "Infinite Verb"],
+	  ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+	  ["VVPP","VVPP ", "Perfect Participle"],
+	  ["XY", "XY ", "Non-Word"]
+	];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/schreibgebrauch.js b/src/main/resources/annotation-scripts/foundries/schreibgebrauch.js
new file mode 100644
index 0000000..850617f
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/schreibgebrauch.js
@@ -0,0 +1,117 @@
+define(["hint/foundries","hint/foundries/stts"], function (ah, sttsArray) {
+//  var sgbrSttsArray = sttsArray.slice(0);
+
+  // Push specific information for Schreibgebrauch
+//  sgbrSttsArray.push(
+//    ["NNE", "NNE", "Normal Nomina with Named Entity"],
+//    ["ADVART","ADVART",   "Adverb with Article"],
+//    ["EMOASC","EMOASC",   "ASCII emoticon"],
+//    ["EMOIMG","EMOIMG",   "Graphic emoticon"],
+//    ["ERRTOK","ERRTOK",   "Tokenisation Error"],
+//    ["HST",     "HST",      "Hashtag"],
+//    ["KOUSPPER","KOUSPPER", "Subordinating Conjunction (with Sentence) with Personal Pronoun"],
+//    ["ONO",     "ONO",      "Onomatopoeia"],
+//    ["PPERPPER","PPERPPER", "Personal Pronoun with Personal Pronoun"],
+//    ["URL",     "URL",      "Uniform Resource Locator"],
+//    ["VAPPER",  "VAPPER",   "Finite Auxiliary Verb with Personal Pronoun"],
+//    ["VMPPER",  "VMPPER",   "Fintite Modal Verb with Personal Pronoun"],
+//    ["VVPPER",  "VVPPER",   "Finite Full Verb with Personal Pronoun"],
+//    ["AW", "AW", "Interaction Word"],
+//    ["ADR", "ADR", "Addressing Term"],
+//    ["AWIND", "AWIND", "Punctuation Indicating Addressing Term"],
+//    ["ERRAW","ERRAW", "Part of Erroneously Separated Compound"]
+//    /*
+//      As KorAP currently doesn't support these tags, they could also be ommited
+//      ["_KOMMA", "_KOMMA", "Comma"],
+//      ["_SONST", "_SONST", "Intrasentential Punctuation Mark"],
+//      ["_ENDE", "_ENDE", "Punctuation Mark at the end of the Sentence"]
+//    */
+//  );
+
+  // Sort by tag
+  sgbrSttsArray.sort(function (a,b) { return a[0].localeCompare(b[0]) });
+
+  
+  ah["-"].push(
+    ["Schreibgebrauch", "sgbr/", "Lemma, Lemma Variants, Part-of-Speech"]
+  );
+
+  ah["sgbr/"] = [
+    ["Lemma", "l="],
+    ["Lemma Variants", "lv="],
+    ["Part-of-Speech", "p="]
+  ];
+
+//  ah["sgbr/p="] = sgbrSttsArray;
+  ah["sgbr/p="] = [
+    ["ADJA","ADJA ", "Attributive Adjective"],
+    ["ADJD","ADJD ", "Predicative Adjective"],
+    ["ADV","ADV ", "Adverb"],
+    ["APPO","APPO ", "Postposition"],
+    ["APPR","APPR ", "Preposition"],
+    ["APPRART","APPRART ", "Preposition with Determiner"],
+    ["APZR","APZR ","Right Circumposition"],
+    ["ART","ART ", "Determiner"],
+    ["CARD","CARD ", "Cardinal Number"],
+    ["FM","FM ", "Foreign Material"],
+    ["ITJ","ITJ ", "Interjection"],
+    ["KOKOM","KOKOM ", "Comparison Particle"],
+    ["KON","KON ", "Coordinating Conjuncion"],
+    ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+    ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+    ["NE","NE ", "Named Entity"],
+    ["NN","NN ", "Normal Nomina"],
+    ["PAV", "PAV ", "Pronominal Adverb"],
+    ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+    ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+    ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+    ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+    ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+    ["PPER","PPER ", "Personal Pronoun"],
+    ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+    ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+    ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+    ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+    ["PRF","PRF ", "Reflexive Pronoun"],
+    ["PROAV","PROAV ", "Pronominal Adverb"],
+    ["PTKA","PTKA ","Particle with Adjective"],
+    ["PTKANT","PTKANT ", "Answering Particle"],
+    ["PTKNEG","PTKNEG ", "Negation Particle"],
+    ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+    ["PTKZU","PTKZU ", "'zu' Particle"],
+    ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+    ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+    ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+    ["TRUNC","TRUNC ","Truncated"],
+    ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+    ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+    ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+    ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+    ["VMFIN","VMFIN ", "Modal Finite Verb"],
+    ["VMINF","VMINF ", "Modal Infinite Verb"],
+    ["VMPP","VMPP ", "Modal Perfect Participle"],
+    ["VVFIN","VVFIN ","Finite Verb"],
+    ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+    ["VVINF","VVINF ", "Infinite Verb"],
+    ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+    ["VVPP","VVPP ", "Perfect Participle"],
+    ["XY", "XY ", "Non-Word"],
+    ["NNE", "NNE", "Normal Nomina with Named Entity"],
+    ["ADVART","ADVART",   "Adverb with Article"],
+    ["EMOASC","EMOASC",   "ASCII emoticon"],
+    ["EMOIMG","EMOIMG",   "Graphic emoticon"],
+    ["ERRTOK","ERRTOK",   "Tokenisation Error"],
+    ["HST",     "HST",      "Hashtag"],
+    ["KOUSPPER","KOUSPPER", "Subordinating Conjunction (with Sentence) with Personal Pronoun"],
+    ["ONO",     "ONO",      "Onomatopoeia"],
+    ["PPERPPER","PPERPPER", "Personal Pronoun with Personal Pronoun"],
+    ["URL",     "URL",      "Uniform Resource Locator"],
+    ["VAPPER",  "VAPPER",   "Finite Auxiliary Verb with Personal Pronoun"],
+    ["VMPPER",  "VMPPER",   "Fintite Modal Verb with Personal Pronoun"],
+    ["VVPPER",  "VVPPER",   "Finite Full Verb with Personal Pronoun"],
+    ["AW", "AW", "Interaction Word"],
+    ["ADR", "ADR", "Addressing Term"],
+    ["AWIND", "AWIND", "Punctuation Indicating Addressing Term"],
+    ["ERRAW","ERRAW", "Part of Erroneously Separated Compound"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/treetagger.js b/src/main/resources/annotation-scripts/foundries/treetagger.js
new file mode 100644
index 0000000..78a692a
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/treetagger.js
@@ -0,0 +1,66 @@
+define(["hint/foundries","hint/foundries/stts"], function (ah, sttsArray) {
+  ah["-"].push(
+    ["TreeTagger", "tt/", "Lemma, Part-of-Speech"]
+  );
+
+  ah["tt/"] = [
+    ["Lemma", "l="],
+    ["Part-of-Speech", "p="]
+  ];
+
+//  ah["tt/p="] = sttsArray;
+  ah["tt/p="] = [
+    ["ADJA","ADJA ", "Attributive Adjective"],
+    ["ADJD","ADJD ", "Predicative Adjective"],
+    ["ADV","ADV ", "Adverb"],
+    ["APPO","APPO ", "Postposition"],
+    ["APPR","APPR ", "Preposition"],
+    ["APPRART","APPRART ", "Preposition with Determiner"],
+    ["APZR","APZR ","Right Circumposition"],
+    ["ART","ART ", "Determiner"],
+    ["CARD","CARD ", "Cardinal Number"],
+    ["FM","FM ", "Foreign Material"],
+    ["ITJ","ITJ ", "Interjection"],
+    ["KOKOM","KOKOM ", "Comparison Particle"],
+    ["KON","KON ", "Coordinating Conjuncion"],
+    ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+    ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+    ["NE","NE ", "Named Entity"],
+    ["NN","NN ", "Normal Nomina"],
+    ["PAV", "PAV ", "Pronominal Adverb"],
+    ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+    ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+    ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+    ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+    ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+    ["PPER","PPER ", "Personal Pronoun"],
+    ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+    ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+    ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+    ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+    ["PRF","PRF ", "Reflexive Pronoun"],
+    ["PROAV","PROAV ", "Pronominal Adverb"],
+    ["PTKA","PTKA ","Particle with Adjective"],
+    ["PTKANT","PTKANT ", "Answering Particle"],
+    ["PTKNEG","PTKNEG ", "Negation Particle"],
+    ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+    ["PTKZU","PTKZU ", "'zu' Particle"],
+    ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+    ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+    ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+    ["TRUNC","TRUNC ","Truncated"],
+    ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+    ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+    ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+    ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+    ["VMFIN","VMFIN ", "Modal Finite Verb"],
+    ["VMINF","VMINF ", "Modal Infinite Verb"],
+    ["VMPP","VMPP ", "Modal Perfect Participle"],
+    ["VVFIN","VVFIN ","Finite Verb"],
+    ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+    ["VVINF","VVINF ", "Infinite Verb"],
+    ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+    ["VVPP","VVPP ", "Perfect Participle"],
+    ["XY", "XY ", "Non-Word"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/foundries/xip.js b/src/main/resources/annotation-scripts/foundries/xip.js
new file mode 100644
index 0000000..ff7ac5e
--- /dev/null
+++ b/src/main/resources/annotation-scripts/foundries/xip.js
@@ -0,0 +1,12 @@
+define(["hint/foundries"], function (ah) {
+  ah["-"].push(
+    ["Xerox Parser", "xip/", "Constituency, Lemma, Part-of-Speech"]
+  );
+
+  ah["xip/"] = [
+    ["Constituency", "c="],
+    // Inactive: ["Dependency", "d="],
+    ["Lemma", "l="],
+    ["Part-of-Speech", "p="]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/variables/negraedges.js b/src/main/resources/annotation-scripts/variables/negraedges.js
new file mode 100644
index 0000000..7dc9b0f
--- /dev/null
+++ b/src/main/resources/annotation-scripts/variables/negraedges.js
@@ -0,0 +1,51 @@
+define(function () {
+  // http://www.coli.uni-saarland.de/projects/sfb378/negra-corpus/negra-corpus.html
+  // http://www.coli.uni-saarland.de/projects/sfb378/negra-corpus/kanten.html
+  return [
+    ["AC","AC ","adpositional case marker"],
+    ["ADC","ADC ","adjective component"],
+    ["AMS","AMS ","measure argument of adj"],
+    ["APP","APP ","apposition"],
+    ["AVC","AVC ","adverbial phrase component"],
+    ["CC","CC ","comparative complement"],
+    ["CD","CD ","coordinating conjunction"],
+    ["CJ","CJ ","conjunct"],
+    ["CM","CM ","comparative concjunction"],
+    ["CP","CP ","complementizer"],
+    ["DA","DA ","dative"],
+    ["DH","DH ","discourse-level head"],
+    ["DM","DM ","discourse marker"],
+    ["GL","GL ","prenominal genitive"],
+    ["GR","GR ","postnominal genitive"],
+    ["HD","HD ","head"],
+    ["JU","JU ","junctor"],
+    ["MC","MC ","comitative"],
+    ["MI","MI ","instrumental"],
+    ["ML","ML ","locative"],
+    ["MNR","MNR ","postnominal modifier"],
+    ["MO","MO ","modifier"],
+    ["MR","MR ","rhetorical modifier"],
+    ["MW","MW ","way (directional modifier)"],
+    ["NG","NG ","negation"],
+    ["NK","NK ","noun kernel modifier"],
+    ["NMC","NMC ","numerical component"], 
+    ["OA","OA ","accusative object"],
+    ["OA2","OA2 ","second accusative object"], 
+    ["OC","OC ","clausal object"],
+    ["OG","OG ","genitive object"], 
+    ["PD","PD ","predicate"],
+    ["PG","PG ","pseudo-genitive"],
+    ["PH","PH ","placeholder"],
+    ["PM","PM ","morphological particle"],
+    ["PNC","PNC ","proper noun component"], 
+    ["RC","RC ","relative clause"],
+    ["RE","RE ","repeated element"],
+    ["RS","RS ","reported speech"],
+    ["SB","SB ","subject"],
+    ["SBP","SBP ","passivised subject (PP)"], 
+    ["SP","SP ","subject or predicate"],
+    ["SVP","SVP ","separable verb prefix"],
+    ["UC","UC ","(idiosyncratic) unit component"], 
+    ["VO","VO ","vocative"]
+  ];
+});
diff --git a/src/main/resources/annotation-scripts/variables/negranodes.js b/src/main/resources/annotation-scripts/variables/negranodes.js
new file mode 100644
index 0000000..b6be581
--- /dev/null
+++ b/src/main/resources/annotation-scripts/variables/negranodes.js
@@ -0,0 +1,32 @@
+define(function () {
+  // http://www.coli.uni-saarland.de/projects/sfb378/negra-corpus/negra-corpus.html
+  // http://www.coli.uni-saarland.de/projects/sfb378/negra-corpus/knoten.html
+  return [
+    ["AA", "AA", "superlative phrase with 'am'"],
+    ["AP","AP", "adjektive phrase"],
+    ["AVP","AVP", "adverbial phrase"],
+    ["CAP","CAP", "coordinated adjektive phrase"],
+    ["CAVP","CAVP", "coordinated adverbial phrase"],
+    ["CAC","CAC", "coordinated adposition"],
+    ["CCP","CCP", "coordinated complementiser"],
+    ["CH","CH", "chunk"],
+    ["CNP","CNP", "coordinated noun phrase"],
+    ["CO","CO", "coordination"],
+    ["CPP","CPP", "coordinated adpositional phrase"],
+    ["CS","CS", "coordinated sentence"],
+    ["CVP","CVP", "coordinated verb phrase (non-finite)"],
+    ["CVZ","CVZ", "coordinated zu-marked infinitive"],
+    ["DL","DL", "discourse level constituent"],
+    ["ISU","ISU", "idiosyncratis unit"],
+    ["MPN","MPN", "multi-word proper noun"],
+    ["MTA","MTA", "multi-token adjective"],
+    ["NM","NM", "multi-token number"],
+    ["NP","NP", "noun phrase"],
+    ["PP","PP", "adpositional phrase"],
+    ["QL","QL", "quasi-languag"],
+    ["ROOT","ROOT", "root node"],
+    ["S","S", "sentence"],
+    ["VP","VP", "verb phrase (non-finite)"],
+    ["VZ","VZ", "zu-marked infinitive"]
+  ]
+});
diff --git a/src/main/resources/annotation-scripts/variables/stts.js b/src/main/resources/annotation-scripts/variables/stts.js
new file mode 100644
index 0000000..f7d3b62
--- /dev/null
+++ b/src/main/resources/annotation-scripts/variables/stts.js
@@ -0,0 +1,59 @@
+define(function () {
+  return [
+    // http://www.ids-mannheim.de/cosmas2/projekt/referenz/stts/morph.html
+    // http://nachhalt.sfb632.uni-potsdam.de/owl-docu/stts.html
+    // "$.", "$(", "$,"
+    ["ADJA","ADJA ", "Attributive Adjective"],
+    ["ADJD","ADJD ", "Predicative Adjective"],
+    ["ADV","ADV ", "Adverb"],
+    ["APPO","APPO ", "Postposition"],
+    ["APPR","APPR ", "Preposition"],
+    ["APPRART","APPRART ", "Preposition with Determiner"],
+    ["APZR","APZR ","Right Circumposition"],
+    ["ART","ART ", "Determiner"],
+    ["CARD","CARD ", "Cardinal Number"],
+    ["FM","FM ", "Foreign Material"],
+    ["ITJ","ITJ ", "Interjection"],
+    ["KOKOM","KOKOM ", "Comparison Particle"],
+    ["KON","KON ", "Coordinating Conjuncion"],
+    ["KOUI","KOUI ", "Subordinating Conjunction with 'zu'"],
+    ["KOUS","KOUS ", "Subordinating Conjunction with Sentence"],
+    ["NE","NE ", "Named Entity"],
+    ["NN","NN ", "Normal Nomina"],
+    ["PAV", "PAV ", "Pronominal Adverb"],
+    ["PDAT","PDAT ","Attributive Demonstrative Pronoun"],
+    ["PDS","PDS ", "Substitutive Demonstrative Pronoun"],
+    ["PIAT","PIAT ", "Attributive Indefinite Pronoun without Determiner"],
+    ["PIDAT","PIDAT ", "Attributive Indefinite Pronoun with Determiner"],
+    ["PIS","PIS ", "Substitutive Indefinite Pronoun"],
+    ["PPER","PPER ", "Personal Pronoun"],
+    ["PPOSAT","PPOSAT ", "Attributive Possessive Pronoun"],
+    ["PPOSS","PPOSS ", "Substitutive Possessive Pronoun"],
+    ["PRELAT","PRELAT ", "Attributive Relative Pronoun"],
+    ["PRELS","PRELS ", "Substitutive Relative Pronoun"],
+    ["PRF","PRF ", "Reflexive Pronoun"],
+    ["PROAV","PROAV ", "Pronominal Adverb"],
+    ["PTKA","PTKA ","Particle with Adjective"],
+    ["PTKANT","PTKANT ", "Answering Particle"],
+    ["PTKNEG","PTKNEG ", "Negation Particle"],
+    ["PTKVZ","PTKVZ ", "Separated Verbal Particle"],
+    ["PTKZU","PTKZU ", "'zu' Particle"],
+    ["PWAT","PWAT ", "Attributive Interrogative Pronoun"],
+    ["PWAV","PWAV ", "Adverbial Interrogative Pronoun"],
+    ["PWS","PWS ", "Substitutive Interrogative Pronoun"],
+    ["TRUNC","TRUNC ","Truncated"],
+    ["VAFIN","VAFIN ", "Auxiliary Finite Verb"],
+    ["VAIMP","VAIMP ", "Auxiliary Finite Imperative Verb"],
+    ["VAINF","VAINF ", "Auxiliary Infinite Verb"],
+    ["VAPP","VAPP ", "Auxiliary Perfect Participle"],
+    ["VMFIN","VMFIN ", "Modal Finite Verb"],
+    ["VMINF","VMINF ", "Modal Infinite Verb"],
+    ["VMPP","VMPP ", "Modal Perfect Participle"],
+    ["VVFIN","VVFIN ","Finite Verb"],
+    ["VVIMP","VVIMP ", "Finite Imperative Verb"],
+    ["VVINF","VVINF ", "Infinite Verb"],
+    ["VVIZU","VVIZU ", "Infinite Verb with 'zu'"],
+    ["VVPP","VVPP ", "Perfect Participle"],
+    ["XY", "XY ", "Non-Word"]
+  ];
+});
diff --git a/src/main/resources/basic-config.xml b/src/main/resources/basic-config.xml
new file mode 100644
index 0000000..d882532
--- /dev/null
+++ b/src/main/resources/basic-config.xml
@@ -0,0 +1,397 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:p="http://www.springframework.org/schema/p"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xmlns:aop="http://www.springframework.org/schema/aop"
+	xmlns:tx="http://www.springframework.org/schema/tx"
+	xmlns="http://www.springframework.org/schema/beans"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:cache="http://www.springframework.org/schema/cache"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans
+           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
+           http://www.springframework.org/schema/tx
+           http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
+           http://www.springframework.org/schema/aop
+           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
+           http://www.springframework.org/schema/context
+           http://www.springframework.org/schema/context/spring-context-4.0.xsd
+           http://www.springframework.org/schema/util
+           http://www.springframework.org/schema/util/spring-util-4.0.xsd">
+
+	<context:component-scan
+		base-package="de.ids_mannheim.korap" />
+	<context:annotation-config />
+
+	<bean id="props"
+		class="org.springframework.beans.factory.config.PropertiesFactoryBean">
+		<property name="ignoreResourceNotFound" value="true" />
+		<property name="locations">
+			<array>
+				<value>classpath:kustvakt.conf</value>
+				<value>file:./kustvakt.conf</value>
+			</array>
+		</property>
+	</bean>
+
+	<bean id="placeholders"
+		class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
+		<property name="ignoreResourceNotFound" value="true" />
+		<property name="locations">
+			<array>
+				<value>classpath:properties/basic-jdbc.properties</value>
+				<value>file:./basic-jdbc.properties</value>
+				<value>classpath:properties/mail.properties</value>
+				<value>file:./mail.properties</value>
+				<value>classpath:properties/hibernate.properties</value>
+
+				<value>classpath:kustvakt.conf</value>
+				<value>file:./kustvakt.conf</value>
+			</array>
+		</property>
+	</bean>
+
+	<bean id='cacheManager'
+		class='org.springframework.cache.ehcache.EhCacheCacheManager'
+		p:cacheManager-ref='ehcache' />
+
+	<bean id='ehcache'
+		class='org.springframework.cache.ehcache.EhCacheManagerFactoryBean'
+		p:configLocation='classpath:ehcache.xml' p:shared='true' />
+
+	<!--class="org.apache.commons.dbcp2.BasicDataSource" -->
+	<!-- org.springframework.jdbc.datasource.SingleConnectionDataSource -->
+	<bean id="dataSource"
+		class="org.apache.commons.dbcp2.BasicDataSource" lazy-init="true">
+		<property name="driverClassName"
+			value="${jdbc.driverClassName}" />
+		<property name="url" value="${jdbc.url}" />
+		<property name="username" value="${jdbc.username}" />
+		<property name="password" value="${jdbc.password}" />
+		<!-- relevant for single connection datasource and sqlite -->
+		<!-- <property name="suppressClose"> <value>true</value> </property> -->
+		<!--<property name="initialSize" value="2"/> -->
+		<property name="maxTotal" value="4" />
+		<property name="maxIdle" value="2" />
+		<property name="minIdle" value="1" />
+		<property name="maxWaitMillis" value="15000" />
+		<!--<property name="poolPreparedStatements" value="true"/> -->
+	</bean>
+
+	<bean id="sqliteDataSource"
+		class="org.springframework.jdbc.datasource.SingleConnectionDataSource"
+		lazy-init="true">
+		<property name="driverClassName"
+			value="${jdbc.driverClassName}" />
+		<property name="url" value="${jdbc.url}" />
+		<property name="username" value="${jdbc.username}" />
+		<property name="password" value="${jdbc.password}" />
+		<property name="connectionProperties">
+			<props>
+				<prop key="date_string_format">yyyy-MM-dd HH:mm:ss</prop>
+			</props>
+		</property>
+
+		<!-- relevant for single connection datasource and sqlite -->
+		<property name="suppressClose">
+			<value>true</value>
+		</property>
+		<!--<property name="initialSize" value="2"/> -->
+		<!--<property name="poolPreparedStatements" value="true"/> -->
+	</bean>
+
+	<!-- to configure database for sqlite, mysql, etc. migrations -->
+	<bean id="flywayConfig"
+		class="org.flywaydb.core.api.configuration.ClassicConfiguration">
+		<property name="baselineOnMigrate" value="true" />
+		<!-- <property name="validateOnMigrate" value="false" /> -->
+		<!-- <property name="cleanOnValidationError" value="true" /> -->
+		<property name="locations"
+			value="#{'${jdbc.schemaPath}'.split(',')}" />
+		<!-- <property name="dataSource" ref="dataSource" /> -->
+		<property name="dataSource" ref="sqliteDataSource" />
+		<property name="outOfOrder" value="true" />
+	</bean>
+
+	<bean id="flyway" class="org.flywaydb.core.Flyway"
+		init-method="migrate">
+		<constructor-arg ref="flywayConfig" />
+	</bean>
+
+
+	<bean id="kustvakt_db"
+		class="de.ids_mannheim.korap.handlers.JDBCClient">
+		<constructor-arg index="0" ref="dataSource" />
+		<!-- deprecated property -->
+		<property name="database" value="${jdbc.database}" />
+	</bean>
+
+	<bean id="entityManagerFactory"
+		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
+		depends-on="flyway">
+		<!-- <property name="dataSource" ref="dataSource" /> -->
+		<property name="dataSource" ref="sqliteDataSource" />
+
+		<property name="packagesToScan">
+			<array>
+				<value>de.ids_mannheim.korap.entity</value>
+				<value>de.ids_mannheim.korap.oauth2.entity</value>
+			</array>
+		</property>
+		<property name="jpaVendorAdapter">
+			<bean id="jpaVendorAdapter"
+				class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
+				<property name="databasePlatform"
+					value="${hibernate.dialect}" />
+			</bean>
+		</property>
+		<property name="jpaProperties">
+			<props>
+				<prop key="hibernate.dialect">${hibernate.dialect}</prop>
+				<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
+				<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
+				<prop key="hibernate.cache.use_query_cache">${hibernate.cache.use_query_cache}</prop>
+				<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache.use_second_level_cache}
+				</prop>
+				<prop key="hibernate.cache.provider_class">${hibernate.cache.provider}</prop>
+				<prop key="hibernate.cache.region.factory_class">${hibernate.cache.region.factory}</prop>
+				<prop key="hibernate.jdbc.time_zone">${hibernate.jdbc.time_zone}</prop>
+				<!-- <prop key="net.sf.ehcache.configurationResourceName">classpath:ehcache.xml</prop> -->
+			</props>
+		</property>
+	</bean>
+
+	<tx:annotation-driven proxy-target-class="true"
+		transaction-manager="transactionManager" />
+
+	<bean id="transactionManager"
+		class="org.springframework.orm.jpa.JpaTransactionManager">
+		<property name="entityManagerFactory"
+			ref="entityManagerFactory" />
+	</bean>
+
+	<bean id="transactionTemplate"
+		class="org.springframework.transaction.support.TransactionTemplate">
+		<constructor-arg ref="transactionManager" />
+	</bean>
+
+	<!-- Data access objects -->
+	<bean id="adminDao" class="de.ids_mannheim.korap.dao.AdminDaoImpl" />
+	<bean id="authorizationDao"
+		class="de.ids_mannheim.korap.oauth2.dao.CachedAuthorizationDaoImpl" />
+
+	<!-- Services -->
+	<bean id="scopeService"
+		class="de.ids_mannheim.korap.oauth2.service.OAuth2ScopeServiceImpl" />
+
+	<!-- props are injected from default-config.xml -->
+	<bean id="kustvakt_config"
+		class="de.ids_mannheim.korap.config.FullConfiguration">
+		<constructor-arg name="properties" ref="props" />
+	</bean>
+
+	<bean id="initializator"
+		class="de.ids_mannheim.de.init.Initializator" init-method="init">
+	</bean>
+
+	<!-- Krill -->
+	<bean id="search_krill"
+		class="de.ids_mannheim.korap.web.SearchKrill">
+		<constructor-arg value="${krill.indexDir}" />
+	</bean>
+
+	<!-- Validator -->
+	<bean id="validator"
+		class="de.ids_mannheim.korap.validator.ApacheValidator" />
+
+	<!-- URLValidator -->
+	<bean id="redirectURIValidator"
+		class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https" index="0" />
+		<constructor-arg index="1" type="long">
+			<util:constant
+				static-field="org.apache.commons.validator.routines.UrlValidator.NO_FRAGMENTS" />
+		</constructor-arg>
+	</bean>
+	<bean id="urlValidator"
+		class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https" />
+	</bean>
+
+	<!-- Rewrite -->
+	<bean id="foundryRewrite"
+		class="de.ids_mannheim.korap.rewrite.FoundryRewrite" />
+	<bean id="collectionRewrite"
+		class="de.ids_mannheim.korap.rewrite.CollectionRewrite" />
+	<bean id="virtualCorpusRewrite"
+		class="de.ids_mannheim.korap.rewrite.VirtualCorpusRewrite" />
+	<bean id="queryReferenceRewrite"
+		class="de.ids_mannheim.korap.rewrite.QueryReferenceRewrite" />
+
+	<util:list id="rewriteTasks"
+		value-type="de.ids_mannheim.korap.rewrite.RewriteTask">
+		<ref bean="foundryRewrite" />
+		<ref bean="collectionRewrite" />
+		<ref bean="virtualCorpusRewrite" />
+		<ref bean="queryReferenceRewrite" />
+	</util:list>
+
+	<bean id="rewriteHandler"
+		class="de.ids_mannheim.korap.rewrite.RewriteHandler">
+		<constructor-arg ref="rewriteTasks" />
+	</bean>
+
+	<bean id="kustvaktResponseHandler"
+		class="de.ids_mannheim.korap.web.KustvaktResponseHandler">
+	</bean>
+
+	<!-- OAuth -->
+	<bean id="oauth2ResponseHandler"
+		class="de.ids_mannheim.korap.web.OAuth2ResponseHandler">
+	</bean>
+
+	<bean id="kustvakt_userdb"
+		class="de.ids_mannheim.korap.handlers.EntityDao">
+		<constructor-arg ref="kustvakt_db" />
+	</bean>
+
+	<bean name="kustvakt_encryption"
+		class="de.ids_mannheim.korap.encryption.KustvaktEncryption">
+		<constructor-arg ref="kustvakt_config" />
+	</bean>
+
+	<!-- authentication providers to use -->
+	<!-- <bean id="api_auth" class="de.ids_mannheim.korap.authentication.APIAuthentication"> 
+		<constructor-arg type="de.ids_mannheim.korap.config.KustvaktConfiguration" 
+		ref="kustvakt_config" /> </bean> -->
+
+	<bean id="ldap_auth"
+		class="de.ids_mannheim.korap.authentication.LdapAuth3">
+		<constructor-arg
+			type="de.ids_mannheim.korap.config.KustvaktConfiguration"
+			ref="kustvakt_config" />
+	</bean>
+
+	<bean id="basic_auth"
+		class="de.ids_mannheim.korap.authentication.BasicAuthentication" />
+
+
+	<bean id="session_auth"
+		class="de.ids_mannheim.korap.authentication.SessionAuthentication">
+		<constructor-arg
+			type="de.ids_mannheim.korap.config.KustvaktConfiguration"
+			ref="kustvakt_config" />
+		<constructor-arg
+			type="de.ids_mannheim.korap.interfaces.EncryptionIface"
+			ref="kustvakt_encryption" />
+	</bean>
+
+	<bean id="oauth2_auth"
+		class="de.ids_mannheim.korap.authentication.OAuth2Authentication" />
+
+	<util:list id="kustvakt_authproviders"
+		value-type="de.ids_mannheim.korap.interfaces.AuthenticationIface">
+		<ref bean="basic_auth" />
+		<ref bean="ldap_auth" />
+		<!-- <ref bean="session_auth" /> <ref bean="api_auth" /> -->
+		<ref bean="oauth2_auth" />
+	</util:list>
+
+
+	<bean id="userdata_details"
+		class="de.ids_mannheim.korap.handlers.UserDetailsDao">
+		<constructor-arg ref="kustvakt_db" />
+	</bean>
+
+	<bean id="userdata_settings"
+		class="de.ids_mannheim.korap.handlers.UserSettingsDao">
+		<constructor-arg ref="kustvakt_db" />
+	</bean>
+
+	<util:list id="kustvakt_userdata"
+		value-type="de.ids_mannheim.korap.interfaces.db.UserdataDbIface">
+		<ref bean="userdata_details" />
+		<ref bean="userdata_settings" />
+	</util:list>
+
+	<!-- specify type for constructor argument -->
+	<bean id="authenticationManager"
+		class="de.ids_mannheim.korap.authentication.KustvaktAuthenticationManager">
+		<constructor-arg
+			type="de.ids_mannheim.korap.interfaces.EntityHandlerIface"
+			ref="kustvakt_userdb" />
+		<constructor-arg
+			type="de.ids_mannheim.korap.interfaces.EncryptionIface"
+			ref="kustvakt_encryption" />
+		<constructor-arg ref="kustvakt_config" />
+		<constructor-arg ref="kustvakt_userdata" />
+		<!-- inject authentication providers to use -->
+		<property name="providers" ref="kustvakt_authproviders" />
+	</bean>
+
+	<!-- todo: if db interfaces not loaded via spring, does transaction even 
+		work then? -->
+	<!-- the transactional advice (i.e. what 'happens'; see the <aop:advisor/> 
+		bean below) -->
+	<tx:advice id="txAdvice" transaction-manager="txManager">
+		<!-- the transactional semantics... -->
+		<tx:attributes>
+			<!-- all methods starting with 'get' are read-only -->
+			<tx:method name="get*" read-only="true"
+				rollback-for="KorAPException" />
+			<!-- other methods use the default transaction settings (see below) -->
+			<tx:method name="*" rollback-for="KorAPException" />
+		</tx:attributes>
+	</tx:advice>
+
+	<!-- ensure that the above transactional advice runs for any execution of 
+		an operation defined by the service interface -->
+	<aop:config>
+		<aop:pointcut id="service"
+			expression="execution(* de.ids_mannheim.korap.interfaces.db.*.*(..))" />
+		<aop:advisor advice-ref="txAdvice" pointcut-ref="service" />
+	</aop:config>
+
+	<!-- similarly, don't forget the PlatformTransactionManager -->
+	<bean id="txManager"
+		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
+		<!-- <property name="dataSource" ref="dataSource" /> -->
+		<property name="dataSource" ref="sqliteDataSource" />
+	</bean>
+
+	<!-- mail -->
+	<bean id="authenticator"
+		class="de.ids_mannheim.korap.service.MailAuthenticator">
+		<constructor-arg index="0" value="${mail.username}" />
+		<constructor-arg index="1" value="${mail.password}" />
+	</bean>
+	<bean id="smtpSession" class="jakarta.mail.Session"
+		factory-method="getInstance">
+		<constructor-arg index="0">
+			<props>
+				<prop key="mail.smtp.submitter">${mail.username}</prop>
+				<prop key="mail.smtp.auth">${mail.auth}</prop>
+				<prop key="mail.smtp.host">${mail.host}</prop>
+				<prop key="mail.smtp.port">${mail.port}</prop>
+				<prop key="mail.smtp.starttls.enable">${mail.starttls.enable}</prop>
+				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
+			</props>
+		</constructor-arg>
+		<constructor-arg index="1" ref="authenticator" />
+	</bean>
+	<bean id="mailSender"
+		class="org.springframework.mail.javamail.JavaMailSenderImpl">
+		<property name="username" value="${mail.username}" />
+		<property name="password" value="${mail.password}" />
+		<property name="session" ref="smtpSession" />
+	</bean>
+	<bean id="velocityEngine"
+		class="org.apache.velocity.app.VelocityEngine">
+		<constructor-arg index="0">
+			<props>
+				<prop key="resource.loader">class</prop>
+				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+				</prop>
+			</props>
+		</constructor-arg>
+	</bean>
+</beans>
diff --git a/src/main/resources/codes.kustvakt b/src/main/resources/codes.kustvakt
new file mode 100644
index 0000000..3365699
--- /dev/null
+++ b/src/main/resources/codes.kustvakt
@@ -0,0 +1,93 @@
+# generic non localized messages for API error and message codes
+# below 500 generic status codes for low level function
+# 501 - 999 specific section codes for database, validation, 
+# upper 1000 service messages as feedback for user (partially masking low level codes) with optional descriptin of low level error
+
+
+# standard system errors
+100 : "Default Kustvakt failure!"
+101 : "No Entry found!"
+102 : "Entry exists already!"
+103 : "Unsupported operation!"
+104 : "Illegal argument found. Request could not be processed!"
+105 : "Missing argument!"
+106 : "Connection Error!"
+107 : "Missing arguments!"
+108 : "Function Not supported!"
+
+
+# 400 codes for authorization and rewrite functions
+
+400 : "Policy failure!"
+# permission denied is a service code, here it should be more 
+# specific about the nature of the error/message
+401 : ""
+402 : "Unsupported resource"
+403 : "Failed rewrite!"
+404 : ""
+405 : "Resource could not be found" --> missing resource
+406 : "No target for resource policy!"
+407 : "No condition for resource policy!"
+408 : "No permission for resource policy!"
+409 : "No policies for resource!"
+
+# 500 database errors
+
+500 : ""
+501 : "Database retrieval failure!"
+502 : "Database insert failure!"
+503 : "Database delete failure!"
+504 : "Database update failure!"
+505 : ""
+506 : ""
+507 : ""
+508 : ""
+509 : ""
+510 : ""
+511 : ""
+512 : ""
+513 : ""
+
+# validation messages
+
+600 : ""
+601 : ""
+602 : ""
+603 : ""
+604 : ""
+605 : ""
+
+
+
+# 1000 service status message codes for logging
+
+1000 : "Status Ok!"
+1001 : "Nothing changed!"
+1002 : "Request could not be processed!"
+1003 : "Access denied!"
+1004 : ""
+1005 : ""
+1006 : ""
+1007 : ""
+1008 : ""
+1009 : ""
+1010 : ""
+
+
+# 2000 codes for REST API services
+2000 : "Account deactivated. Please verify account before using this API"
+2001 : "Account confirmation failed. Please contact an adminstrator"
+2002 : "Already logged in!"
+2003 : "Authentication credentials expired!"
+2004 : "Bad credentials!"
+2005 : ""
+2006 : "Password reset failed"
+
+2007 : "Login successful!"
+2008 : "Login failed!"
+2009 : "Logout successful!"
+2010 : "Logout failed!"
+2011 : "Client registration failed!"
+2012 : "Deleting client information failed!"
+2013 : "Client could not be authorized!"
+
diff --git a/src/main/resources/db/lite/V1.1__annotation.sql b/src/main/resources/db/lite/V1.1__annotation.sql
new file mode 100644
index 0000000..e81fd53
--- /dev/null
+++ b/src/main/resources/db/lite/V1.1__annotation.sql
@@ -0,0 +1,391 @@
+INSERT INTO "annotation" VALUES(1,'opennlp','foundry',NULL,'OpenNLP',NULL);
+INSERT INTO "annotation" VALUES(2,'p','layer',NULL,'Part-of-Speech',NULL);
+INSERT INTO "annotation" VALUES(3,'ADJA','key','ADJA ','Attributive Adjective',NULL);
+INSERT INTO "annotation" VALUES(4,'ADJD','key','ADJD ','Predicative Adjective',NULL);
+INSERT INTO "annotation" VALUES(5,'ADV','key','ADV ','Adverb',NULL);
+INSERT INTO "annotation" VALUES(6,'APPO','key','APPO ','Postposition',NULL);
+INSERT INTO "annotation" VALUES(7,'APPR','key','APPR ','Preposition',NULL);
+INSERT INTO "annotation" VALUES(8,'APPRART','key','APPRART ','Preposition with Determiner',NULL);
+INSERT INTO "annotation" VALUES(9,'APZR','key','APZR ','Right Circumposition',NULL);
+INSERT INTO "annotation" VALUES(10,'ART','key','ART ','Determiner',NULL);
+INSERT INTO "annotation" VALUES(11,'CARD','key','CARD ','Cardinal Number',NULL);
+INSERT INTO "annotation" VALUES(12,'FM','key','FM ','Foreign Material',NULL);
+INSERT INTO "annotation" VALUES(13,'ITJ','key','ITJ ','Interjection',NULL);
+INSERT INTO "annotation" VALUES(14,'KOKOM','key','KOKOM ','Comparison Particle',NULL);
+INSERT INTO "annotation" VALUES(15,'KON','key','KON ','Coordinating Conjuncion',NULL);
+INSERT INTO "annotation" VALUES(16,'KOUI','key','KOUI ','Subordinating Conjunction with ''zu''',NULL);
+INSERT INTO "annotation" VALUES(17,'KOUS','key','KOUS ','Subordinating Conjunction with Sentence',NULL);
+INSERT INTO "annotation" VALUES(18,'NE','key','NE ','Named Entity',NULL);
+INSERT INTO "annotation" VALUES(19,'NN','key','NN ','Normal Nomina',NULL);
+INSERT INTO "annotation" VALUES(20,'PAV','key','PAV ','Pronominal Adverb',NULL);
+INSERT INTO "annotation" VALUES(21,'PDAT','key','PDAT ','Attributive Demonstrative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(22,'PDS','key','PDS ','Substitutive Demonstrative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(23,'PIAT','key','PIAT ','Attributive Indefinite Pronoun without Determiner',NULL);
+INSERT INTO "annotation" VALUES(24,'PIDAT','key','PIDAT ','Attributive Indefinite Pronoun with Determiner',NULL);
+INSERT INTO "annotation" VALUES(25,'PIS','key','PIS ','Substitutive Indefinite Pronoun',NULL);
+INSERT INTO "annotation" VALUES(26,'PPER','key','PPER ','Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(27,'PPOSAT','key','PPOSAT ','Attributive Possessive Pronoun',NULL);
+INSERT INTO "annotation" VALUES(28,'PPOSS','key','PPOSS ','Substitutive Possessive Pronoun',NULL);
+INSERT INTO "annotation" VALUES(29,'PRELAT','key','PRELAT ','Attributive Relative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(30,'PRELS','key','PRELS ','Substitutive Relative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(31,'PRF','key','PRF ','Reflexive Pronoun',NULL);
+INSERT INTO "annotation" VALUES(32,'PROAV','key','PROAV ','Pronominal Adverb',NULL);
+INSERT INTO "annotation" VALUES(33,'PTKA','key','PTKA ','Particle with Adjective',NULL);
+INSERT INTO "annotation" VALUES(34,'PTKANT','key','PTKANT ','Answering Particle',NULL);
+INSERT INTO "annotation" VALUES(35,'PTKNEG','key','PTKNEG ','Negation Particle',NULL);
+INSERT INTO "annotation" VALUES(36,'PTKVZ','key','PTKVZ ','Separated Verbal Particle',NULL);
+INSERT INTO "annotation" VALUES(37,'PTKZU','key','PTKZU ','''zu'' Particle',NULL);
+INSERT INTO "annotation" VALUES(38,'PWAT','key','PWAT ','Attributive Interrogative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(39,'PWAV','key','PWAV ','Adverbial Interrogative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(40,'PWS','key','PWS ','Substitutive Interrogative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(41,'TRUNC','key','TRUNC ','Truncated',NULL);
+INSERT INTO "annotation" VALUES(42,'VAFIN','key','VAFIN ','Auxiliary Finite Verb',NULL);
+INSERT INTO "annotation" VALUES(43,'VAIMP','key','VAIMP ','Auxiliary Finite Imperative Verb',NULL);
+INSERT INTO "annotation" VALUES(44,'VAINF','key','VAINF ','Auxiliary Infinite Verb',NULL);
+INSERT INTO "annotation" VALUES(45,'VAPP','key','VAPP ','Auxiliary Perfect Participle',NULL);
+INSERT INTO "annotation" VALUES(46,'VMFIN','key','VMFIN ','Modal Finite Verb',NULL);
+INSERT INTO "annotation" VALUES(47,'VMINF','key','VMINF ','Modal Infinite Verb',NULL);
+INSERT INTO "annotation" VALUES(48,'VMPP','key','VMPP ','Modal Perfect Participle',NULL);
+INSERT INTO "annotation" VALUES(49,'VVFIN','key','VVFIN ','Finite Verb',NULL);
+INSERT INTO "annotation" VALUES(50,'VVIMP','key','VVIMP ','Finite Imperative Verb',NULL);
+INSERT INTO "annotation" VALUES(51,'VVINF','key','VVINF ','Infinite Verb',NULL);
+INSERT INTO "annotation" VALUES(52,'VVIZU','key','VVIZU ','Infinite Verb with ''zu''',NULL);
+INSERT INTO "annotation" VALUES(53,'VVPP','key','VVPP ','Perfect Participle',NULL);
+INSERT INTO "annotation" VALUES(54,'XY','key','XY ','Non-Word',NULL);
+INSERT INTO "annotation" VALUES(55,'mate','foundry',NULL,'Mate',NULL);
+INSERT INTO "annotation" VALUES(56,'l','layer',NULL,'Lemma',NULL);
+INSERT INTO "annotation" VALUES(57,'m','layer',NULL,'Morphology',NULL);
+INSERT INTO "annotation" VALUES(58,'<root-POS>','key','<root-POS>','Root Part of Speech',NULL);
+INSERT INTO "annotation" VALUES(59,'case','key',NULL,'Case',NULL);
+INSERT INTO "annotation" VALUES(60,'degree','key',NULL,'Degree',NULL);
+INSERT INTO "annotation" VALUES(61,'gender','key',NULL,'Gender',NULL);
+INSERT INTO "annotation" VALUES(62,'mood','key',NULL,'Mood',NULL);
+INSERT INTO "annotation" VALUES(63,'number','key',NULL,'Number',NULL);
+INSERT INTO "annotation" VALUES(64,'person','key',NULL,'Person',NULL);
+INSERT INTO "annotation" VALUES(65,'tense','key',NULL,'Tense',NULL);
+INSERT INTO "annotation" VALUES(66,'<no-type> ','key',NULL,'No type',NULL);
+INSERT INTO "annotation" VALUES(67,'acc','value','acc ','Accusative',NULL);
+INSERT INTO "annotation" VALUES(68,'dat','value','dat ','Dative',NULL);
+INSERT INTO "annotation" VALUES(69,'gen','value','gen ','Genitive',NULL);
+INSERT INTO "annotation" VALUES(70,'nom','value','nom ','Nominative',NULL);
+INSERT INTO "annotation" VALUES(71,'*','value','* ','Undefined',NULL);
+INSERT INTO "annotation" VALUES(72,'comp','value','comp ','Comparative',NULL);
+INSERT INTO "annotation" VALUES(73,'pos','value','pos ','Positive',NULL);
+INSERT INTO "annotation" VALUES(74,'sup','value','sup ','Superative',NULL);
+INSERT INTO "annotation" VALUES(75,'fem','value','fem ','Feminium',NULL);
+INSERT INTO "annotation" VALUES(76,'masc','value','masc ','Masculinum',NULL);
+INSERT INTO "annotation" VALUES(77,'neut','value','neut ','Neuter',NULL);
+INSERT INTO "annotation" VALUES(78,'imp','value','imp ','Imperative',NULL);
+INSERT INTO "annotation" VALUES(79,'ind','value','ind ','Indicative',NULL);
+INSERT INTO "annotation" VALUES(80,'subj','value','subj ','Subjunctive',NULL);
+INSERT INTO "annotation" VALUES(81,'pl','value','pl ','Plural',NULL);
+INSERT INTO "annotation" VALUES(82,'sg','value','sg ','Singular',NULL);
+INSERT INTO "annotation" VALUES(83,'1','value','1 ','First Person',NULL);
+INSERT INTO "annotation" VALUES(84,'2','value','2 ','Second Person',NULL);
+INSERT INTO "annotation" VALUES(85,'3','value','3 ','Third Person',NULL);
+INSERT INTO "annotation" VALUES(86,'past','value','past ','Past',NULL);
+INSERT INTO "annotation" VALUES(87,'pres','value','pres ','Present',NULL);
+INSERT INTO "annotation" VALUES(88,'malt','foundry',NULL,'Malt',NULL);
+INSERT INTO "annotation" VALUES(89,'d','layer',NULL,'Dependency',NULL);
+INSERT INTO "annotation" VALUES(90,'-PUNCT-','key','-PUNCT- ','',NULL);
+INSERT INTO "annotation" VALUES(91,'-UNKNOWN-','key','-UNKNOWN- ','',NULL);
+INSERT INTO "annotation" VALUES(92,'APP','key','APP ','',NULL);
+INSERT INTO "annotation" VALUES(93,'ATTR','key','ATTR ','',NULL);
+INSERT INTO "annotation" VALUES(94,'AUX','key','AUX ','',NULL);
+INSERT INTO "annotation" VALUES(95,'AVZ','key','AVZ ','',NULL);
+INSERT INTO "annotation" VALUES(96,'CJ','key','CJ ','',NULL);
+INSERT INTO "annotation" VALUES(97,'DET','key','DET ','',NULL);
+INSERT INTO "annotation" VALUES(98,'EXPL','key','EXPL ','',NULL);
+INSERT INTO "annotation" VALUES(99,'GMOD','key','GMOD ','',NULL);
+INSERT INTO "annotation" VALUES(100,'GRAD','key','GRAD ','',NULL);
+INSERT INTO "annotation" VALUES(101,'KOM','key','KOM ','',NULL);
+INSERT INTO "annotation" VALUES(102,'KONJ','key','KONJ ','',NULL);
+INSERT INTO "annotation" VALUES(103,'NEB','key','NEB ','',NULL);
+INSERT INTO "annotation" VALUES(104,'OBJA','key','OBJA ','',NULL);
+INSERT INTO "annotation" VALUES(105,'OBJC','key','OBJC ','',NULL);
+INSERT INTO "annotation" VALUES(106,'OBJD','key','OBJD ','',NULL);
+INSERT INTO "annotation" VALUES(107,'OBJG','key','OBJG ','',NULL);
+INSERT INTO "annotation" VALUES(108,'OBJI','key','OBJI ','',NULL);
+INSERT INTO "annotation" VALUES(109,'OBJP','key','OBJP ','',NULL);
+INSERT INTO "annotation" VALUES(110,'PAR','key','PAR ','',NULL);
+INSERT INTO "annotation" VALUES(111,'PART','key','PART ','',NULL);
+INSERT INTO "annotation" VALUES(112,'PN','key','PN ','',NULL);
+INSERT INTO "annotation" VALUES(113,'PP','key','PP ','',NULL);
+INSERT INTO "annotation" VALUES(114,'PRED','key','PRED ','',NULL);
+INSERT INTO "annotation" VALUES(115,'REL','key','REL ','',NULL);
+INSERT INTO "annotation" VALUES(116,'ROOT','key','ROOT ','',NULL);
+INSERT INTO "annotation" VALUES(117,'S','key','S ','',NULL);
+INSERT INTO "annotation" VALUES(118,'SUBJ','key','SUBJ ','',NULL);
+INSERT INTO "annotation" VALUES(119,'SUBJC','key','SUBJC ','',NULL);
+INSERT INTO "annotation" VALUES(120,'ZEIT','key','ZEIT ','',NULL);
+INSERT INTO "annotation" VALUES(121,'gmod-app','key','gmod-app ','',NULL);
+INSERT INTO "annotation" VALUES(122,'koord','key','koord ','',NULL);
+INSERT INTO "annotation" VALUES(123,'drukola','foundry',NULL,'DRuKoLa',NULL);
+INSERT INTO "annotation" VALUES(124,'ctag','key',NULL,'CTAG',NULL);
+INSERT INTO "annotation" VALUES(125,'A','value','a ','Adjective',NULL);
+INSERT INTO "annotation" VALUES(126,'Y','value','y ','Abbreviation',NULL);
+INSERT INTO "annotation" VALUES(127,'AN','value','an ','Adjective, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(128,'APRY','value','apry ','Adjective, Plural, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(129,'APN','value','apn ','Adjective, Plural, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(130,'APOY','value','apoy ','Adjective, Plural, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(131,'APON','value','apon ','Adjective, Plural, Oblique, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(132,'ASRY','value','asry ','Adjective, Singular, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(133,'ASN','value','asn ','Adjective, Singular, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(134,'ASOY','value','asoy ','Adjective, Singular, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(135,'ASON','value','ason ','Adjective, Singular, Oblique, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(136,'ASVY','value','asvy ','Adjective, Singular, Vocative, Definite',NULL);
+INSERT INTO "annotation" VALUES(137,'ASVN','value','asvn ','Adjective, Singular, Vocative, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(138,'R','value','r ','Adverb',NULL);
+INSERT INTO "annotation" VALUES(139,'RC','value','rc ','Adverb, Portmanteau',NULL);
+INSERT INTO "annotation" VALUES(140,'TS','value','ts ','Article, Definite or Possessive, Singular',NULL);
+INSERT INTO "annotation" VALUES(141,'TP','value','tp ','Article, Indefinite or Possessive, Plural',NULL);
+INSERT INTO "annotation" VALUES(142,'TPR','value','tpr ','Article, non-Possessive, Plural, Direct',NULL);
+INSERT INTO "annotation" VALUES(143,'TPO','value','tpo ','Article, non-Possessive, Plural, Oblique',NULL);
+INSERT INTO "annotation" VALUES(144,'TSR','value','tsr ','Article, non-Possessive, Singular, Direct',NULL);
+INSERT INTO "annotation" VALUES(145,'TSO','value','tso ','Article, non-Possessive, Singular, Oblique',NULL);
+INSERT INTO "annotation" VALUES(146,'NPRY','value','npry ','Common Noun, Plural, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(147,'NPN','value','npn ','Common Noun, Plural, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(148,'NPOY','value','npoy ','Common Noun, Plural, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(149,'NPVY','value','npvy ','Common Noun, Plural, Vocative, Definite',NULL);
+INSERT INTO "annotation" VALUES(150,'NN','value','nn ','Common Noun, singular',NULL);
+INSERT INTO "annotation" VALUES(151,'NSY','value','nsy ','Common Noun, Singular, Definite',NULL);
+INSERT INTO "annotation" VALUES(152,'NSRY','value','nsry ','Common Noun, Singular, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(153,'NSRN','value','nsrn ','Common Noun, Singular, Direct, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(154,'NSN','value','nsn ','Common Noun, Singular, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(155,'NSOY','value','nsoy ','Common Noun, Singular, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(156,'NSON','value','nson ','Common Noun, Singular, Oblique, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(157,'NSVY','value','nsvy ','Common Noun, Singular, Vocative, Definite',NULL);
+INSERT INTO "annotation" VALUES(158,'NSVN','value','nsvn ','Common Noun, Singular, Vocative, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(159,'CR','value','cr ','Conjunctio, portmanteau',NULL);
+INSERT INTO "annotation" VALUES(160,'C','value','c ','Conjunction',NULL);
+INSERT INTO "annotation" VALUES(161,'QF','value','qf ','Future Particle',NULL);
+INSERT INTO "annotation" VALUES(162,'QN','value','qn ','Infinitival Particle',NULL);
+INSERT INTO "annotation" VALUES(163,'I','value','i ','Interjection',NULL);
+INSERT INTO "annotation" VALUES(164,'QZ','value','qz ','Negative Particle',NULL);
+INSERT INTO "annotation" VALUES(165,'M','value','m ','Numeral',NULL);
+INSERT INTO "annotation" VALUES(166,'PP','value','pp ','Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(167,'PPP','value','ppp ','Personal Pronoun, Plural',NULL);
+INSERT INTO "annotation" VALUES(168,'PPPA','value','pppa ','Personal Pronoun, Plural, Acc.',NULL);
+INSERT INTO "annotation" VALUES(169,'PPPD','value','pppd ','Personal Pronoun, Plural, Dative',NULL);
+INSERT INTO "annotation" VALUES(170,'PPPR','value','pppr ','Personal Pronoun, Plural, Direct',NULL);
+INSERT INTO "annotation" VALUES(171,'PPPO','value','pppo ','Personal Pronoun, Plural, Oblique',NULL);
+INSERT INTO "annotation" VALUES(172,'PPS','value','pps ','Personal Pronoun, Singular',NULL);
+INSERT INTO "annotation" VALUES(173,'PPSA','value','ppsa ','Personal Pronoun, Singular, Accusative',NULL);
+INSERT INTO "annotation" VALUES(174,'PPSD','value','ppsd ','Personal Pronoun, Singular, Dative',NULL);
+INSERT INTO "annotation" VALUES(175,'PPSR','value','ppsr ','Personal Pronoun, Singular, Direct',NULL);
+INSERT INTO "annotation" VALUES(176,'PPSN','value','ppsn ','Personal Pronoun, Singular, Nominative',NULL);
+INSERT INTO "annotation" VALUES(177,'PPSO','value','ppso ','Personal Pronoun, Singular, Oblique',NULL);
+INSERT INTO "annotation" VALUES(178,'S','value','s ','Preposition',NULL);
+INSERT INTO "annotation" VALUES(179,'DMPR','value','dmpr ','Pronoun or Determiner, Demonstrative, Plural, Direct',NULL);
+INSERT INTO "annotation" VALUES(180,'DMPO','value','dmpo ','Pronoun or Determiner, Demonstrative, Plural, Oblique',NULL);
+INSERT INTO "annotation" VALUES(181,'DMSR','value','dmsr ','Pronoun or Determiner, Demonstrative, Singular, Direct',NULL);
+INSERT INTO "annotation" VALUES(182,'DMSO','value','dmso ','Pronoun or Determiner, Demonstrative, Singular, Oblique',NULL);
+INSERT INTO "annotation" VALUES(183,'PS','value','ps ','Pronoun or Determiner, Poss or Emph',NULL);
+INSERT INTO "annotation" VALUES(184,'PSS','value','pss ','Pronoun or Determiner, Poss or Emph, Singular',NULL);
+INSERT INTO "annotation" VALUES(185,'RELR','value','relr ','Pronoun or Determiner, Relative, Direct',NULL);
+INSERT INTO "annotation" VALUES(186,'RELO','value','relo ','Pronoun or Determiner, Relative, Oblique',NULL);
+INSERT INTO "annotation" VALUES(187,'NP','value','np ','Proper Noun',NULL);
+INSERT INTO "annotation" VALUES(188,'PI','value','pi ','Quantifier Pronoun or Determiner',NULL);
+INSERT INTO "annotation" VALUES(189,'PXA','value','pxa ','Reflexive Pronoun, Accusative',NULL);
+INSERT INTO "annotation" VALUES(190,'PXD','value','pxd ','Reflexive Pronoun, Dative',NULL);
+INSERT INTO "annotation" VALUES(191,'X','value','x ','Residual',NULL);
+INSERT INTO "annotation" VALUES(192,'QS','value','qs ','Subjunctive Particle',NULL);
+INSERT INTO "annotation" VALUES(193,'VA','value','va ','Verb, Auxiliary',NULL);
+INSERT INTO "annotation" VALUES(194,'VA1','value','va1 ','Verb, Auxiliary, 1st person',NULL);
+INSERT INTO "annotation" VALUES(195,'VA2P','value','va2p ','Verb, Auxiliary, 2nd person, Plural',NULL);
+INSERT INTO "annotation" VALUES(196,'VA2S','value','va2s ','Verb, Auxiliary, 2nd person, Singular',NULL);
+INSERT INTO "annotation" VALUES(197,'VA3','value','va3 ','Verb, Auxiliary, 3rd person',NULL);
+INSERT INTO "annotation" VALUES(198,'VA3P','value','va3p ','Verb, Auxiliary, 3rd person, Plural',NULL);
+INSERT INTO "annotation" VALUES(199,'VA3S','value','va3s ','Verb, Auxiliary, 3rd person, Singular',NULL);
+INSERT INTO "annotation" VALUES(200,'VA1P','value','va1p ','Verb, Auxiliary,1st person, Plural',NULL);
+INSERT INTO "annotation" VALUES(201,'VA1S','value','va1s ','Verb, Auxiliary,1st person, Singular',NULL);
+INSERT INTO "annotation" VALUES(202,'VG','value','vg ','Verb, Gerund',NULL);
+INSERT INTO "annotation" VALUES(203,'VN','value','vn ','Verb, Infinitive',NULL);
+INSERT INTO "annotation" VALUES(204,'V1','value','v1 ','Verb, Main, 1st person',NULL);
+INSERT INTO "annotation" VALUES(205,'V2','value','v2 ','Verb, Main, 2nd person',NULL);
+INSERT INTO "annotation" VALUES(206,'V3','value','v3 ','Verb, Main, 3rd person',NULL);
+INSERT INTO "annotation" VALUES(207,'VPPF','value','vppf ','Verb, Participle, Plural, Feminine',NULL);
+INSERT INTO "annotation" VALUES(208,'VPPM','value','vppm ','Verb, Participle, Plural, Masculine',NULL);
+INSERT INTO "annotation" VALUES(209,'VPSF','value','vpsf ','Verb, Participle, Singular, Feminine',NULL);
+INSERT INTO "annotation" VALUES(210,'VPSM','value','vpsm ','Verb, Participle, Singular, Masculine',NULL);
+INSERT INTO "annotation" VALUES(211,'cnx','foundry',NULL,'Connexor',NULL);
+INSERT INTO "annotation" VALUES(212,'c','layer',NULL,'Constituency',NULL);
+INSERT INTO "annotation" VALUES(213,'syn','layer',NULL,'Syntax',NULL);
+INSERT INTO "annotation" VALUES(214,'np','key','np ','Nominal Phrase',NULL);
+INSERT INTO "annotation" VALUES(215,'Abbr','key','Abbr ','Nouns: Abbreviation',NULL);
+INSERT INTO "annotation" VALUES(216,'CMP','key','CMP ','Adjective: Comparative',NULL);
+INSERT INTO "annotation" VALUES(217,'IMP','key','IMP ','Mood: Imperative',NULL);
+INSERT INTO "annotation" VALUES(218,'IND','key','IND ','Mood: Indicative',NULL);
+INSERT INTO "annotation" VALUES(219,'INF','key','INF ','Infinitive',NULL);
+INSERT INTO "annotation" VALUES(220,'ORD','key','ORD ','Numeral: Ordinal',NULL);
+INSERT INTO "annotation" VALUES(221,'PAST','key','PAST ','Tense: past',NULL);
+INSERT INTO "annotation" VALUES(222,'PCP','key','PCP ','Participle',NULL);
+INSERT INTO "annotation" VALUES(223,'PERF','key','PERF ','Perfective Participle',NULL);
+INSERT INTO "annotation" VALUES(224,'PL','key','PL ','Nouns: Plural',NULL);
+INSERT INTO "annotation" VALUES(225,'PRES','key','PRES ','Tense: present',NULL);
+INSERT INTO "annotation" VALUES(226,'PROG','key','PROG ','Progressive Participle',NULL);
+INSERT INTO "annotation" VALUES(227,'Prop','key','Prop ','Nouns: Proper Noun',NULL);
+INSERT INTO "annotation" VALUES(228,'SUB','key','SUB ','Mood: Subjunctive',NULL);
+INSERT INTO "annotation" VALUES(229,'SUP','key','SUP ','Adjective: Superlative',NULL);
+INSERT INTO "annotation" VALUES(230,'A','key','A ','Adjective',NULL);
+INSERT INTO "annotation" VALUES(231,'CC','key','CC ','Coordination Marker',NULL);
+INSERT INTO "annotation" VALUES(232,'CS','key','CS ','Clause Marker',NULL);
+INSERT INTO "annotation" VALUES(233,'INTERJ','key','INTERJ ','Interjection',NULL);
+INSERT INTO "annotation" VALUES(234,'N','key','N ','Noun',NULL);
+INSERT INTO "annotation" VALUES(235,'NUM','key','NUM ','Numeral',NULL);
+INSERT INTO "annotation" VALUES(236,'PREP','key','PREP ','Preposition',NULL);
+INSERT INTO "annotation" VALUES(237,'PRON','key','PRON ','Pro-Nominal',NULL);
+INSERT INTO "annotation" VALUES(238,'V','key','V ','Verb',NULL);
+INSERT INTO "annotation" VALUES(239,'@ADVL','key','@ADVL ','Adverbial Head',NULL);
+INSERT INTO "annotation" VALUES(240,'@AUX','key','@AUX ','Auxiliary Verb',NULL);
+INSERT INTO "annotation" VALUES(241,'@CC','key','@CC ','Coordination',NULL);
+INSERT INTO "annotation" VALUES(242,'@MAIN','key','@MAIN ','Main Verb',NULL);
+INSERT INTO "annotation" VALUES(243,'@NH','key','@NH ','Nominal Head',NULL);
+INSERT INTO "annotation" VALUES(244,'@POSTMOD','key','@POSTMOD ','Postmodifier',NULL);
+INSERT INTO "annotation" VALUES(245,'@PREMARK','key','@PREMARK ','Preposed Marker',NULL);
+INSERT INTO "annotation" VALUES(246,'@PREMOD','key','@PREMOD ','Premodifier',NULL);
+INSERT INTO "annotation" VALUES(247,'marmot','foundry',NULL,'MarMoT',NULL);
+INSERT INTO "annotation" VALUES(248,'base','foundry',NULL,'Base Annotation',NULL);
+INSERT INTO "annotation" VALUES(249,'s','layer',NULL,'Structure',NULL);
+INSERT INTO "annotation" VALUES(250,'s','key','s','Sentence',NULL);
+INSERT INTO "annotation" VALUES(251,'p','key','p','Paragraph',NULL);
+INSERT INTO "annotation" VALUES(252,'t','key','t','Text',NULL);
+INSERT INTO "annotation" VALUES(253,'xip','foundry',NULL,'Xerox Parser',NULL);
+INSERT INTO "annotation" VALUES(254,'lwc','foundry',NULL,'LWC',NULL);
+INSERT INTO "annotation" VALUES(255,'AC','key','AC ','adpositional case marker',NULL);
+INSERT INTO "annotation" VALUES(256,'ADC','key','ADC ','adjective component',NULL);
+INSERT INTO "annotation" VALUES(257,'AMS','key','AMS ','measure argument of adj',NULL);
+INSERT INTO "annotation" VALUES(258,'AVC','key','AVC ','adverbial phrase component',NULL);
+INSERT INTO "annotation" VALUES(259,'CD','key','CD ','coordinating conjunction',NULL);
+INSERT INTO "annotation" VALUES(260,'CM','key','CM ','comparative concjunction',NULL);
+INSERT INTO "annotation" VALUES(261,'CP','key','CP ','complementizer',NULL);
+INSERT INTO "annotation" VALUES(262,'DA','key','DA ','dative',NULL);
+INSERT INTO "annotation" VALUES(263,'DH','key','DH ','discourse-level head',NULL);
+INSERT INTO "annotation" VALUES(264,'DM','key','DM ','discourse marker',NULL);
+INSERT INTO "annotation" VALUES(265,'GL','key','GL ','prenominal genitive',NULL);
+INSERT INTO "annotation" VALUES(266,'GR','key','GR ','postnominal genitive',NULL);
+INSERT INTO "annotation" VALUES(267,'HD','key','HD ','head',NULL);
+INSERT INTO "annotation" VALUES(268,'JU','key','JU ','junctor',NULL);
+INSERT INTO "annotation" VALUES(269,'MC','key','MC ','comitative',NULL);
+INSERT INTO "annotation" VALUES(270,'MI','key','MI ','instrumental',NULL);
+INSERT INTO "annotation" VALUES(271,'ML','key','ML ','locative',NULL);
+INSERT INTO "annotation" VALUES(272,'MNR','key','MNR ','postnominal modifier',NULL);
+INSERT INTO "annotation" VALUES(273,'MO','key','MO ','modifier',NULL);
+INSERT INTO "annotation" VALUES(274,'MR','key','MR ','rhetorical modifier',NULL);
+INSERT INTO "annotation" VALUES(275,'MW','key','MW ','way (directional modifier)',NULL);
+INSERT INTO "annotation" VALUES(276,'NG','key','NG ','negation',NULL);
+INSERT INTO "annotation" VALUES(277,'NK','key','NK ','noun kernel modifier',NULL);
+INSERT INTO "annotation" VALUES(278,'NMC','key','NMC ','numerical component',NULL);
+INSERT INTO "annotation" VALUES(279,'OA','key','OA ','accusative object',NULL);
+INSERT INTO "annotation" VALUES(280,'OA2','key','OA2 ','second accusative object',NULL);
+INSERT INTO "annotation" VALUES(281,'OC','key','OC ','clausal object',NULL);
+INSERT INTO "annotation" VALUES(282,'OG','key','OG ','genitive object',NULL);
+INSERT INTO "annotation" VALUES(283,'PD','key','PD ','predicate',NULL);
+INSERT INTO "annotation" VALUES(284,'PG','key','PG ','pseudo-genitive',NULL);
+INSERT INTO "annotation" VALUES(285,'PH','key','PH ','placeholder',NULL);
+INSERT INTO "annotation" VALUES(286,'PM','key','PM ','morphological particle',NULL);
+INSERT INTO "annotation" VALUES(287,'PNC','key','PNC ','proper noun component',NULL);
+INSERT INTO "annotation" VALUES(288,'RC','key','RC ','relative clause',NULL);
+INSERT INTO "annotation" VALUES(289,'RE','key','RE ','repeated element',NULL);
+INSERT INTO "annotation" VALUES(290,'RS','key','RS ','reported speech',NULL);
+INSERT INTO "annotation" VALUES(291,'SB','key','SB ','subject',NULL);
+INSERT INTO "annotation" VALUES(292,'SBP','key','SBP ','passivised subject (PP)',NULL);
+INSERT INTO "annotation" VALUES(293,'SP','key','SP ','subject or predicate',NULL);
+INSERT INTO "annotation" VALUES(294,'SVP','key','SVP ','separable verb prefix',NULL);
+INSERT INTO "annotation" VALUES(295,'UC','key','UC ','(idiosyncratic) unit component',NULL);
+INSERT INTO "annotation" VALUES(296,'VO','key','VO ','vocative',NULL);
+INSERT INTO "annotation" VALUES(297,'dereko','foundry',NULL,'DeReKo',NULL);
+INSERT INTO "annotation" VALUES(298,'sgbr','foundry',NULL,'Schreibgebrauch',NULL);
+INSERT INTO "annotation" VALUES(299,'lv','layer',NULL,'Lemma Variants',NULL);
+INSERT INTO "annotation" VALUES(300,'NNE','key','NNE','Normal Nomina with Named Entity',NULL);
+INSERT INTO "annotation" VALUES(301,'ADVART','key','ADVART','Adverb with Article',NULL);
+INSERT INTO "annotation" VALUES(302,'EMOASC','key','EMOASC','ASCII emoticon',NULL);
+INSERT INTO "annotation" VALUES(303,'EMOIMG','key','EMOIMG','Graphic emoticon',NULL);
+INSERT INTO "annotation" VALUES(304,'ERRTOK','key','ERRTOK','Tokenisation Error',NULL);
+INSERT INTO "annotation" VALUES(305,'HST','key','HST','Hashtag',NULL);
+INSERT INTO "annotation" VALUES(306,'KOUSPPER','key','KOUSPPER','Subordinating Conjunction (with Sentence) with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(307,'ONO','key','ONO','Onomatopoeia',NULL);
+INSERT INTO "annotation" VALUES(308,'PPERPPER','key','PPERPPER','Personal Pronoun with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(309,'URL','key','URL','Uniform Resource Locator',NULL);
+INSERT INTO "annotation" VALUES(310,'VAPPER','key','VAPPER','Finite Auxiliary Verb with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(311,'VMPPER','key','VMPPER','Fintite Modal Verb with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(312,'VVPPER','key','VVPPER','Finite Full Verb with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(313,'AW','key','AW','Interaction Word',NULL);
+INSERT INTO "annotation" VALUES(314,'ADR','key','ADR','Addressing Term',NULL);
+INSERT INTO "annotation" VALUES(315,'AWIND','key','AWIND','Punctuation Indicating Addressing Term',NULL);
+INSERT INTO "annotation" VALUES(316,'ERRAW','key','ERRAW','Part of Erroneously Separated Compound',NULL);
+INSERT INTO "annotation" VALUES(317,'corenlp','foundry',NULL,'CoreNLP',NULL);
+INSERT INTO "annotation" VALUES(318,'ne','layer',NULL,'Named Entity',NULL);
+INSERT INTO "annotation" VALUES(319,'ne_dewac_175m_600','layer',NULL,'Named Entity',NULL);
+INSERT INTO "annotation" VALUES(320,'ne_hgc_175m_600','layer',NULL,'Named Entity',NULL);
+INSERT INTO "annotation" VALUES(321,'I-LOC','key','I-LOC ','Location',NULL);
+INSERT INTO "annotation" VALUES(322,'I-MISC','key','I-MISC ','Miscellaneous',NULL);
+INSERT INTO "annotation" VALUES(323,'I-ORG','key','I-ORG ','Organization',NULL);
+INSERT INTO "annotation" VALUES(324,'I-PER','key','I-PER ','Person',NULL);
+INSERT INTO "annotation" VALUES(325,'AA','key','AA','superlative phrase with ''am''',NULL);
+INSERT INTO "annotation" VALUES(326,'AP','key','AP','adjektive phrase',NULL);
+INSERT INTO "annotation" VALUES(327,'AVP','key','AVP','adverbial phrase',NULL);
+INSERT INTO "annotation" VALUES(328,'CAP','key','CAP','coordinated adjektive phrase',NULL);
+INSERT INTO "annotation" VALUES(329,'CAVP','key','CAVP','coordinated adverbial phrase',NULL);
+INSERT INTO "annotation" VALUES(330,'CAC','key','CAC','coordinated adposition',NULL);
+INSERT INTO "annotation" VALUES(331,'CCP','key','CCP','coordinated complementiser',NULL);
+INSERT INTO "annotation" VALUES(332,'CH','key','CH','chunk',NULL);
+INSERT INTO "annotation" VALUES(333,'CNP','key','CNP','coordinated noun phrase',NULL);
+INSERT INTO "annotation" VALUES(334,'CO','key','CO','coordination',NULL);
+INSERT INTO "annotation" VALUES(335,'CPP','key','CPP','coordinated adpositional phrase',NULL);
+INSERT INTO "annotation" VALUES(336,'CVP','key','CVP','coordinated verb phrase (non-finite)',NULL);
+INSERT INTO "annotation" VALUES(337,'CVZ','key','CVZ','coordinated zu-marked infinitive',NULL);
+INSERT INTO "annotation" VALUES(338,'DL','key','DL','discourse level constituent',NULL);
+INSERT INTO "annotation" VALUES(339,'ISU','key','ISU','idiosyncratis unit',NULL);
+INSERT INTO "annotation" VALUES(340,'MPN','key','MPN','multi-word proper noun',NULL);
+INSERT INTO "annotation" VALUES(341,'MTA','key','MTA','multi-token adjective',NULL);
+INSERT INTO "annotation" VALUES(342,'NM','key','NM','multi-token number',NULL);
+INSERT INTO "annotation" VALUES(343,'NP','key','NP','noun phrase',NULL);
+INSERT INTO "annotation" VALUES(344,'QL','key','QL','quasi-languag',NULL);
+INSERT INTO "annotation" VALUES(345,'VP','key','VP','verb phrase (non-finite)',NULL);
+INSERT INTO "annotation" VALUES(346,'VZ','key','VZ','zu-marked infinitive',NULL);
+INSERT INTO "annotation" VALUES(347,'AC','value','AC ','adpositional case marker',NULL);
+INSERT INTO "annotation" VALUES(348,'ADC','value','ADC ','adjective component',NULL);
+INSERT INTO "annotation" VALUES(349,'AMS','value','AMS ','measure argument of adj',NULL);
+INSERT INTO "annotation" VALUES(350,'APP','value','APP ','apposition',NULL);
+INSERT INTO "annotation" VALUES(351,'AVC','value','AVC ','adverbial phrase component',NULL);
+INSERT INTO "annotation" VALUES(352,'CC','value','CC ','comparative complement',NULL);
+INSERT INTO "annotation" VALUES(353,'CD','value','CD ','coordinating conjunction',NULL);
+INSERT INTO "annotation" VALUES(354,'CJ','value','CJ ','conjunct',NULL);
+INSERT INTO "annotation" VALUES(355,'CM','value','CM ','comparative concjunction',NULL);
+INSERT INTO "annotation" VALUES(356,'CP','value','CP ','complementizer',NULL);
+INSERT INTO "annotation" VALUES(357,'DA','value','DA ','dative',NULL);
+INSERT INTO "annotation" VALUES(358,'DH','value','DH ','discourse-level head',NULL);
+INSERT INTO "annotation" VALUES(359,'DM','value','DM ','discourse marker',NULL);
+INSERT INTO "annotation" VALUES(360,'GL','value','GL ','prenominal genitive',NULL);
+INSERT INTO "annotation" VALUES(361,'GR','value','GR ','postnominal genitive',NULL);
+INSERT INTO "annotation" VALUES(362,'HD','value','HD ','head',NULL);
+INSERT INTO "annotation" VALUES(363,'JU','value','JU ','junctor',NULL);
+INSERT INTO "annotation" VALUES(364,'MC','value','MC ','comitative',NULL);
+INSERT INTO "annotation" VALUES(365,'MI','value','MI ','instrumental',NULL);
+INSERT INTO "annotation" VALUES(366,'ML','value','ML ','locative',NULL);
+INSERT INTO "annotation" VALUES(367,'MNR','value','MNR ','postnominal modifier',NULL);
+INSERT INTO "annotation" VALUES(368,'MO','value','MO ','modifier',NULL);
+INSERT INTO "annotation" VALUES(369,'MR','value','MR ','rhetorical modifier',NULL);
+INSERT INTO "annotation" VALUES(370,'MW','value','MW ','way (directional modifier)',NULL);
+INSERT INTO "annotation" VALUES(371,'NG','value','NG ','negation',NULL);
+INSERT INTO "annotation" VALUES(372,'NK','value','NK ','noun kernel modifier',NULL);
+INSERT INTO "annotation" VALUES(373,'NMC','value','NMC ','numerical component',NULL);
+INSERT INTO "annotation" VALUES(374,'OA','value','OA ','accusative object',NULL);
+INSERT INTO "annotation" VALUES(375,'OA2','value','OA2 ','second accusative object',NULL);
+INSERT INTO "annotation" VALUES(376,'OC','value','OC ','clausal object',NULL);
+INSERT INTO "annotation" VALUES(377,'OG','value','OG ','genitive object',NULL);
+INSERT INTO "annotation" VALUES(378,'PD','value','PD ','predicate',NULL);
+INSERT INTO "annotation" VALUES(379,'PG','value','PG ','pseudo-genitive',NULL);
+INSERT INTO "annotation" VALUES(380,'PH','value','PH ','placeholder',NULL);
+INSERT INTO "annotation" VALUES(381,'PM','value','PM ','morphological particle',NULL);
+INSERT INTO "annotation" VALUES(382,'PNC','value','PNC ','proper noun component',NULL);
+INSERT INTO "annotation" VALUES(383,'RE','value','RE ','repeated element',NULL);
+INSERT INTO "annotation" VALUES(384,'RS','value','RS ','reported speech',NULL);
+INSERT INTO "annotation" VALUES(385,'SB','value','SB ','subject',NULL);
+INSERT INTO "annotation" VALUES(386,'SBP','value','SBP ','passivised subject (PP)',NULL);
+INSERT INTO "annotation" VALUES(387,'SP','value','SP ','subject or predicate',NULL);
+INSERT INTO "annotation" VALUES(388,'SVP','value','SVP ','separable verb prefix',NULL);
+INSERT INTO "annotation" VALUES(389,'UC','value','UC ','(idiosyncratic) unit component',NULL);
+INSERT INTO "annotation" VALUES(390,'VO','value','VO ','vocative',NULL);
+INSERT INTO "annotation" VALUES(391,'tt','foundry',NULL,'TreeTagger',NULL);
diff --git a/src/main/resources/db/lite/V1.2__annotation_layer.sql b/src/main/resources/db/lite/V1.2__annotation_layer.sql
new file mode 100644
index 0000000..03ae9fc
--- /dev/null
+++ b/src/main/resources/db/lite/V1.2__annotation_layer.sql
@@ -0,0 +1,31 @@
+INSERT INTO "annotation_layer" VALUES(1,1,2,'OpenNLP Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(2,55,56,'Mate Lemma');
+INSERT INTO "annotation_layer" VALUES(3,55,57,'Mate Morphology');
+INSERT INTO "annotation_layer" VALUES(4,55,2,'Mate Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(5,88,89,'Malt Dependency');
+INSERT INTO "annotation_layer" VALUES(6,123,56,'DRuKoLa Lemma');
+INSERT INTO "annotation_layer" VALUES(7,123,57,'DRuKoLa Morphology');
+INSERT INTO "annotation_layer" VALUES(8,123,2,'DRuKoLa Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(9,211,212,'Connexor Constituency');
+INSERT INTO "annotation_layer" VALUES(10,211,56,'Connexor Lemma');
+INSERT INTO "annotation_layer" VALUES(11,211,57,'Connexor Morphology');
+INSERT INTO "annotation_layer" VALUES(12,211,2,'Connexor Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(13,211,213,'Connexor Syntax');
+INSERT INTO "annotation_layer" VALUES(14,247,57,'MarMoT Morphology');
+INSERT INTO "annotation_layer" VALUES(15,247,2,'MarMoT Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(16,248,249,'Base Annotation Structure');
+INSERT INTO "annotation_layer" VALUES(17,253,212,'Xerox Parser Constituency');
+INSERT INTO "annotation_layer" VALUES(18,253,56,'Xerox Parser Lemma');
+INSERT INTO "annotation_layer" VALUES(19,253,2,'Xerox Parser Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(20,254,89,'LWC Dependency');
+INSERT INTO "annotation_layer" VALUES(21,297,249,'DeReKo Structure');
+INSERT INTO "annotation_layer" VALUES(22,298,56,'Schreibgebrauch Lemma');
+INSERT INTO "annotation_layer" VALUES(23,298,299,'Schreibgebrauch Lemma Variants');
+INSERT INTO "annotation_layer" VALUES(24,298,2,'Schreibgebrauch Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(25,317,212,'CoreNLP Constituency');
+INSERT INTO "annotation_layer" VALUES(26,317,318,'CoreNLP Named Entity');
+INSERT INTO "annotation_layer" VALUES(27,317,319,'CoreNLP Named Entity');
+INSERT INTO "annotation_layer" VALUES(28,317,320,'CoreNLP Named Entity');
+INSERT INTO "annotation_layer" VALUES(29,317,2,'CoreNLP Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(30,391,56,'TreeTagger Lemma');
+INSERT INTO "annotation_layer" VALUES(31,391,2,'TreeTagger Part-of-Speech');
diff --git a/src/main/resources/db/lite/V1.3__annotation_key.sql b/src/main/resources/db/lite/V1.3__annotation_key.sql
new file mode 100644
index 0000000..f713062
--- /dev/null
+++ b/src/main/resources/db/lite/V1.3__annotation_key.sql
@@ -0,0 +1,503 @@
+INSERT INTO "annotation_key" VALUES(1,1,3);
+INSERT INTO "annotation_key" VALUES(2,1,4);
+INSERT INTO "annotation_key" VALUES(3,1,5);
+INSERT INTO "annotation_key" VALUES(4,1,6);
+INSERT INTO "annotation_key" VALUES(5,1,7);
+INSERT INTO "annotation_key" VALUES(6,1,8);
+INSERT INTO "annotation_key" VALUES(7,1,9);
+INSERT INTO "annotation_key" VALUES(8,1,10);
+INSERT INTO "annotation_key" VALUES(9,1,11);
+INSERT INTO "annotation_key" VALUES(10,1,12);
+INSERT INTO "annotation_key" VALUES(11,1,13);
+INSERT INTO "annotation_key" VALUES(12,1,14);
+INSERT INTO "annotation_key" VALUES(13,1,15);
+INSERT INTO "annotation_key" VALUES(14,1,16);
+INSERT INTO "annotation_key" VALUES(15,1,17);
+INSERT INTO "annotation_key" VALUES(16,1,18);
+INSERT INTO "annotation_key" VALUES(17,1,19);
+INSERT INTO "annotation_key" VALUES(18,1,20);
+INSERT INTO "annotation_key" VALUES(19,1,21);
+INSERT INTO "annotation_key" VALUES(20,1,22);
+INSERT INTO "annotation_key" VALUES(21,1,23);
+INSERT INTO "annotation_key" VALUES(22,1,24);
+INSERT INTO "annotation_key" VALUES(23,1,25);
+INSERT INTO "annotation_key" VALUES(24,1,26);
+INSERT INTO "annotation_key" VALUES(25,1,27);
+INSERT INTO "annotation_key" VALUES(26,1,28);
+INSERT INTO "annotation_key" VALUES(27,1,29);
+INSERT INTO "annotation_key" VALUES(28,1,30);
+INSERT INTO "annotation_key" VALUES(29,1,31);
+INSERT INTO "annotation_key" VALUES(30,1,32);
+INSERT INTO "annotation_key" VALUES(31,1,33);
+INSERT INTO "annotation_key" VALUES(32,1,34);
+INSERT INTO "annotation_key" VALUES(33,1,35);
+INSERT INTO "annotation_key" VALUES(34,1,36);
+INSERT INTO "annotation_key" VALUES(35,1,37);
+INSERT INTO "annotation_key" VALUES(36,1,38);
+INSERT INTO "annotation_key" VALUES(37,1,39);
+INSERT INTO "annotation_key" VALUES(38,1,40);
+INSERT INTO "annotation_key" VALUES(39,1,41);
+INSERT INTO "annotation_key" VALUES(40,1,42);
+INSERT INTO "annotation_key" VALUES(41,1,43);
+INSERT INTO "annotation_key" VALUES(42,1,44);
+INSERT INTO "annotation_key" VALUES(43,1,45);
+INSERT INTO "annotation_key" VALUES(44,1,46);
+INSERT INTO "annotation_key" VALUES(45,1,47);
+INSERT INTO "annotation_key" VALUES(46,1,48);
+INSERT INTO "annotation_key" VALUES(47,1,49);
+INSERT INTO "annotation_key" VALUES(48,1,50);
+INSERT INTO "annotation_key" VALUES(49,1,51);
+INSERT INTO "annotation_key" VALUES(50,1,52);
+INSERT INTO "annotation_key" VALUES(51,1,53);
+INSERT INTO "annotation_key" VALUES(52,1,54);
+INSERT INTO "annotation_key" VALUES(53,4,3);
+INSERT INTO "annotation_key" VALUES(54,4,4);
+INSERT INTO "annotation_key" VALUES(55,4,5);
+INSERT INTO "annotation_key" VALUES(56,4,6);
+INSERT INTO "annotation_key" VALUES(57,4,7);
+INSERT INTO "annotation_key" VALUES(58,4,8);
+INSERT INTO "annotation_key" VALUES(59,4,9);
+INSERT INTO "annotation_key" VALUES(60,4,10);
+INSERT INTO "annotation_key" VALUES(61,4,11);
+INSERT INTO "annotation_key" VALUES(62,4,12);
+INSERT INTO "annotation_key" VALUES(63,4,13);
+INSERT INTO "annotation_key" VALUES(64,4,14);
+INSERT INTO "annotation_key" VALUES(65,4,15);
+INSERT INTO "annotation_key" VALUES(66,4,16);
+INSERT INTO "annotation_key" VALUES(67,4,17);
+INSERT INTO "annotation_key" VALUES(68,4,18);
+INSERT INTO "annotation_key" VALUES(69,4,19);
+INSERT INTO "annotation_key" VALUES(70,4,20);
+INSERT INTO "annotation_key" VALUES(71,4,21);
+INSERT INTO "annotation_key" VALUES(72,4,22);
+INSERT INTO "annotation_key" VALUES(73,4,23);
+INSERT INTO "annotation_key" VALUES(74,4,24);
+INSERT INTO "annotation_key" VALUES(75,4,25);
+INSERT INTO "annotation_key" VALUES(76,4,26);
+INSERT INTO "annotation_key" VALUES(77,4,27);
+INSERT INTO "annotation_key" VALUES(78,4,28);
+INSERT INTO "annotation_key" VALUES(79,4,29);
+INSERT INTO "annotation_key" VALUES(80,4,30);
+INSERT INTO "annotation_key" VALUES(81,4,31);
+INSERT INTO "annotation_key" VALUES(82,4,32);
+INSERT INTO "annotation_key" VALUES(83,4,33);
+INSERT INTO "annotation_key" VALUES(84,4,34);
+INSERT INTO "annotation_key" VALUES(85,4,35);
+INSERT INTO "annotation_key" VALUES(86,4,36);
+INSERT INTO "annotation_key" VALUES(87,4,37);
+INSERT INTO "annotation_key" VALUES(88,4,38);
+INSERT INTO "annotation_key" VALUES(89,4,39);
+INSERT INTO "annotation_key" VALUES(90,4,40);
+INSERT INTO "annotation_key" VALUES(91,4,41);
+INSERT INTO "annotation_key" VALUES(92,4,42);
+INSERT INTO "annotation_key" VALUES(93,4,43);
+INSERT INTO "annotation_key" VALUES(94,4,44);
+INSERT INTO "annotation_key" VALUES(95,4,45);
+INSERT INTO "annotation_key" VALUES(96,4,46);
+INSERT INTO "annotation_key" VALUES(97,4,47);
+INSERT INTO "annotation_key" VALUES(98,4,48);
+INSERT INTO "annotation_key" VALUES(99,4,49);
+INSERT INTO "annotation_key" VALUES(100,4,50);
+INSERT INTO "annotation_key" VALUES(101,4,51);
+INSERT INTO "annotation_key" VALUES(102,4,52);
+INSERT INTO "annotation_key" VALUES(103,4,53);
+INSERT INTO "annotation_key" VALUES(104,4,54);
+INSERT INTO "annotation_key" VALUES(105,4,58);
+INSERT INTO "annotation_key" VALUES(106,3,59);
+INSERT INTO "annotation_key" VALUES(107,3,60);
+INSERT INTO "annotation_key" VALUES(108,3,61);
+INSERT INTO "annotation_key" VALUES(109,3,62);
+INSERT INTO "annotation_key" VALUES(110,3,63);
+INSERT INTO "annotation_key" VALUES(111,3,64);
+INSERT INTO "annotation_key" VALUES(112,3,65);
+INSERT INTO "annotation_key" VALUES(113,3,66);
+INSERT INTO "annotation_key" VALUES(114,5,90);
+INSERT INTO "annotation_key" VALUES(115,5,91);
+INSERT INTO "annotation_key" VALUES(116,5,5);
+INSERT INTO "annotation_key" VALUES(117,5,92);
+INSERT INTO "annotation_key" VALUES(118,5,93);
+INSERT INTO "annotation_key" VALUES(119,5,94);
+INSERT INTO "annotation_key" VALUES(120,5,95);
+INSERT INTO "annotation_key" VALUES(121,5,96);
+INSERT INTO "annotation_key" VALUES(122,5,97);
+INSERT INTO "annotation_key" VALUES(123,5,98);
+INSERT INTO "annotation_key" VALUES(124,5,99);
+INSERT INTO "annotation_key" VALUES(125,5,100);
+INSERT INTO "annotation_key" VALUES(126,5,101);
+INSERT INTO "annotation_key" VALUES(127,5,15);
+INSERT INTO "annotation_key" VALUES(128,5,102);
+INSERT INTO "annotation_key" VALUES(129,5,103);
+INSERT INTO "annotation_key" VALUES(130,5,104);
+INSERT INTO "annotation_key" VALUES(131,5,105);
+INSERT INTO "annotation_key" VALUES(132,5,106);
+INSERT INTO "annotation_key" VALUES(133,5,107);
+INSERT INTO "annotation_key" VALUES(134,5,108);
+INSERT INTO "annotation_key" VALUES(135,5,109);
+INSERT INTO "annotation_key" VALUES(136,5,110);
+INSERT INTO "annotation_key" VALUES(137,5,111);
+INSERT INTO "annotation_key" VALUES(138,5,112);
+INSERT INTO "annotation_key" VALUES(139,5,113);
+INSERT INTO "annotation_key" VALUES(140,5,114);
+INSERT INTO "annotation_key" VALUES(141,5,115);
+INSERT INTO "annotation_key" VALUES(142,5,116);
+INSERT INTO "annotation_key" VALUES(143,5,117);
+INSERT INTO "annotation_key" VALUES(144,5,118);
+INSERT INTO "annotation_key" VALUES(145,5,119);
+INSERT INTO "annotation_key" VALUES(146,5,120);
+INSERT INTO "annotation_key" VALUES(147,5,121);
+INSERT INTO "annotation_key" VALUES(148,5,122);
+INSERT INTO "annotation_key" VALUES(149,7,124);
+INSERT INTO "annotation_key" VALUES(150,9,214);
+INSERT INTO "annotation_key" VALUES(151,11,215);
+INSERT INTO "annotation_key" VALUES(152,11,216);
+INSERT INTO "annotation_key" VALUES(153,11,217);
+INSERT INTO "annotation_key" VALUES(154,11,218);
+INSERT INTO "annotation_key" VALUES(155,11,219);
+INSERT INTO "annotation_key" VALUES(156,11,220);
+INSERT INTO "annotation_key" VALUES(157,11,221);
+INSERT INTO "annotation_key" VALUES(158,11,222);
+INSERT INTO "annotation_key" VALUES(159,11,223);
+INSERT INTO "annotation_key" VALUES(160,11,224);
+INSERT INTO "annotation_key" VALUES(161,11,225);
+INSERT INTO "annotation_key" VALUES(162,11,226);
+INSERT INTO "annotation_key" VALUES(163,11,227);
+INSERT INTO "annotation_key" VALUES(164,11,228);
+INSERT INTO "annotation_key" VALUES(165,11,229);
+INSERT INTO "annotation_key" VALUES(166,12,230);
+INSERT INTO "annotation_key" VALUES(167,12,5);
+INSERT INTO "annotation_key" VALUES(168,12,231);
+INSERT INTO "annotation_key" VALUES(169,12,232);
+INSERT INTO "annotation_key" VALUES(170,12,97);
+INSERT INTO "annotation_key" VALUES(171,12,233);
+INSERT INTO "annotation_key" VALUES(172,12,234);
+INSERT INTO "annotation_key" VALUES(173,12,235);
+INSERT INTO "annotation_key" VALUES(174,12,236);
+INSERT INTO "annotation_key" VALUES(175,12,237);
+INSERT INTO "annotation_key" VALUES(176,12,238);
+INSERT INTO "annotation_key" VALUES(177,13,239);
+INSERT INTO "annotation_key" VALUES(178,13,240);
+INSERT INTO "annotation_key" VALUES(179,13,241);
+INSERT INTO "annotation_key" VALUES(180,13,242);
+INSERT INTO "annotation_key" VALUES(181,13,243);
+INSERT INTO "annotation_key" VALUES(182,13,244);
+INSERT INTO "annotation_key" VALUES(183,13,245);
+INSERT INTO "annotation_key" VALUES(184,13,246);
+INSERT INTO "annotation_key" VALUES(185,15,3);
+INSERT INTO "annotation_key" VALUES(186,15,4);
+INSERT INTO "annotation_key" VALUES(187,15,5);
+INSERT INTO "annotation_key" VALUES(188,15,6);
+INSERT INTO "annotation_key" VALUES(189,15,7);
+INSERT INTO "annotation_key" VALUES(190,15,8);
+INSERT INTO "annotation_key" VALUES(191,15,9);
+INSERT INTO "annotation_key" VALUES(192,15,10);
+INSERT INTO "annotation_key" VALUES(193,15,11);
+INSERT INTO "annotation_key" VALUES(194,15,12);
+INSERT INTO "annotation_key" VALUES(195,15,13);
+INSERT INTO "annotation_key" VALUES(196,15,14);
+INSERT INTO "annotation_key" VALUES(197,15,15);
+INSERT INTO "annotation_key" VALUES(198,15,16);
+INSERT INTO "annotation_key" VALUES(199,15,17);
+INSERT INTO "annotation_key" VALUES(200,15,18);
+INSERT INTO "annotation_key" VALUES(201,15,19);
+INSERT INTO "annotation_key" VALUES(202,15,20);
+INSERT INTO "annotation_key" VALUES(203,15,21);
+INSERT INTO "annotation_key" VALUES(204,15,22);
+INSERT INTO "annotation_key" VALUES(205,15,23);
+INSERT INTO "annotation_key" VALUES(206,15,24);
+INSERT INTO "annotation_key" VALUES(207,15,25);
+INSERT INTO "annotation_key" VALUES(208,15,26);
+INSERT INTO "annotation_key" VALUES(209,15,27);
+INSERT INTO "annotation_key" VALUES(210,15,28);
+INSERT INTO "annotation_key" VALUES(211,15,29);
+INSERT INTO "annotation_key" VALUES(212,15,30);
+INSERT INTO "annotation_key" VALUES(213,15,31);
+INSERT INTO "annotation_key" VALUES(214,15,32);
+INSERT INTO "annotation_key" VALUES(215,15,33);
+INSERT INTO "annotation_key" VALUES(216,15,34);
+INSERT INTO "annotation_key" VALUES(217,15,35);
+INSERT INTO "annotation_key" VALUES(218,15,36);
+INSERT INTO "annotation_key" VALUES(219,15,37);
+INSERT INTO "annotation_key" VALUES(220,15,38);
+INSERT INTO "annotation_key" VALUES(221,15,39);
+INSERT INTO "annotation_key" VALUES(222,15,40);
+INSERT INTO "annotation_key" VALUES(223,15,41);
+INSERT INTO "annotation_key" VALUES(224,15,42);
+INSERT INTO "annotation_key" VALUES(225,15,43);
+INSERT INTO "annotation_key" VALUES(226,15,44);
+INSERT INTO "annotation_key" VALUES(227,15,45);
+INSERT INTO "annotation_key" VALUES(228,15,46);
+INSERT INTO "annotation_key" VALUES(229,15,47);
+INSERT INTO "annotation_key" VALUES(230,15,48);
+INSERT INTO "annotation_key" VALUES(231,15,49);
+INSERT INTO "annotation_key" VALUES(232,15,50);
+INSERT INTO "annotation_key" VALUES(233,15,51);
+INSERT INTO "annotation_key" VALUES(234,15,52);
+INSERT INTO "annotation_key" VALUES(235,15,53);
+INSERT INTO "annotation_key" VALUES(236,15,54);
+INSERT INTO "annotation_key" VALUES(237,14,59);
+INSERT INTO "annotation_key" VALUES(238,14,60);
+INSERT INTO "annotation_key" VALUES(239,14,61);
+INSERT INTO "annotation_key" VALUES(240,14,62);
+INSERT INTO "annotation_key" VALUES(241,14,63);
+INSERT INTO "annotation_key" VALUES(242,14,64);
+INSERT INTO "annotation_key" VALUES(243,14,65);
+INSERT INTO "annotation_key" VALUES(244,14,66);
+INSERT INTO "annotation_key" VALUES(245,16,250);
+INSERT INTO "annotation_key" VALUES(246,16,251);
+INSERT INTO "annotation_key" VALUES(247,16,252);
+INSERT INTO "annotation_key" VALUES(248,20,255);
+INSERT INTO "annotation_key" VALUES(249,20,256);
+INSERT INTO "annotation_key" VALUES(250,20,257);
+INSERT INTO "annotation_key" VALUES(251,20,92);
+INSERT INTO "annotation_key" VALUES(252,20,258);
+INSERT INTO "annotation_key" VALUES(253,20,231);
+INSERT INTO "annotation_key" VALUES(254,20,259);
+INSERT INTO "annotation_key" VALUES(255,20,96);
+INSERT INTO "annotation_key" VALUES(256,20,260);
+INSERT INTO "annotation_key" VALUES(257,20,261);
+INSERT INTO "annotation_key" VALUES(258,20,262);
+INSERT INTO "annotation_key" VALUES(259,20,263);
+INSERT INTO "annotation_key" VALUES(260,20,264);
+INSERT INTO "annotation_key" VALUES(261,20,265);
+INSERT INTO "annotation_key" VALUES(262,20,266);
+INSERT INTO "annotation_key" VALUES(263,20,267);
+INSERT INTO "annotation_key" VALUES(264,20,268);
+INSERT INTO "annotation_key" VALUES(265,20,269);
+INSERT INTO "annotation_key" VALUES(266,20,270);
+INSERT INTO "annotation_key" VALUES(267,20,271);
+INSERT INTO "annotation_key" VALUES(268,20,272);
+INSERT INTO "annotation_key" VALUES(269,20,273);
+INSERT INTO "annotation_key" VALUES(270,20,274);
+INSERT INTO "annotation_key" VALUES(271,20,275);
+INSERT INTO "annotation_key" VALUES(272,20,276);
+INSERT INTO "annotation_key" VALUES(273,20,277);
+INSERT INTO "annotation_key" VALUES(274,20,278);
+INSERT INTO "annotation_key" VALUES(275,20,279);
+INSERT INTO "annotation_key" VALUES(276,20,280);
+INSERT INTO "annotation_key" VALUES(277,20,281);
+INSERT INTO "annotation_key" VALUES(278,20,282);
+INSERT INTO "annotation_key" VALUES(279,20,283);
+INSERT INTO "annotation_key" VALUES(280,20,284);
+INSERT INTO "annotation_key" VALUES(281,20,285);
+INSERT INTO "annotation_key" VALUES(282,20,286);
+INSERT INTO "annotation_key" VALUES(283,20,287);
+INSERT INTO "annotation_key" VALUES(284,20,288);
+INSERT INTO "annotation_key" VALUES(285,20,289);
+INSERT INTO "annotation_key" VALUES(286,20,290);
+INSERT INTO "annotation_key" VALUES(287,20,291);
+INSERT INTO "annotation_key" VALUES(288,20,292);
+INSERT INTO "annotation_key" VALUES(289,20,293);
+INSERT INTO "annotation_key" VALUES(290,20,294);
+INSERT INTO "annotation_key" VALUES(291,20,295);
+INSERT INTO "annotation_key" VALUES(292,20,296);
+INSERT INTO "annotation_key" VALUES(293,24,3);
+INSERT INTO "annotation_key" VALUES(294,24,4);
+INSERT INTO "annotation_key" VALUES(295,24,5);
+INSERT INTO "annotation_key" VALUES(296,24,6);
+INSERT INTO "annotation_key" VALUES(297,24,7);
+INSERT INTO "annotation_key" VALUES(298,24,8);
+INSERT INTO "annotation_key" VALUES(299,24,9);
+INSERT INTO "annotation_key" VALUES(300,24,10);
+INSERT INTO "annotation_key" VALUES(301,24,11);
+INSERT INTO "annotation_key" VALUES(302,24,12);
+INSERT INTO "annotation_key" VALUES(303,24,13);
+INSERT INTO "annotation_key" VALUES(304,24,14);
+INSERT INTO "annotation_key" VALUES(305,24,15);
+INSERT INTO "annotation_key" VALUES(306,24,16);
+INSERT INTO "annotation_key" VALUES(307,24,17);
+INSERT INTO "annotation_key" VALUES(308,24,18);
+INSERT INTO "annotation_key" VALUES(309,24,19);
+INSERT INTO "annotation_key" VALUES(310,24,20);
+INSERT INTO "annotation_key" VALUES(311,24,21);
+INSERT INTO "annotation_key" VALUES(312,24,22);
+INSERT INTO "annotation_key" VALUES(313,24,23);
+INSERT INTO "annotation_key" VALUES(314,24,24);
+INSERT INTO "annotation_key" VALUES(315,24,25);
+INSERT INTO "annotation_key" VALUES(316,24,26);
+INSERT INTO "annotation_key" VALUES(317,24,27);
+INSERT INTO "annotation_key" VALUES(318,24,28);
+INSERT INTO "annotation_key" VALUES(319,24,29);
+INSERT INTO "annotation_key" VALUES(320,24,30);
+INSERT INTO "annotation_key" VALUES(321,24,31);
+INSERT INTO "annotation_key" VALUES(322,24,32);
+INSERT INTO "annotation_key" VALUES(323,24,33);
+INSERT INTO "annotation_key" VALUES(324,24,34);
+INSERT INTO "annotation_key" VALUES(325,24,35);
+INSERT INTO "annotation_key" VALUES(326,24,36);
+INSERT INTO "annotation_key" VALUES(327,24,37);
+INSERT INTO "annotation_key" VALUES(328,24,38);
+INSERT INTO "annotation_key" VALUES(329,24,39);
+INSERT INTO "annotation_key" VALUES(330,24,40);
+INSERT INTO "annotation_key" VALUES(331,24,41);
+INSERT INTO "annotation_key" VALUES(332,24,42);
+INSERT INTO "annotation_key" VALUES(333,24,43);
+INSERT INTO "annotation_key" VALUES(334,24,44);
+INSERT INTO "annotation_key" VALUES(335,24,45);
+INSERT INTO "annotation_key" VALUES(336,24,46);
+INSERT INTO "annotation_key" VALUES(337,24,47);
+INSERT INTO "annotation_key" VALUES(338,24,48);
+INSERT INTO "annotation_key" VALUES(339,24,49);
+INSERT INTO "annotation_key" VALUES(340,24,50);
+INSERT INTO "annotation_key" VALUES(341,24,51);
+INSERT INTO "annotation_key" VALUES(342,24,52);
+INSERT INTO "annotation_key" VALUES(343,24,53);
+INSERT INTO "annotation_key" VALUES(344,24,54);
+INSERT INTO "annotation_key" VALUES(345,24,300);
+INSERT INTO "annotation_key" VALUES(346,24,301);
+INSERT INTO "annotation_key" VALUES(347,24,302);
+INSERT INTO "annotation_key" VALUES(348,24,303);
+INSERT INTO "annotation_key" VALUES(349,24,304);
+INSERT INTO "annotation_key" VALUES(350,24,305);
+INSERT INTO "annotation_key" VALUES(351,24,306);
+INSERT INTO "annotation_key" VALUES(352,24,307);
+INSERT INTO "annotation_key" VALUES(353,24,308);
+INSERT INTO "annotation_key" VALUES(354,24,309);
+INSERT INTO "annotation_key" VALUES(355,24,310);
+INSERT INTO "annotation_key" VALUES(356,24,311);
+INSERT INTO "annotation_key" VALUES(357,24,312);
+INSERT INTO "annotation_key" VALUES(358,24,313);
+INSERT INTO "annotation_key" VALUES(359,24,314);
+INSERT INTO "annotation_key" VALUES(360,24,315);
+INSERT INTO "annotation_key" VALUES(361,24,316);
+INSERT INTO "annotation_key" VALUES(362,26,321);
+INSERT INTO "annotation_key" VALUES(363,26,322);
+INSERT INTO "annotation_key" VALUES(364,26,323);
+INSERT INTO "annotation_key" VALUES(365,26,324);
+INSERT INTO "annotation_key" VALUES(366,27,321);
+INSERT INTO "annotation_key" VALUES(367,27,322);
+INSERT INTO "annotation_key" VALUES(368,27,323);
+INSERT INTO "annotation_key" VALUES(369,27,324);
+INSERT INTO "annotation_key" VALUES(370,28,321);
+INSERT INTO "annotation_key" VALUES(371,28,322);
+INSERT INTO "annotation_key" VALUES(372,28,323);
+INSERT INTO "annotation_key" VALUES(373,28,324);
+INSERT INTO "annotation_key" VALUES(374,29,3);
+INSERT INTO "annotation_key" VALUES(375,29,4);
+INSERT INTO "annotation_key" VALUES(376,29,5);
+INSERT INTO "annotation_key" VALUES(377,29,6);
+INSERT INTO "annotation_key" VALUES(378,29,7);
+INSERT INTO "annotation_key" VALUES(379,29,8);
+INSERT INTO "annotation_key" VALUES(380,29,9);
+INSERT INTO "annotation_key" VALUES(381,29,10);
+INSERT INTO "annotation_key" VALUES(382,29,11);
+INSERT INTO "annotation_key" VALUES(383,29,12);
+INSERT INTO "annotation_key" VALUES(384,29,13);
+INSERT INTO "annotation_key" VALUES(385,29,14);
+INSERT INTO "annotation_key" VALUES(386,29,15);
+INSERT INTO "annotation_key" VALUES(387,29,16);
+INSERT INTO "annotation_key" VALUES(388,29,17);
+INSERT INTO "annotation_key" VALUES(389,29,18);
+INSERT INTO "annotation_key" VALUES(390,29,19);
+INSERT INTO "annotation_key" VALUES(391,29,20);
+INSERT INTO "annotation_key" VALUES(392,29,21);
+INSERT INTO "annotation_key" VALUES(393,29,22);
+INSERT INTO "annotation_key" VALUES(394,29,23);
+INSERT INTO "annotation_key" VALUES(395,29,24);
+INSERT INTO "annotation_key" VALUES(396,29,25);
+INSERT INTO "annotation_key" VALUES(397,29,26);
+INSERT INTO "annotation_key" VALUES(398,29,27);
+INSERT INTO "annotation_key" VALUES(399,29,28);
+INSERT INTO "annotation_key" VALUES(400,29,29);
+INSERT INTO "annotation_key" VALUES(401,29,30);
+INSERT INTO "annotation_key" VALUES(402,29,31);
+INSERT INTO "annotation_key" VALUES(403,29,32);
+INSERT INTO "annotation_key" VALUES(404,29,33);
+INSERT INTO "annotation_key" VALUES(405,29,34);
+INSERT INTO "annotation_key" VALUES(406,29,35);
+INSERT INTO "annotation_key" VALUES(407,29,36);
+INSERT INTO "annotation_key" VALUES(408,29,37);
+INSERT INTO "annotation_key" VALUES(409,29,38);
+INSERT INTO "annotation_key" VALUES(410,29,39);
+INSERT INTO "annotation_key" VALUES(411,29,40);
+INSERT INTO "annotation_key" VALUES(412,29,41);
+INSERT INTO "annotation_key" VALUES(413,29,42);
+INSERT INTO "annotation_key" VALUES(414,29,43);
+INSERT INTO "annotation_key" VALUES(415,29,44);
+INSERT INTO "annotation_key" VALUES(416,29,45);
+INSERT INTO "annotation_key" VALUES(417,29,46);
+INSERT INTO "annotation_key" VALUES(418,29,47);
+INSERT INTO "annotation_key" VALUES(419,29,48);
+INSERT INTO "annotation_key" VALUES(420,29,49);
+INSERT INTO "annotation_key" VALUES(421,29,50);
+INSERT INTO "annotation_key" VALUES(422,29,51);
+INSERT INTO "annotation_key" VALUES(423,29,52);
+INSERT INTO "annotation_key" VALUES(424,29,53);
+INSERT INTO "annotation_key" VALUES(425,29,54);
+INSERT INTO "annotation_key" VALUES(426,25,325);
+INSERT INTO "annotation_key" VALUES(427,25,326);
+INSERT INTO "annotation_key" VALUES(428,25,327);
+INSERT INTO "annotation_key" VALUES(429,25,328);
+INSERT INTO "annotation_key" VALUES(430,25,329);
+INSERT INTO "annotation_key" VALUES(431,25,330);
+INSERT INTO "annotation_key" VALUES(432,25,331);
+INSERT INTO "annotation_key" VALUES(433,25,332);
+INSERT INTO "annotation_key" VALUES(434,25,333);
+INSERT INTO "annotation_key" VALUES(435,25,334);
+INSERT INTO "annotation_key" VALUES(436,25,335);
+INSERT INTO "annotation_key" VALUES(437,25,232);
+INSERT INTO "annotation_key" VALUES(438,25,336);
+INSERT INTO "annotation_key" VALUES(439,25,337);
+INSERT INTO "annotation_key" VALUES(440,25,338);
+INSERT INTO "annotation_key" VALUES(441,25,339);
+INSERT INTO "annotation_key" VALUES(442,25,340);
+INSERT INTO "annotation_key" VALUES(443,25,341);
+INSERT INTO "annotation_key" VALUES(444,25,342);
+INSERT INTO "annotation_key" VALUES(445,25,343);
+INSERT INTO "annotation_key" VALUES(446,25,113);
+INSERT INTO "annotation_key" VALUES(447,25,344);
+INSERT INTO "annotation_key" VALUES(448,25,116);
+INSERT INTO "annotation_key" VALUES(449,25,117);
+INSERT INTO "annotation_key" VALUES(450,25,345);
+INSERT INTO "annotation_key" VALUES(451,25,346);
+INSERT INTO "annotation_key" VALUES(452,31,3);
+INSERT INTO "annotation_key" VALUES(453,31,4);
+INSERT INTO "annotation_key" VALUES(454,31,5);
+INSERT INTO "annotation_key" VALUES(455,31,6);
+INSERT INTO "annotation_key" VALUES(456,31,7);
+INSERT INTO "annotation_key" VALUES(457,31,8);
+INSERT INTO "annotation_key" VALUES(458,31,9);
+INSERT INTO "annotation_key" VALUES(459,31,10);
+INSERT INTO "annotation_key" VALUES(460,31,11);
+INSERT INTO "annotation_key" VALUES(461,31,12);
+INSERT INTO "annotation_key" VALUES(462,31,13);
+INSERT INTO "annotation_key" VALUES(463,31,14);
+INSERT INTO "annotation_key" VALUES(464,31,15);
+INSERT INTO "annotation_key" VALUES(465,31,16);
+INSERT INTO "annotation_key" VALUES(466,31,17);
+INSERT INTO "annotation_key" VALUES(467,31,18);
+INSERT INTO "annotation_key" VALUES(468,31,19);
+INSERT INTO "annotation_key" VALUES(469,31,20);
+INSERT INTO "annotation_key" VALUES(470,31,21);
+INSERT INTO "annotation_key" VALUES(471,31,22);
+INSERT INTO "annotation_key" VALUES(472,31,23);
+INSERT INTO "annotation_key" VALUES(473,31,24);
+INSERT INTO "annotation_key" VALUES(474,31,25);
+INSERT INTO "annotation_key" VALUES(475,31,26);
+INSERT INTO "annotation_key" VALUES(476,31,27);
+INSERT INTO "annotation_key" VALUES(477,31,28);
+INSERT INTO "annotation_key" VALUES(478,31,29);
+INSERT INTO "annotation_key" VALUES(479,31,30);
+INSERT INTO "annotation_key" VALUES(480,31,31);
+INSERT INTO "annotation_key" VALUES(481,31,32);
+INSERT INTO "annotation_key" VALUES(482,31,33);
+INSERT INTO "annotation_key" VALUES(483,31,34);
+INSERT INTO "annotation_key" VALUES(484,31,35);
+INSERT INTO "annotation_key" VALUES(485,31,36);
+INSERT INTO "annotation_key" VALUES(486,31,37);
+INSERT INTO "annotation_key" VALUES(487,31,38);
+INSERT INTO "annotation_key" VALUES(488,31,39);
+INSERT INTO "annotation_key" VALUES(489,31,40);
+INSERT INTO "annotation_key" VALUES(490,31,41);
+INSERT INTO "annotation_key" VALUES(491,31,42);
+INSERT INTO "annotation_key" VALUES(492,31,43);
+INSERT INTO "annotation_key" VALUES(493,31,44);
+INSERT INTO "annotation_key" VALUES(494,31,45);
+INSERT INTO "annotation_key" VALUES(495,31,46);
+INSERT INTO "annotation_key" VALUES(496,31,47);
+INSERT INTO "annotation_key" VALUES(497,31,48);
+INSERT INTO "annotation_key" VALUES(498,31,49);
+INSERT INTO "annotation_key" VALUES(499,31,50);
+INSERT INTO "annotation_key" VALUES(500,31,51);
+INSERT INTO "annotation_key" VALUES(501,31,52);
+INSERT INTO "annotation_key" VALUES(502,31,53);
+INSERT INTO "annotation_key" VALUES(503,31,54);
\ No newline at end of file
diff --git a/src/main/resources/db/lite/V1.4__annotation_value.sql b/src/main/resources/db/lite/V1.4__annotation_value.sql
new file mode 100644
index 0000000..256cb83
--- /dev/null
+++ b/src/main/resources/db/lite/V1.4__annotation_value.sql
@@ -0,0 +1,1302 @@
+INSERT INTO "annotation_value" VALUES(1,106,70);
+INSERT INTO "annotation_value" VALUES(2,106,68);
+INSERT INTO "annotation_value" VALUES(3,106,71);
+INSERT INTO "annotation_value" VALUES(4,106,69);
+INSERT INTO "annotation_value" VALUES(5,106,67);
+INSERT INTO "annotation_value" VALUES(6,107,73);
+INSERT INTO "annotation_value" VALUES(7,107,74);
+INSERT INTO "annotation_value" VALUES(8,107,72);
+INSERT INTO "annotation_value" VALUES(9,108,77);
+INSERT INTO "annotation_value" VALUES(10,108,76);
+INSERT INTO "annotation_value" VALUES(11,108,75);
+INSERT INTO "annotation_value" VALUES(12,108,71);
+INSERT INTO "annotation_value" VALUES(13,109,78);
+INSERT INTO "annotation_value" VALUES(14,109,79);
+INSERT INTO "annotation_value" VALUES(15,109,80);
+INSERT INTO "annotation_value" VALUES(16,110,71);
+INSERT INTO "annotation_value" VALUES(17,110,82);
+INSERT INTO "annotation_value" VALUES(18,110,81);
+INSERT INTO "annotation_value" VALUES(19,111,84);
+INSERT INTO "annotation_value" VALUES(20,111,83);
+INSERT INTO "annotation_value" VALUES(21,111,85);
+INSERT INTO "annotation_value" VALUES(22,112,87);
+INSERT INTO "annotation_value" VALUES(23,112,86);
+INSERT INTO "annotation_value" VALUES(24,149,197);
+INSERT INTO "annotation_value" VALUES(25,149,179);
+INSERT INTO "annotation_value" VALUES(26,149,181);
+INSERT INTO "annotation_value" VALUES(27,149,206);
+INSERT INTO "annotation_value" VALUES(28,149,190);
+INSERT INTO "annotation_value" VALUES(29,149,146);
+INSERT INTO "annotation_value" VALUES(30,149,134);
+INSERT INTO "annotation_value" VALUES(31,149,180);
+INSERT INTO "annotation_value" VALUES(32,149,130);
+INSERT INTO "annotation_value" VALUES(33,149,148);
+INSERT INTO "annotation_value" VALUES(34,149,126);
+INSERT INTO "annotation_value" VALUES(35,149,184);
+INSERT INTO "annotation_value" VALUES(36,149,159);
+INSERT INTO "annotation_value" VALUES(37,149,153);
+INSERT INTO "annotation_value" VALUES(38,149,149);
+INSERT INTO "annotation_value" VALUES(39,149,141);
+INSERT INTO "annotation_value" VALUES(40,149,132);
+INSERT INTO "annotation_value" VALUES(41,149,133);
+INSERT INTO "annotation_value" VALUES(42,149,173);
+INSERT INTO "annotation_value" VALUES(43,149,207);
+INSERT INTO "annotation_value" VALUES(44,149,185);
+INSERT INTO "annotation_value" VALUES(45,149,140);
+INSERT INTO "annotation_value" VALUES(46,149,193);
+INSERT INTO "annotation_value" VALUES(47,149,189);
+INSERT INTO "annotation_value" VALUES(48,149,142);
+INSERT INTO "annotation_value" VALUES(49,149,161);
+INSERT INTO "annotation_value" VALUES(50,149,175);
+INSERT INTO "annotation_value" VALUES(51,149,170);
+INSERT INTO "annotation_value" VALUES(52,149,137);
+INSERT INTO "annotation_value" VALUES(53,149,160);
+INSERT INTO "annotation_value" VALUES(54,149,195);
+INSERT INTO "annotation_value" VALUES(55,149,187);
+INSERT INTO "annotation_value" VALUES(56,149,177);
+INSERT INTO "annotation_value" VALUES(57,149,194);
+INSERT INTO "annotation_value" VALUES(58,149,171);
+INSERT INTO "annotation_value" VALUES(59,149,183);
+INSERT INTO "annotation_value" VALUES(60,149,152);
+INSERT INTO "annotation_value" VALUES(61,149,154);
+INSERT INTO "annotation_value" VALUES(62,149,157);
+INSERT INTO "annotation_value" VALUES(63,149,156);
+INSERT INTO "annotation_value" VALUES(64,149,151);
+INSERT INTO "annotation_value" VALUES(65,149,209);
+INSERT INTO "annotation_value" VALUES(66,149,208);
+INSERT INTO "annotation_value" VALUES(67,149,176);
+INSERT INTO "annotation_value" VALUES(68,149,198);
+INSERT INTO "annotation_value" VALUES(69,149,155);
+INSERT INTO "annotation_value" VALUES(70,149,204);
+INSERT INTO "annotation_value" VALUES(71,149,174);
+INSERT INTO "annotation_value" VALUES(72,149,210);
+INSERT INTO "annotation_value" VALUES(73,149,196);
+INSERT INTO "annotation_value" VALUES(74,149,188);
+INSERT INTO "annotation_value" VALUES(75,149,158);
+INSERT INTO "annotation_value" VALUES(76,149,135);
+INSERT INTO "annotation_value" VALUES(77,149,172);
+INSERT INTO "annotation_value" VALUES(78,149,200);
+INSERT INTO "annotation_value" VALUES(79,149,164);
+INSERT INTO "annotation_value" VALUES(80,149,186);
+INSERT INTO "annotation_value" VALUES(81,149,191);
+INSERT INTO "annotation_value" VALUES(82,149,125);
+INSERT INTO "annotation_value" VALUES(83,149,165);
+INSERT INTO "annotation_value" VALUES(84,149,145);
+INSERT INTO "annotation_value" VALUES(85,149,182);
+INSERT INTO "annotation_value" VALUES(86,149,192);
+INSERT INTO "annotation_value" VALUES(87,149,162);
+INSERT INTO "annotation_value" VALUES(88,149,168);
+INSERT INTO "annotation_value" VALUES(89,149,129);
+INSERT INTO "annotation_value" VALUES(90,149,201);
+INSERT INTO "annotation_value" VALUES(91,149,203);
+INSERT INTO "annotation_value" VALUES(92,149,128);
+INSERT INTO "annotation_value" VALUES(93,149,166);
+INSERT INTO "annotation_value" VALUES(94,149,139);
+INSERT INTO "annotation_value" VALUES(95,149,147);
+INSERT INTO "annotation_value" VALUES(96,149,150);
+INSERT INTO "annotation_value" VALUES(97,149,178);
+INSERT INTO "annotation_value" VALUES(98,149,143);
+INSERT INTO "annotation_value" VALUES(99,149,205);
+INSERT INTO "annotation_value" VALUES(100,149,127);
+INSERT INTO "annotation_value" VALUES(101,149,144);
+INSERT INTO "annotation_value" VALUES(102,149,169);
+INSERT INTO "annotation_value" VALUES(103,149,163);
+INSERT INTO "annotation_value" VALUES(104,149,202);
+INSERT INTO "annotation_value" VALUES(105,149,167);
+INSERT INTO "annotation_value" VALUES(106,149,131);
+INSERT INTO "annotation_value" VALUES(107,149,199);
+INSERT INTO "annotation_value" VALUES(108,149,138);
+INSERT INTO "annotation_value" VALUES(109,149,136);
+INSERT INTO "annotation_value" VALUES(110,237,68);
+INSERT INTO "annotation_value" VALUES(111,237,70);
+INSERT INTO "annotation_value" VALUES(112,237,71);
+INSERT INTO "annotation_value" VALUES(113,237,67);
+INSERT INTO "annotation_value" VALUES(114,237,69);
+INSERT INTO "annotation_value" VALUES(115,238,73);
+INSERT INTO "annotation_value" VALUES(116,238,72);
+INSERT INTO "annotation_value" VALUES(117,238,74);
+INSERT INTO "annotation_value" VALUES(118,239,77);
+INSERT INTO "annotation_value" VALUES(119,239,75);
+INSERT INTO "annotation_value" VALUES(120,239,76);
+INSERT INTO "annotation_value" VALUES(121,239,71);
+INSERT INTO "annotation_value" VALUES(122,240,78);
+INSERT INTO "annotation_value" VALUES(123,240,79);
+INSERT INTO "annotation_value" VALUES(124,240,80);
+INSERT INTO "annotation_value" VALUES(125,241,81);
+INSERT INTO "annotation_value" VALUES(126,241,82);
+INSERT INTO "annotation_value" VALUES(127,241,71);
+INSERT INTO "annotation_value" VALUES(128,242,84);
+INSERT INTO "annotation_value" VALUES(129,242,83);
+INSERT INTO "annotation_value" VALUES(130,242,85);
+INSERT INTO "annotation_value" VALUES(131,243,87);
+INSERT INTO "annotation_value" VALUES(132,243,86);
+INSERT INTO "annotation_value" VALUES(133,426,385);
+INSERT INTO "annotation_value" VALUES(134,426,379);
+INSERT INTO "annotation_value" VALUES(135,426,381);
+INSERT INTO "annotation_value" VALUES(136,426,370);
+INSERT INTO "annotation_value" VALUES(137,426,364);
+INSERT INTO "annotation_value" VALUES(138,426,365);
+INSERT INTO "annotation_value" VALUES(139,426,377);
+INSERT INTO "annotation_value" VALUES(140,426,373);
+INSERT INTO "annotation_value" VALUES(141,426,351);
+INSERT INTO "annotation_value" VALUES(142,426,355);
+INSERT INTO "annotation_value" VALUES(143,426,374);
+INSERT INTO "annotation_value" VALUES(144,426,380);
+INSERT INTO "annotation_value" VALUES(145,426,359);
+INSERT INTO "annotation_value" VALUES(146,426,349);
+INSERT INTO "annotation_value" VALUES(147,426,363);
+INSERT INTO "annotation_value" VALUES(148,426,378);
+INSERT INTO "annotation_value" VALUES(149,426,368);
+INSERT INTO "annotation_value" VALUES(150,426,347);
+INSERT INTO "annotation_value" VALUES(151,426,350);
+INSERT INTO "annotation_value" VALUES(152,426,356);
+INSERT INTO "annotation_value" VALUES(153,426,375);
+INSERT INTO "annotation_value" VALUES(154,426,386);
+INSERT INTO "annotation_value" VALUES(155,426,358);
+INSERT INTO "annotation_value" VALUES(156,426,348);
+INSERT INTO "annotation_value" VALUES(157,426,389);
+INSERT INTO "annotation_value" VALUES(158,426,352);
+INSERT INTO "annotation_value" VALUES(159,426,388);
+INSERT INTO "annotation_value" VALUES(160,426,372);
+INSERT INTO "annotation_value" VALUES(161,426,360);
+INSERT INTO "annotation_value" VALUES(162,426,376);
+INSERT INTO "annotation_value" VALUES(163,426,366);
+INSERT INTO "annotation_value" VALUES(164,426,382);
+INSERT INTO "annotation_value" VALUES(165,426,357);
+INSERT INTO "annotation_value" VALUES(166,426,384);
+INSERT INTO "annotation_value" VALUES(167,426,354);
+INSERT INTO "annotation_value" VALUES(168,426,383);
+INSERT INTO "annotation_value" VALUES(169,426,361);
+INSERT INTO "annotation_value" VALUES(170,426,390);
+INSERT INTO "annotation_value" VALUES(171,426,362);
+INSERT INTO "annotation_value" VALUES(172,426,367);
+INSERT INTO "annotation_value" VALUES(173,426,369);
+INSERT INTO "annotation_value" VALUES(174,426,371);
+INSERT INTO "annotation_value" VALUES(175,426,139);
+INSERT INTO "annotation_value" VALUES(176,426,353);
+INSERT INTO "annotation_value" VALUES(177,426,387);
+INSERT INTO "annotation_value" VALUES(178,427,376);
+INSERT INTO "annotation_value" VALUES(179,427,369);
+INSERT INTO "annotation_value" VALUES(180,427,347);
+INSERT INTO "annotation_value" VALUES(181,427,371);
+INSERT INTO "annotation_value" VALUES(182,427,373);
+INSERT INTO "annotation_value" VALUES(183,427,360);
+INSERT INTO "annotation_value" VALUES(184,427,389);
+INSERT INTO "annotation_value" VALUES(185,427,386);
+INSERT INTO "annotation_value" VALUES(186,427,384);
+INSERT INTO "annotation_value" VALUES(187,427,361);
+INSERT INTO "annotation_value" VALUES(188,427,365);
+INSERT INTO "annotation_value" VALUES(189,427,356);
+INSERT INTO "annotation_value" VALUES(190,427,351);
+INSERT INTO "annotation_value" VALUES(191,427,355);
+INSERT INTO "annotation_value" VALUES(192,427,390);
+INSERT INTO "annotation_value" VALUES(193,427,350);
+INSERT INTO "annotation_value" VALUES(194,427,370);
+INSERT INTO "annotation_value" VALUES(195,427,388);
+INSERT INTO "annotation_value" VALUES(196,427,377);
+INSERT INTO "annotation_value" VALUES(197,427,139);
+INSERT INTO "annotation_value" VALUES(198,427,387);
+INSERT INTO "annotation_value" VALUES(199,427,381);
+INSERT INTO "annotation_value" VALUES(200,427,364);
+INSERT INTO "annotation_value" VALUES(201,427,354);
+INSERT INTO "annotation_value" VALUES(202,427,375);
+INSERT INTO "annotation_value" VALUES(203,427,382);
+INSERT INTO "annotation_value" VALUES(204,427,362);
+INSERT INTO "annotation_value" VALUES(205,427,359);
+INSERT INTO "annotation_value" VALUES(206,427,358);
+INSERT INTO "annotation_value" VALUES(207,427,366);
+INSERT INTO "annotation_value" VALUES(208,427,357);
+INSERT INTO "annotation_value" VALUES(209,427,368);
+INSERT INTO "annotation_value" VALUES(210,427,353);
+INSERT INTO "annotation_value" VALUES(211,427,372);
+INSERT INTO "annotation_value" VALUES(212,427,349);
+INSERT INTO "annotation_value" VALUES(213,427,378);
+INSERT INTO "annotation_value" VALUES(214,427,383);
+INSERT INTO "annotation_value" VALUES(215,427,348);
+INSERT INTO "annotation_value" VALUES(216,427,367);
+INSERT INTO "annotation_value" VALUES(217,427,379);
+INSERT INTO "annotation_value" VALUES(218,427,374);
+INSERT INTO "annotation_value" VALUES(219,427,385);
+INSERT INTO "annotation_value" VALUES(220,427,380);
+INSERT INTO "annotation_value" VALUES(221,427,363);
+INSERT INTO "annotation_value" VALUES(222,427,352);
+INSERT INTO "annotation_value" VALUES(223,428,390);
+INSERT INTO "annotation_value" VALUES(224,428,379);
+INSERT INTO "annotation_value" VALUES(225,428,348);
+INSERT INTO "annotation_value" VALUES(226,428,362);
+INSERT INTO "annotation_value" VALUES(227,428,384);
+INSERT INTO "annotation_value" VALUES(228,428,369);
+INSERT INTO "annotation_value" VALUES(229,428,353);
+INSERT INTO "annotation_value" VALUES(230,428,367);
+INSERT INTO "annotation_value" VALUES(231,428,360);
+INSERT INTO "annotation_value" VALUES(232,428,382);
+INSERT INTO "annotation_value" VALUES(233,428,356);
+INSERT INTO "annotation_value" VALUES(234,428,378);
+INSERT INTO "annotation_value" VALUES(235,428,357);
+INSERT INTO "annotation_value" VALUES(236,428,352);
+INSERT INTO "annotation_value" VALUES(237,428,381);
+INSERT INTO "annotation_value" VALUES(238,428,370);
+INSERT INTO "annotation_value" VALUES(239,428,373);
+INSERT INTO "annotation_value" VALUES(240,428,349);
+INSERT INTO "annotation_value" VALUES(241,428,372);
+INSERT INTO "annotation_value" VALUES(242,428,376);
+INSERT INTO "annotation_value" VALUES(243,428,366);
+INSERT INTO "annotation_value" VALUES(244,428,368);
+INSERT INTO "annotation_value" VALUES(245,428,139);
+INSERT INTO "annotation_value" VALUES(246,428,347);
+INSERT INTO "annotation_value" VALUES(247,428,389);
+INSERT INTO "annotation_value" VALUES(248,428,363);
+INSERT INTO "annotation_value" VALUES(249,428,383);
+INSERT INTO "annotation_value" VALUES(250,428,365);
+INSERT INTO "annotation_value" VALUES(251,428,364);
+INSERT INTO "annotation_value" VALUES(252,428,371);
+INSERT INTO "annotation_value" VALUES(253,428,359);
+INSERT INTO "annotation_value" VALUES(254,428,355);
+INSERT INTO "annotation_value" VALUES(255,428,351);
+INSERT INTO "annotation_value" VALUES(256,428,387);
+INSERT INTO "annotation_value" VALUES(257,428,358);
+INSERT INTO "annotation_value" VALUES(258,428,361);
+INSERT INTO "annotation_value" VALUES(259,428,385);
+INSERT INTO "annotation_value" VALUES(260,428,374);
+INSERT INTO "annotation_value" VALUES(261,428,350);
+INSERT INTO "annotation_value" VALUES(262,428,377);
+INSERT INTO "annotation_value" VALUES(263,428,354);
+INSERT INTO "annotation_value" VALUES(264,428,388);
+INSERT INTO "annotation_value" VALUES(265,428,380);
+INSERT INTO "annotation_value" VALUES(266,428,386);
+INSERT INTO "annotation_value" VALUES(267,428,375);
+INSERT INTO "annotation_value" VALUES(268,429,382);
+INSERT INTO "annotation_value" VALUES(269,429,383);
+INSERT INTO "annotation_value" VALUES(270,429,374);
+INSERT INTO "annotation_value" VALUES(271,429,364);
+INSERT INTO "annotation_value" VALUES(272,429,380);
+INSERT INTO "annotation_value" VALUES(273,429,365);
+INSERT INTO "annotation_value" VALUES(274,429,377);
+INSERT INTO "annotation_value" VALUES(275,429,357);
+INSERT INTO "annotation_value" VALUES(276,429,371);
+INSERT INTO "annotation_value" VALUES(277,429,362);
+INSERT INTO "annotation_value" VALUES(278,429,376);
+INSERT INTO "annotation_value" VALUES(279,429,381);
+INSERT INTO "annotation_value" VALUES(280,429,355);
+INSERT INTO "annotation_value" VALUES(281,429,385);
+INSERT INTO "annotation_value" VALUES(282,429,378);
+INSERT INTO "annotation_value" VALUES(283,429,384);
+INSERT INTO "annotation_value" VALUES(284,429,361);
+INSERT INTO "annotation_value" VALUES(285,429,388);
+INSERT INTO "annotation_value" VALUES(286,429,379);
+INSERT INTO "annotation_value" VALUES(287,429,369);
+INSERT INTO "annotation_value" VALUES(288,429,356);
+INSERT INTO "annotation_value" VALUES(289,429,351);
+INSERT INTO "annotation_value" VALUES(290,429,358);
+INSERT INTO "annotation_value" VALUES(291,429,373);
+INSERT INTO "annotation_value" VALUES(292,429,354);
+INSERT INTO "annotation_value" VALUES(293,429,389);
+INSERT INTO "annotation_value" VALUES(294,429,372);
+INSERT INTO "annotation_value" VALUES(295,429,386);
+INSERT INTO "annotation_value" VALUES(296,429,390);
+INSERT INTO "annotation_value" VALUES(297,429,375);
+INSERT INTO "annotation_value" VALUES(298,429,353);
+INSERT INTO "annotation_value" VALUES(299,429,139);
+INSERT INTO "annotation_value" VALUES(300,429,366);
+INSERT INTO "annotation_value" VALUES(301,429,349);
+INSERT INTO "annotation_value" VALUES(302,429,368);
+INSERT INTO "annotation_value" VALUES(303,429,348);
+INSERT INTO "annotation_value" VALUES(304,429,363);
+INSERT INTO "annotation_value" VALUES(305,429,360);
+INSERT INTO "annotation_value" VALUES(306,429,367);
+INSERT INTO "annotation_value" VALUES(307,429,352);
+INSERT INTO "annotation_value" VALUES(308,429,359);
+INSERT INTO "annotation_value" VALUES(309,429,370);
+INSERT INTO "annotation_value" VALUES(310,429,387);
+INSERT INTO "annotation_value" VALUES(311,429,350);
+INSERT INTO "annotation_value" VALUES(312,429,347);
+INSERT INTO "annotation_value" VALUES(313,430,374);
+INSERT INTO "annotation_value" VALUES(314,430,377);
+INSERT INTO "annotation_value" VALUES(315,430,356);
+INSERT INTO "annotation_value" VALUES(316,430,372);
+INSERT INTO "annotation_value" VALUES(317,430,371);
+INSERT INTO "annotation_value" VALUES(318,430,383);
+INSERT INTO "annotation_value" VALUES(319,430,382);
+INSERT INTO "annotation_value" VALUES(320,430,388);
+INSERT INTO "annotation_value" VALUES(321,430,354);
+INSERT INTO "annotation_value" VALUES(322,430,386);
+INSERT INTO "annotation_value" VALUES(323,430,380);
+INSERT INTO "annotation_value" VALUES(324,430,360);
+INSERT INTO "annotation_value" VALUES(325,430,358);
+INSERT INTO "annotation_value" VALUES(326,430,359);
+INSERT INTO "annotation_value" VALUES(327,430,381);
+INSERT INTO "annotation_value" VALUES(328,430,375);
+INSERT INTO "annotation_value" VALUES(329,430,363);
+INSERT INTO "annotation_value" VALUES(330,430,139);
+INSERT INTO "annotation_value" VALUES(331,430,350);
+INSERT INTO "annotation_value" VALUES(332,430,347);
+INSERT INTO "annotation_value" VALUES(333,430,373);
+INSERT INTO "annotation_value" VALUES(334,430,365);
+INSERT INTO "annotation_value" VALUES(335,430,355);
+INSERT INTO "annotation_value" VALUES(336,430,364);
+INSERT INTO "annotation_value" VALUES(337,430,370);
+INSERT INTO "annotation_value" VALUES(338,430,369);
+INSERT INTO "annotation_value" VALUES(339,430,366);
+INSERT INTO "annotation_value" VALUES(340,430,367);
+INSERT INTO "annotation_value" VALUES(341,430,376);
+INSERT INTO "annotation_value" VALUES(342,430,384);
+INSERT INTO "annotation_value" VALUES(343,430,352);
+INSERT INTO "annotation_value" VALUES(344,430,389);
+INSERT INTO "annotation_value" VALUES(345,430,361);
+INSERT INTO "annotation_value" VALUES(346,430,387);
+INSERT INTO "annotation_value" VALUES(347,430,351);
+INSERT INTO "annotation_value" VALUES(348,430,385);
+INSERT INTO "annotation_value" VALUES(349,430,390);
+INSERT INTO "annotation_value" VALUES(350,430,349);
+INSERT INTO "annotation_value" VALUES(351,430,378);
+INSERT INTO "annotation_value" VALUES(352,430,348);
+INSERT INTO "annotation_value" VALUES(353,430,368);
+INSERT INTO "annotation_value" VALUES(354,430,379);
+INSERT INTO "annotation_value" VALUES(355,430,362);
+INSERT INTO "annotation_value" VALUES(356,430,357);
+INSERT INTO "annotation_value" VALUES(357,430,353);
+INSERT INTO "annotation_value" VALUES(358,431,363);
+INSERT INTO "annotation_value" VALUES(359,431,385);
+INSERT INTO "annotation_value" VALUES(360,431,368);
+INSERT INTO "annotation_value" VALUES(361,431,387);
+INSERT INTO "annotation_value" VALUES(362,431,375);
+INSERT INTO "annotation_value" VALUES(363,431,367);
+INSERT INTO "annotation_value" VALUES(364,431,356);
+INSERT INTO "annotation_value" VALUES(365,431,390);
+INSERT INTO "annotation_value" VALUES(366,431,388);
+INSERT INTO "annotation_value" VALUES(367,431,359);
+INSERT INTO "annotation_value" VALUES(368,431,371);
+INSERT INTO "annotation_value" VALUES(369,431,373);
+INSERT INTO "annotation_value" VALUES(370,431,386);
+INSERT INTO "annotation_value" VALUES(371,431,357);
+INSERT INTO "annotation_value" VALUES(372,431,377);
+INSERT INTO "annotation_value" VALUES(373,431,362);
+INSERT INTO "annotation_value" VALUES(374,431,381);
+INSERT INTO "annotation_value" VALUES(375,431,382);
+INSERT INTO "annotation_value" VALUES(376,431,350);
+INSERT INTO "annotation_value" VALUES(377,431,376);
+INSERT INTO "annotation_value" VALUES(378,431,139);
+INSERT INTO "annotation_value" VALUES(379,431,358);
+INSERT INTO "annotation_value" VALUES(380,431,379);
+INSERT INTO "annotation_value" VALUES(381,431,384);
+INSERT INTO "annotation_value" VALUES(382,431,380);
+INSERT INTO "annotation_value" VALUES(383,431,360);
+INSERT INTO "annotation_value" VALUES(384,431,366);
+INSERT INTO "annotation_value" VALUES(385,431,378);
+INSERT INTO "annotation_value" VALUES(386,431,361);
+INSERT INTO "annotation_value" VALUES(387,431,353);
+INSERT INTO "annotation_value" VALUES(388,431,374);
+INSERT INTO "annotation_value" VALUES(389,431,352);
+INSERT INTO "annotation_value" VALUES(390,431,347);
+INSERT INTO "annotation_value" VALUES(391,431,389);
+INSERT INTO "annotation_value" VALUES(392,431,351);
+INSERT INTO "annotation_value" VALUES(393,431,349);
+INSERT INTO "annotation_value" VALUES(394,431,370);
+INSERT INTO "annotation_value" VALUES(395,431,369);
+INSERT INTO "annotation_value" VALUES(396,431,372);
+INSERT INTO "annotation_value" VALUES(397,431,354);
+INSERT INTO "annotation_value" VALUES(398,431,365);
+INSERT INTO "annotation_value" VALUES(399,431,348);
+INSERT INTO "annotation_value" VALUES(400,431,355);
+INSERT INTO "annotation_value" VALUES(401,431,364);
+INSERT INTO "annotation_value" VALUES(402,431,383);
+INSERT INTO "annotation_value" VALUES(403,432,388);
+INSERT INTO "annotation_value" VALUES(404,432,387);
+INSERT INTO "annotation_value" VALUES(405,432,359);
+INSERT INTO "annotation_value" VALUES(406,432,382);
+INSERT INTO "annotation_value" VALUES(407,432,348);
+INSERT INTO "annotation_value" VALUES(408,432,369);
+INSERT INTO "annotation_value" VALUES(409,432,350);
+INSERT INTO "annotation_value" VALUES(410,432,372);
+INSERT INTO "annotation_value" VALUES(411,432,375);
+INSERT INTO "annotation_value" VALUES(412,432,363);
+INSERT INTO "annotation_value" VALUES(413,432,139);
+INSERT INTO "annotation_value" VALUES(414,432,384);
+INSERT INTO "annotation_value" VALUES(415,432,358);
+INSERT INTO "annotation_value" VALUES(416,432,381);
+INSERT INTO "annotation_value" VALUES(417,432,390);
+INSERT INTO "annotation_value" VALUES(418,432,360);
+INSERT INTO "annotation_value" VALUES(419,432,377);
+INSERT INTO "annotation_value" VALUES(420,432,379);
+INSERT INTO "annotation_value" VALUES(421,432,364);
+INSERT INTO "annotation_value" VALUES(422,432,385);
+INSERT INTO "annotation_value" VALUES(423,432,371);
+INSERT INTO "annotation_value" VALUES(424,432,353);
+INSERT INTO "annotation_value" VALUES(425,432,365);
+INSERT INTO "annotation_value" VALUES(426,432,376);
+INSERT INTO "annotation_value" VALUES(427,432,352);
+INSERT INTO "annotation_value" VALUES(428,432,370);
+INSERT INTO "annotation_value" VALUES(429,432,347);
+INSERT INTO "annotation_value" VALUES(430,432,373);
+INSERT INTO "annotation_value" VALUES(431,432,357);
+INSERT INTO "annotation_value" VALUES(432,432,356);
+INSERT INTO "annotation_value" VALUES(433,432,366);
+INSERT INTO "annotation_value" VALUES(434,432,389);
+INSERT INTO "annotation_value" VALUES(435,432,349);
+INSERT INTO "annotation_value" VALUES(436,432,383);
+INSERT INTO "annotation_value" VALUES(437,432,355);
+INSERT INTO "annotation_value" VALUES(438,432,378);
+INSERT INTO "annotation_value" VALUES(439,432,351);
+INSERT INTO "annotation_value" VALUES(440,432,386);
+INSERT INTO "annotation_value" VALUES(441,432,362);
+INSERT INTO "annotation_value" VALUES(442,432,380);
+INSERT INTO "annotation_value" VALUES(443,432,374);
+INSERT INTO "annotation_value" VALUES(444,432,354);
+INSERT INTO "annotation_value" VALUES(445,432,361);
+INSERT INTO "annotation_value" VALUES(446,432,368);
+INSERT INTO "annotation_value" VALUES(447,432,367);
+INSERT INTO "annotation_value" VALUES(448,433,372);
+INSERT INTO "annotation_value" VALUES(449,433,370);
+INSERT INTO "annotation_value" VALUES(450,433,382);
+INSERT INTO "annotation_value" VALUES(451,433,377);
+INSERT INTO "annotation_value" VALUES(452,433,368);
+INSERT INTO "annotation_value" VALUES(453,433,386);
+INSERT INTO "annotation_value" VALUES(454,433,355);
+INSERT INTO "annotation_value" VALUES(455,433,347);
+INSERT INTO "annotation_value" VALUES(456,433,349);
+INSERT INTO "annotation_value" VALUES(457,433,363);
+INSERT INTO "annotation_value" VALUES(458,433,139);
+INSERT INTO "annotation_value" VALUES(459,433,348);
+INSERT INTO "annotation_value" VALUES(460,433,359);
+INSERT INTO "annotation_value" VALUES(461,433,378);
+INSERT INTO "annotation_value" VALUES(462,433,371);
+INSERT INTO "annotation_value" VALUES(463,433,376);
+INSERT INTO "annotation_value" VALUES(464,433,373);
+INSERT INTO "annotation_value" VALUES(465,433,384);
+INSERT INTO "annotation_value" VALUES(466,433,356);
+INSERT INTO "annotation_value" VALUES(467,433,387);
+INSERT INTO "annotation_value" VALUES(468,433,362);
+INSERT INTO "annotation_value" VALUES(469,433,390);
+INSERT INTO "annotation_value" VALUES(470,433,380);
+INSERT INTO "annotation_value" VALUES(471,433,351);
+INSERT INTO "annotation_value" VALUES(472,433,374);
+INSERT INTO "annotation_value" VALUES(473,433,350);
+INSERT INTO "annotation_value" VALUES(474,433,366);
+INSERT INTO "annotation_value" VALUES(475,433,381);
+INSERT INTO "annotation_value" VALUES(476,433,385);
+INSERT INTO "annotation_value" VALUES(477,433,375);
+INSERT INTO "annotation_value" VALUES(478,433,352);
+INSERT INTO "annotation_value" VALUES(479,433,364);
+INSERT INTO "annotation_value" VALUES(480,433,365);
+INSERT INTO "annotation_value" VALUES(481,433,389);
+INSERT INTO "annotation_value" VALUES(482,433,360);
+INSERT INTO "annotation_value" VALUES(483,433,388);
+INSERT INTO "annotation_value" VALUES(484,433,357);
+INSERT INTO "annotation_value" VALUES(485,433,379);
+INSERT INTO "annotation_value" VALUES(486,433,358);
+INSERT INTO "annotation_value" VALUES(487,433,367);
+INSERT INTO "annotation_value" VALUES(488,433,354);
+INSERT INTO "annotation_value" VALUES(489,433,353);
+INSERT INTO "annotation_value" VALUES(490,433,369);
+INSERT INTO "annotation_value" VALUES(491,433,361);
+INSERT INTO "annotation_value" VALUES(492,433,383);
+INSERT INTO "annotation_value" VALUES(493,434,382);
+INSERT INTO "annotation_value" VALUES(494,434,356);
+INSERT INTO "annotation_value" VALUES(495,434,380);
+INSERT INTO "annotation_value" VALUES(496,434,352);
+INSERT INTO "annotation_value" VALUES(497,434,354);
+INSERT INTO "annotation_value" VALUES(498,434,361);
+INSERT INTO "annotation_value" VALUES(499,434,384);
+INSERT INTO "annotation_value" VALUES(500,434,367);
+INSERT INTO "annotation_value" VALUES(501,434,377);
+INSERT INTO "annotation_value" VALUES(502,434,365);
+INSERT INTO "annotation_value" VALUES(503,434,370);
+INSERT INTO "annotation_value" VALUES(504,434,357);
+INSERT INTO "annotation_value" VALUES(505,434,351);
+INSERT INTO "annotation_value" VALUES(506,434,376);
+INSERT INTO "annotation_value" VALUES(507,434,348);
+INSERT INTO "annotation_value" VALUES(508,434,387);
+INSERT INTO "annotation_value" VALUES(509,434,366);
+INSERT INTO "annotation_value" VALUES(510,434,375);
+INSERT INTO "annotation_value" VALUES(511,434,360);
+INSERT INTO "annotation_value" VALUES(512,434,358);
+INSERT INTO "annotation_value" VALUES(513,434,381);
+INSERT INTO "annotation_value" VALUES(514,434,363);
+INSERT INTO "annotation_value" VALUES(515,434,139);
+INSERT INTO "annotation_value" VALUES(516,434,373);
+INSERT INTO "annotation_value" VALUES(517,434,372);
+INSERT INTO "annotation_value" VALUES(518,434,386);
+INSERT INTO "annotation_value" VALUES(519,434,362);
+INSERT INTO "annotation_value" VALUES(520,434,371);
+INSERT INTO "annotation_value" VALUES(521,434,350);
+INSERT INTO "annotation_value" VALUES(522,434,379);
+INSERT INTO "annotation_value" VALUES(523,434,355);
+INSERT INTO "annotation_value" VALUES(524,434,347);
+INSERT INTO "annotation_value" VALUES(525,434,385);
+INSERT INTO "annotation_value" VALUES(526,434,349);
+INSERT INTO "annotation_value" VALUES(527,434,389);
+INSERT INTO "annotation_value" VALUES(528,434,374);
+INSERT INTO "annotation_value" VALUES(529,434,364);
+INSERT INTO "annotation_value" VALUES(530,434,378);
+INSERT INTO "annotation_value" VALUES(531,434,359);
+INSERT INTO "annotation_value" VALUES(532,434,368);
+INSERT INTO "annotation_value" VALUES(533,434,383);
+INSERT INTO "annotation_value" VALUES(534,434,388);
+INSERT INTO "annotation_value" VALUES(535,434,390);
+INSERT INTO "annotation_value" VALUES(536,434,369);
+INSERT INTO "annotation_value" VALUES(537,434,353);
+INSERT INTO "annotation_value" VALUES(538,435,374);
+INSERT INTO "annotation_value" VALUES(539,435,368);
+INSERT INTO "annotation_value" VALUES(540,435,383);
+INSERT INTO "annotation_value" VALUES(541,435,350);
+INSERT INTO "annotation_value" VALUES(542,435,379);
+INSERT INTO "annotation_value" VALUES(543,435,387);
+INSERT INTO "annotation_value" VALUES(544,435,381);
+INSERT INTO "annotation_value" VALUES(545,435,351);
+INSERT INTO "annotation_value" VALUES(546,435,389);
+INSERT INTO "annotation_value" VALUES(547,435,366);
+INSERT INTO "annotation_value" VALUES(548,435,388);
+INSERT INTO "annotation_value" VALUES(549,435,376);
+INSERT INTO "annotation_value" VALUES(550,435,361);
+INSERT INTO "annotation_value" VALUES(551,435,380);
+INSERT INTO "annotation_value" VALUES(552,435,364);
+INSERT INTO "annotation_value" VALUES(553,435,354);
+INSERT INTO "annotation_value" VALUES(554,435,355);
+INSERT INTO "annotation_value" VALUES(555,435,359);
+INSERT INTO "annotation_value" VALUES(556,435,370);
+INSERT INTO "annotation_value" VALUES(557,435,352);
+INSERT INTO "annotation_value" VALUES(558,435,385);
+INSERT INTO "annotation_value" VALUES(559,435,367);
+INSERT INTO "annotation_value" VALUES(560,435,386);
+INSERT INTO "annotation_value" VALUES(561,435,390);
+INSERT INTO "annotation_value" VALUES(562,435,363);
+INSERT INTO "annotation_value" VALUES(563,435,349);
+INSERT INTO "annotation_value" VALUES(564,435,378);
+INSERT INTO "annotation_value" VALUES(565,435,357);
+INSERT INTO "annotation_value" VALUES(566,435,139);
+INSERT INTO "annotation_value" VALUES(567,435,373);
+INSERT INTO "annotation_value" VALUES(568,435,365);
+INSERT INTO "annotation_value" VALUES(569,435,353);
+INSERT INTO "annotation_value" VALUES(570,435,362);
+INSERT INTO "annotation_value" VALUES(571,435,382);
+INSERT INTO "annotation_value" VALUES(572,435,377);
+INSERT INTO "annotation_value" VALUES(573,435,371);
+INSERT INTO "annotation_value" VALUES(574,435,372);
+INSERT INTO "annotation_value" VALUES(575,435,358);
+INSERT INTO "annotation_value" VALUES(576,435,384);
+INSERT INTO "annotation_value" VALUES(577,435,356);
+INSERT INTO "annotation_value" VALUES(578,435,369);
+INSERT INTO "annotation_value" VALUES(579,435,347);
+INSERT INTO "annotation_value" VALUES(580,435,348);
+INSERT INTO "annotation_value" VALUES(581,435,375);
+INSERT INTO "annotation_value" VALUES(582,435,360);
+INSERT INTO "annotation_value" VALUES(583,436,359);
+INSERT INTO "annotation_value" VALUES(584,436,358);
+INSERT INTO "annotation_value" VALUES(585,436,347);
+INSERT INTO "annotation_value" VALUES(586,436,368);
+INSERT INTO "annotation_value" VALUES(587,436,352);
+INSERT INTO "annotation_value" VALUES(588,436,387);
+INSERT INTO "annotation_value" VALUES(589,436,383);
+INSERT INTO "annotation_value" VALUES(590,436,390);
+INSERT INTO "annotation_value" VALUES(591,436,366);
+INSERT INTO "annotation_value" VALUES(592,436,362);
+INSERT INTO "annotation_value" VALUES(593,436,350);
+INSERT INTO "annotation_value" VALUES(594,436,375);
+INSERT INTO "annotation_value" VALUES(595,436,389);
+INSERT INTO "annotation_value" VALUES(596,436,357);
+INSERT INTO "annotation_value" VALUES(597,436,373);
+INSERT INTO "annotation_value" VALUES(598,436,385);
+INSERT INTO "annotation_value" VALUES(599,436,354);
+INSERT INTO "annotation_value" VALUES(600,436,364);
+INSERT INTO "annotation_value" VALUES(601,436,382);
+INSERT INTO "annotation_value" VALUES(602,436,349);
+INSERT INTO "annotation_value" VALUES(603,436,372);
+INSERT INTO "annotation_value" VALUES(604,436,351);
+INSERT INTO "annotation_value" VALUES(605,436,378);
+INSERT INTO "annotation_value" VALUES(606,436,348);
+INSERT INTO "annotation_value" VALUES(607,436,371);
+INSERT INTO "annotation_value" VALUES(608,436,369);
+INSERT INTO "annotation_value" VALUES(609,436,388);
+INSERT INTO "annotation_value" VALUES(610,436,360);
+INSERT INTO "annotation_value" VALUES(611,436,376);
+INSERT INTO "annotation_value" VALUES(612,436,356);
+INSERT INTO "annotation_value" VALUES(613,436,384);
+INSERT INTO "annotation_value" VALUES(614,436,386);
+INSERT INTO "annotation_value" VALUES(615,436,379);
+INSERT INTO "annotation_value" VALUES(616,436,377);
+INSERT INTO "annotation_value" VALUES(617,436,367);
+INSERT INTO "annotation_value" VALUES(618,436,353);
+INSERT INTO "annotation_value" VALUES(619,436,370);
+INSERT INTO "annotation_value" VALUES(620,436,139);
+INSERT INTO "annotation_value" VALUES(621,436,374);
+INSERT INTO "annotation_value" VALUES(622,436,363);
+INSERT INTO "annotation_value" VALUES(623,436,380);
+INSERT INTO "annotation_value" VALUES(624,436,381);
+INSERT INTO "annotation_value" VALUES(625,436,355);
+INSERT INTO "annotation_value" VALUES(626,436,361);
+INSERT INTO "annotation_value" VALUES(627,436,365);
+INSERT INTO "annotation_value" VALUES(628,437,365);
+INSERT INTO "annotation_value" VALUES(629,437,347);
+INSERT INTO "annotation_value" VALUES(630,437,388);
+INSERT INTO "annotation_value" VALUES(631,437,357);
+INSERT INTO "annotation_value" VALUES(632,437,373);
+INSERT INTO "annotation_value" VALUES(633,437,386);
+INSERT INTO "annotation_value" VALUES(634,437,367);
+INSERT INTO "annotation_value" VALUES(635,437,366);
+INSERT INTO "annotation_value" VALUES(636,437,383);
+INSERT INTO "annotation_value" VALUES(637,437,368);
+INSERT INTO "annotation_value" VALUES(638,437,377);
+INSERT INTO "annotation_value" VALUES(639,437,376);
+INSERT INTO "annotation_value" VALUES(640,437,374);
+INSERT INTO "annotation_value" VALUES(641,437,358);
+INSERT INTO "annotation_value" VALUES(642,437,375);
+INSERT INTO "annotation_value" VALUES(643,437,349);
+INSERT INTO "annotation_value" VALUES(644,437,379);
+INSERT INTO "annotation_value" VALUES(645,437,381);
+INSERT INTO "annotation_value" VALUES(646,437,359);
+INSERT INTO "annotation_value" VALUES(647,437,139);
+INSERT INTO "annotation_value" VALUES(648,437,352);
+INSERT INTO "annotation_value" VALUES(649,437,387);
+INSERT INTO "annotation_value" VALUES(650,437,369);
+INSERT INTO "annotation_value" VALUES(651,437,348);
+INSERT INTO "annotation_value" VALUES(652,437,361);
+INSERT INTO "annotation_value" VALUES(653,437,363);
+INSERT INTO "annotation_value" VALUES(654,437,362);
+INSERT INTO "annotation_value" VALUES(655,437,380);
+INSERT INTO "annotation_value" VALUES(656,437,382);
+INSERT INTO "annotation_value" VALUES(657,437,353);
+INSERT INTO "annotation_value" VALUES(658,437,372);
+INSERT INTO "annotation_value" VALUES(659,437,356);
+INSERT INTO "annotation_value" VALUES(660,437,350);
+INSERT INTO "annotation_value" VALUES(661,437,378);
+INSERT INTO "annotation_value" VALUES(662,437,354);
+INSERT INTO "annotation_value" VALUES(663,437,370);
+INSERT INTO "annotation_value" VALUES(664,437,371);
+INSERT INTO "annotation_value" VALUES(665,437,384);
+INSERT INTO "annotation_value" VALUES(666,437,390);
+INSERT INTO "annotation_value" VALUES(667,437,385);
+INSERT INTO "annotation_value" VALUES(668,437,351);
+INSERT INTO "annotation_value" VALUES(669,437,389);
+INSERT INTO "annotation_value" VALUES(670,437,360);
+INSERT INTO "annotation_value" VALUES(671,437,355);
+INSERT INTO "annotation_value" VALUES(672,437,364);
+INSERT INTO "annotation_value" VALUES(673,438,347);
+INSERT INTO "annotation_value" VALUES(674,438,383);
+INSERT INTO "annotation_value" VALUES(675,438,364);
+INSERT INTO "annotation_value" VALUES(676,438,359);
+INSERT INTO "annotation_value" VALUES(677,438,380);
+INSERT INTO "annotation_value" VALUES(678,438,378);
+INSERT INTO "annotation_value" VALUES(679,438,356);
+INSERT INTO "annotation_value" VALUES(680,438,355);
+INSERT INTO "annotation_value" VALUES(681,438,376);
+INSERT INTO "annotation_value" VALUES(682,438,139);
+INSERT INTO "annotation_value" VALUES(683,438,379);
+INSERT INTO "annotation_value" VALUES(684,438,362);
+INSERT INTO "annotation_value" VALUES(685,438,352);
+INSERT INTO "annotation_value" VALUES(686,438,374);
+INSERT INTO "annotation_value" VALUES(687,438,371);
+INSERT INTO "annotation_value" VALUES(688,438,367);
+INSERT INTO "annotation_value" VALUES(689,438,386);
+INSERT INTO "annotation_value" VALUES(690,438,389);
+INSERT INTO "annotation_value" VALUES(691,438,361);
+INSERT INTO "annotation_value" VALUES(692,438,385);
+INSERT INTO "annotation_value" VALUES(693,438,373);
+INSERT INTO "annotation_value" VALUES(694,438,375);
+INSERT INTO "annotation_value" VALUES(695,438,387);
+INSERT INTO "annotation_value" VALUES(696,438,363);
+INSERT INTO "annotation_value" VALUES(697,438,354);
+INSERT INTO "annotation_value" VALUES(698,438,370);
+INSERT INTO "annotation_value" VALUES(699,438,390);
+INSERT INTO "annotation_value" VALUES(700,438,358);
+INSERT INTO "annotation_value" VALUES(701,438,366);
+INSERT INTO "annotation_value" VALUES(702,438,372);
+INSERT INTO "annotation_value" VALUES(703,438,351);
+INSERT INTO "annotation_value" VALUES(704,438,381);
+INSERT INTO "annotation_value" VALUES(705,438,357);
+INSERT INTO "annotation_value" VALUES(706,438,368);
+INSERT INTO "annotation_value" VALUES(707,438,377);
+INSERT INTO "annotation_value" VALUES(708,438,388);
+INSERT INTO "annotation_value" VALUES(709,438,353);
+INSERT INTO "annotation_value" VALUES(710,438,360);
+INSERT INTO "annotation_value" VALUES(711,438,349);
+INSERT INTO "annotation_value" VALUES(712,438,348);
+INSERT INTO "annotation_value" VALUES(713,438,350);
+INSERT INTO "annotation_value" VALUES(714,438,384);
+INSERT INTO "annotation_value" VALUES(715,438,365);
+INSERT INTO "annotation_value" VALUES(716,438,382);
+INSERT INTO "annotation_value" VALUES(717,438,369);
+INSERT INTO "annotation_value" VALUES(718,439,389);
+INSERT INTO "annotation_value" VALUES(719,439,378);
+INSERT INTO "annotation_value" VALUES(720,439,347);
+INSERT INTO "annotation_value" VALUES(721,439,373);
+INSERT INTO "annotation_value" VALUES(722,439,366);
+INSERT INTO "annotation_value" VALUES(723,439,384);
+INSERT INTO "annotation_value" VALUES(724,439,349);
+INSERT INTO "annotation_value" VALUES(725,439,361);
+INSERT INTO "annotation_value" VALUES(726,439,360);
+INSERT INTO "annotation_value" VALUES(727,439,370);
+INSERT INTO "annotation_value" VALUES(728,439,351);
+INSERT INTO "annotation_value" VALUES(729,439,376);
+INSERT INTO "annotation_value" VALUES(730,439,372);
+INSERT INTO "annotation_value" VALUES(731,439,358);
+INSERT INTO "annotation_value" VALUES(732,439,353);
+INSERT INTO "annotation_value" VALUES(733,439,363);
+INSERT INTO "annotation_value" VALUES(734,439,365);
+INSERT INTO "annotation_value" VALUES(735,439,364);
+INSERT INTO "annotation_value" VALUES(736,439,350);
+INSERT INTO "annotation_value" VALUES(737,439,382);
+INSERT INTO "annotation_value" VALUES(738,439,359);
+INSERT INTO "annotation_value" VALUES(739,439,355);
+INSERT INTO "annotation_value" VALUES(740,439,381);
+INSERT INTO "annotation_value" VALUES(741,439,386);
+INSERT INTO "annotation_value" VALUES(742,439,375);
+INSERT INTO "annotation_value" VALUES(743,439,354);
+INSERT INTO "annotation_value" VALUES(744,439,369);
+INSERT INTO "annotation_value" VALUES(745,439,367);
+INSERT INTO "annotation_value" VALUES(746,439,357);
+INSERT INTO "annotation_value" VALUES(747,439,348);
+INSERT INTO "annotation_value" VALUES(748,439,362);
+INSERT INTO "annotation_value" VALUES(749,439,352);
+INSERT INTO "annotation_value" VALUES(750,439,368);
+INSERT INTO "annotation_value" VALUES(751,439,374);
+INSERT INTO "annotation_value" VALUES(752,439,383);
+INSERT INTO "annotation_value" VALUES(753,439,388);
+INSERT INTO "annotation_value" VALUES(754,439,387);
+INSERT INTO "annotation_value" VALUES(755,439,380);
+INSERT INTO "annotation_value" VALUES(756,439,139);
+INSERT INTO "annotation_value" VALUES(757,439,390);
+INSERT INTO "annotation_value" VALUES(758,439,371);
+INSERT INTO "annotation_value" VALUES(759,439,385);
+INSERT INTO "annotation_value" VALUES(760,439,379);
+INSERT INTO "annotation_value" VALUES(761,439,377);
+INSERT INTO "annotation_value" VALUES(762,439,356);
+INSERT INTO "annotation_value" VALUES(763,440,377);
+INSERT INTO "annotation_value" VALUES(764,440,365);
+INSERT INTO "annotation_value" VALUES(765,440,350);
+INSERT INTO "annotation_value" VALUES(766,440,363);
+INSERT INTO "annotation_value" VALUES(767,440,382);
+INSERT INTO "annotation_value" VALUES(768,440,356);
+INSERT INTO "annotation_value" VALUES(769,440,379);
+INSERT INTO "annotation_value" VALUES(770,440,347);
+INSERT INTO "annotation_value" VALUES(771,440,376);
+INSERT INTO "annotation_value" VALUES(772,440,364);
+INSERT INTO "annotation_value" VALUES(773,440,372);
+INSERT INTO "annotation_value" VALUES(774,440,374);
+INSERT INTO "annotation_value" VALUES(775,440,367);
+INSERT INTO "annotation_value" VALUES(776,440,370);
+INSERT INTO "annotation_value" VALUES(777,440,373);
+INSERT INTO "annotation_value" VALUES(778,440,387);
+INSERT INTO "annotation_value" VALUES(779,440,355);
+INSERT INTO "annotation_value" VALUES(780,440,383);
+INSERT INTO "annotation_value" VALUES(781,440,368);
+INSERT INTO "annotation_value" VALUES(782,440,390);
+INSERT INTO "annotation_value" VALUES(783,440,371);
+INSERT INTO "annotation_value" VALUES(784,440,380);
+INSERT INTO "annotation_value" VALUES(785,440,352);
+INSERT INTO "annotation_value" VALUES(786,440,369);
+INSERT INTO "annotation_value" VALUES(787,440,362);
+INSERT INTO "annotation_value" VALUES(788,440,349);
+INSERT INTO "annotation_value" VALUES(789,440,139);
+INSERT INTO "annotation_value" VALUES(790,440,378);
+INSERT INTO "annotation_value" VALUES(791,440,354);
+INSERT INTO "annotation_value" VALUES(792,440,385);
+INSERT INTO "annotation_value" VALUES(793,440,351);
+INSERT INTO "annotation_value" VALUES(794,440,375);
+INSERT INTO "annotation_value" VALUES(795,440,388);
+INSERT INTO "annotation_value" VALUES(796,440,381);
+INSERT INTO "annotation_value" VALUES(797,440,389);
+INSERT INTO "annotation_value" VALUES(798,440,361);
+INSERT INTO "annotation_value" VALUES(799,440,348);
+INSERT INTO "annotation_value" VALUES(800,440,357);
+INSERT INTO "annotation_value" VALUES(801,440,353);
+INSERT INTO "annotation_value" VALUES(802,440,359);
+INSERT INTO "annotation_value" VALUES(803,440,386);
+INSERT INTO "annotation_value" VALUES(804,440,366);
+INSERT INTO "annotation_value" VALUES(805,440,358);
+INSERT INTO "annotation_value" VALUES(806,440,384);
+INSERT INTO "annotation_value" VALUES(807,440,360);
+INSERT INTO "annotation_value" VALUES(808,441,383);
+INSERT INTO "annotation_value" VALUES(809,441,355);
+INSERT INTO "annotation_value" VALUES(810,441,374);
+INSERT INTO "annotation_value" VALUES(811,441,350);
+INSERT INTO "annotation_value" VALUES(812,441,364);
+INSERT INTO "annotation_value" VALUES(813,441,363);
+INSERT INTO "annotation_value" VALUES(814,441,361);
+INSERT INTO "annotation_value" VALUES(815,441,376);
+INSERT INTO "annotation_value" VALUES(816,441,386);
+INSERT INTO "annotation_value" VALUES(817,441,387);
+INSERT INTO "annotation_value" VALUES(818,441,385);
+INSERT INTO "annotation_value" VALUES(819,441,369);
+INSERT INTO "annotation_value" VALUES(820,441,358);
+INSERT INTO "annotation_value" VALUES(821,441,381);
+INSERT INTO "annotation_value" VALUES(822,441,360);
+INSERT INTO "annotation_value" VALUES(823,441,348);
+INSERT INTO "annotation_value" VALUES(824,441,388);
+INSERT INTO "annotation_value" VALUES(825,441,377);
+INSERT INTO "annotation_value" VALUES(826,441,373);
+INSERT INTO "annotation_value" VALUES(827,441,382);
+INSERT INTO "annotation_value" VALUES(828,441,389);
+INSERT INTO "annotation_value" VALUES(829,441,379);
+INSERT INTO "annotation_value" VALUES(830,441,384);
+INSERT INTO "annotation_value" VALUES(831,441,371);
+INSERT INTO "annotation_value" VALUES(832,441,356);
+INSERT INTO "annotation_value" VALUES(833,441,359);
+INSERT INTO "annotation_value" VALUES(834,441,353);
+INSERT INTO "annotation_value" VALUES(835,441,372);
+INSERT INTO "annotation_value" VALUES(836,441,366);
+INSERT INTO "annotation_value" VALUES(837,441,349);
+INSERT INTO "annotation_value" VALUES(838,441,390);
+INSERT INTO "annotation_value" VALUES(839,441,139);
+INSERT INTO "annotation_value" VALUES(840,441,351);
+INSERT INTO "annotation_value" VALUES(841,441,367);
+INSERT INTO "annotation_value" VALUES(842,441,370);
+INSERT INTO "annotation_value" VALUES(843,441,380);
+INSERT INTO "annotation_value" VALUES(844,441,357);
+INSERT INTO "annotation_value" VALUES(845,441,352);
+INSERT INTO "annotation_value" VALUES(846,441,347);
+INSERT INTO "annotation_value" VALUES(847,441,354);
+INSERT INTO "annotation_value" VALUES(848,441,365);
+INSERT INTO "annotation_value" VALUES(849,441,368);
+INSERT INTO "annotation_value" VALUES(850,441,378);
+INSERT INTO "annotation_value" VALUES(851,441,375);
+INSERT INTO "annotation_value" VALUES(852,441,362);
+INSERT INTO "annotation_value" VALUES(853,442,353);
+INSERT INTO "annotation_value" VALUES(854,442,389);
+INSERT INTO "annotation_value" VALUES(855,442,378);
+INSERT INTO "annotation_value" VALUES(856,442,382);
+INSERT INTO "annotation_value" VALUES(857,442,380);
+INSERT INTO "annotation_value" VALUES(858,442,381);
+INSERT INTO "annotation_value" VALUES(859,442,355);
+INSERT INTO "annotation_value" VALUES(860,442,384);
+INSERT INTO "annotation_value" VALUES(861,442,139);
+INSERT INTO "annotation_value" VALUES(862,442,358);
+INSERT INTO "annotation_value" VALUES(863,442,354);
+INSERT INTO "annotation_value" VALUES(864,442,365);
+INSERT INTO "annotation_value" VALUES(865,442,360);
+INSERT INTO "annotation_value" VALUES(866,442,361);
+INSERT INTO "annotation_value" VALUES(867,442,387);
+INSERT INTO "annotation_value" VALUES(868,442,356);
+INSERT INTO "annotation_value" VALUES(869,442,367);
+INSERT INTO "annotation_value" VALUES(870,442,364);
+INSERT INTO "annotation_value" VALUES(871,442,348);
+INSERT INTO "annotation_value" VALUES(872,442,383);
+INSERT INTO "annotation_value" VALUES(873,442,371);
+INSERT INTO "annotation_value" VALUES(874,442,369);
+INSERT INTO "annotation_value" VALUES(875,442,368);
+INSERT INTO "annotation_value" VALUES(876,442,374);
+INSERT INTO "annotation_value" VALUES(877,442,377);
+INSERT INTO "annotation_value" VALUES(878,442,351);
+INSERT INTO "annotation_value" VALUES(879,442,350);
+INSERT INTO "annotation_value" VALUES(880,442,372);
+INSERT INTO "annotation_value" VALUES(881,442,379);
+INSERT INTO "annotation_value" VALUES(882,442,349);
+INSERT INTO "annotation_value" VALUES(883,442,390);
+INSERT INTO "annotation_value" VALUES(884,442,363);
+INSERT INTO "annotation_value" VALUES(885,442,385);
+INSERT INTO "annotation_value" VALUES(886,442,347);
+INSERT INTO "annotation_value" VALUES(887,442,388);
+INSERT INTO "annotation_value" VALUES(888,442,362);
+INSERT INTO "annotation_value" VALUES(889,442,376);
+INSERT INTO "annotation_value" VALUES(890,442,373);
+INSERT INTO "annotation_value" VALUES(891,442,357);
+INSERT INTO "annotation_value" VALUES(892,442,386);
+INSERT INTO "annotation_value" VALUES(893,442,359);
+INSERT INTO "annotation_value" VALUES(894,442,370);
+INSERT INTO "annotation_value" VALUES(895,442,375);
+INSERT INTO "annotation_value" VALUES(896,442,366);
+INSERT INTO "annotation_value" VALUES(897,442,352);
+INSERT INTO "annotation_value" VALUES(898,443,364);
+INSERT INTO "annotation_value" VALUES(899,443,382);
+INSERT INTO "annotation_value" VALUES(900,443,388);
+INSERT INTO "annotation_value" VALUES(901,443,374);
+INSERT INTO "annotation_value" VALUES(902,443,353);
+INSERT INTO "annotation_value" VALUES(903,443,375);
+INSERT INTO "annotation_value" VALUES(904,443,363);
+INSERT INTO "annotation_value" VALUES(905,443,366);
+INSERT INTO "annotation_value" VALUES(906,443,379);
+INSERT INTO "annotation_value" VALUES(907,443,368);
+INSERT INTO "annotation_value" VALUES(908,443,354);
+INSERT INTO "annotation_value" VALUES(909,443,389);
+INSERT INTO "annotation_value" VALUES(910,443,361);
+INSERT INTO "annotation_value" VALUES(911,443,347);
+INSERT INTO "annotation_value" VALUES(912,443,383);
+INSERT INTO "annotation_value" VALUES(913,443,378);
+INSERT INTO "annotation_value" VALUES(914,443,358);
+INSERT INTO "annotation_value" VALUES(915,443,352);
+INSERT INTO "annotation_value" VALUES(916,443,362);
+INSERT INTO "annotation_value" VALUES(917,443,385);
+INSERT INTO "annotation_value" VALUES(918,443,372);
+INSERT INTO "annotation_value" VALUES(919,443,390);
+INSERT INTO "annotation_value" VALUES(920,443,351);
+INSERT INTO "annotation_value" VALUES(921,443,386);
+INSERT INTO "annotation_value" VALUES(922,443,384);
+INSERT INTO "annotation_value" VALUES(923,443,376);
+INSERT INTO "annotation_value" VALUES(924,443,139);
+INSERT INTO "annotation_value" VALUES(925,443,387);
+INSERT INTO "annotation_value" VALUES(926,443,357);
+INSERT INTO "annotation_value" VALUES(927,443,381);
+INSERT INTO "annotation_value" VALUES(928,443,355);
+INSERT INTO "annotation_value" VALUES(929,443,373);
+INSERT INTO "annotation_value" VALUES(930,443,356);
+INSERT INTO "annotation_value" VALUES(931,443,377);
+INSERT INTO "annotation_value" VALUES(932,443,349);
+INSERT INTO "annotation_value" VALUES(933,443,350);
+INSERT INTO "annotation_value" VALUES(934,443,359);
+INSERT INTO "annotation_value" VALUES(935,443,369);
+INSERT INTO "annotation_value" VALUES(936,443,380);
+INSERT INTO "annotation_value" VALUES(937,443,370);
+INSERT INTO "annotation_value" VALUES(938,443,360);
+INSERT INTO "annotation_value" VALUES(939,443,367);
+INSERT INTO "annotation_value" VALUES(940,443,371);
+INSERT INTO "annotation_value" VALUES(941,443,365);
+INSERT INTO "annotation_value" VALUES(942,443,348);
+INSERT INTO "annotation_value" VALUES(943,444,383);
+INSERT INTO "annotation_value" VALUES(944,444,351);
+INSERT INTO "annotation_value" VALUES(945,444,350);
+INSERT INTO "annotation_value" VALUES(946,444,353);
+INSERT INTO "annotation_value" VALUES(947,444,374);
+INSERT INTO "annotation_value" VALUES(948,444,368);
+INSERT INTO "annotation_value" VALUES(949,444,348);
+INSERT INTO "annotation_value" VALUES(950,444,380);
+INSERT INTO "annotation_value" VALUES(951,444,359);
+INSERT INTO "annotation_value" VALUES(952,444,360);
+INSERT INTO "annotation_value" VALUES(953,444,357);
+INSERT INTO "annotation_value" VALUES(954,444,370);
+INSERT INTO "annotation_value" VALUES(955,444,361);
+INSERT INTO "annotation_value" VALUES(956,444,366);
+INSERT INTO "annotation_value" VALUES(957,444,385);
+INSERT INTO "annotation_value" VALUES(958,444,388);
+INSERT INTO "annotation_value" VALUES(959,444,367);
+INSERT INTO "annotation_value" VALUES(960,444,378);
+INSERT INTO "annotation_value" VALUES(961,444,373);
+INSERT INTO "annotation_value" VALUES(962,444,382);
+INSERT INTO "annotation_value" VALUES(963,444,377);
+INSERT INTO "annotation_value" VALUES(964,444,389);
+INSERT INTO "annotation_value" VALUES(965,444,355);
+INSERT INTO "annotation_value" VALUES(966,444,364);
+INSERT INTO "annotation_value" VALUES(967,444,371);
+INSERT INTO "annotation_value" VALUES(968,444,358);
+INSERT INTO "annotation_value" VALUES(969,444,376);
+INSERT INTO "annotation_value" VALUES(970,444,369);
+INSERT INTO "annotation_value" VALUES(971,444,347);
+INSERT INTO "annotation_value" VALUES(972,444,375);
+INSERT INTO "annotation_value" VALUES(973,444,349);
+INSERT INTO "annotation_value" VALUES(974,444,362);
+INSERT INTO "annotation_value" VALUES(975,444,352);
+INSERT INTO "annotation_value" VALUES(976,444,356);
+INSERT INTO "annotation_value" VALUES(977,444,381);
+INSERT INTO "annotation_value" VALUES(978,444,384);
+INSERT INTO "annotation_value" VALUES(979,444,372);
+INSERT INTO "annotation_value" VALUES(980,444,139);
+INSERT INTO "annotation_value" VALUES(981,444,363);
+INSERT INTO "annotation_value" VALUES(982,444,379);
+INSERT INTO "annotation_value" VALUES(983,444,386);
+INSERT INTO "annotation_value" VALUES(984,444,354);
+INSERT INTO "annotation_value" VALUES(985,444,387);
+INSERT INTO "annotation_value" VALUES(986,444,390);
+INSERT INTO "annotation_value" VALUES(987,444,365);
+INSERT INTO "annotation_value" VALUES(988,445,348);
+INSERT INTO "annotation_value" VALUES(989,445,383);
+INSERT INTO "annotation_value" VALUES(990,445,364);
+INSERT INTO "annotation_value" VALUES(991,445,347);
+INSERT INTO "annotation_value" VALUES(992,445,361);
+INSERT INTO "annotation_value" VALUES(993,445,376);
+INSERT INTO "annotation_value" VALUES(994,445,370);
+INSERT INTO "annotation_value" VALUES(995,445,363);
+INSERT INTO "annotation_value" VALUES(996,445,352);
+INSERT INTO "annotation_value" VALUES(997,445,356);
+INSERT INTO "annotation_value" VALUES(998,445,372);
+INSERT INTO "annotation_value" VALUES(999,445,386);
+INSERT INTO "annotation_value" VALUES(1000,445,378);
+INSERT INTO "annotation_value" VALUES(1001,445,390);
+INSERT INTO "annotation_value" VALUES(1002,445,355);
+INSERT INTO "annotation_value" VALUES(1003,445,366);
+INSERT INTO "annotation_value" VALUES(1004,445,139);
+INSERT INTO "annotation_value" VALUES(1005,445,385);
+INSERT INTO "annotation_value" VALUES(1006,445,368);
+INSERT INTO "annotation_value" VALUES(1007,445,365);
+INSERT INTO "annotation_value" VALUES(1008,445,360);
+INSERT INTO "annotation_value" VALUES(1009,445,362);
+INSERT INTO "annotation_value" VALUES(1010,445,367);
+INSERT INTO "annotation_value" VALUES(1011,445,357);
+INSERT INTO "annotation_value" VALUES(1012,445,388);
+INSERT INTO "annotation_value" VALUES(1013,445,382);
+INSERT INTO "annotation_value" VALUES(1014,445,379);
+INSERT INTO "annotation_value" VALUES(1015,445,381);
+INSERT INTO "annotation_value" VALUES(1016,445,359);
+INSERT INTO "annotation_value" VALUES(1017,445,350);
+INSERT INTO "annotation_value" VALUES(1018,445,387);
+INSERT INTO "annotation_value" VALUES(1019,445,358);
+INSERT INTO "annotation_value" VALUES(1020,445,375);
+INSERT INTO "annotation_value" VALUES(1021,445,374);
+INSERT INTO "annotation_value" VALUES(1022,445,371);
+INSERT INTO "annotation_value" VALUES(1023,445,349);
+INSERT INTO "annotation_value" VALUES(1024,445,377);
+INSERT INTO "annotation_value" VALUES(1025,445,380);
+INSERT INTO "annotation_value" VALUES(1026,445,354);
+INSERT INTO "annotation_value" VALUES(1027,445,384);
+INSERT INTO "annotation_value" VALUES(1028,445,351);
+INSERT INTO "annotation_value" VALUES(1029,445,369);
+INSERT INTO "annotation_value" VALUES(1030,445,353);
+INSERT INTO "annotation_value" VALUES(1031,445,373);
+INSERT INTO "annotation_value" VALUES(1032,445,389);
+INSERT INTO "annotation_value" VALUES(1033,446,374);
+INSERT INTO "annotation_value" VALUES(1034,446,373);
+INSERT INTO "annotation_value" VALUES(1035,446,381);
+INSERT INTO "annotation_value" VALUES(1036,446,380);
+INSERT INTO "annotation_value" VALUES(1037,446,383);
+INSERT INTO "annotation_value" VALUES(1038,446,362);
+INSERT INTO "annotation_value" VALUES(1039,446,356);
+INSERT INTO "annotation_value" VALUES(1040,446,353);
+INSERT INTO "annotation_value" VALUES(1041,446,359);
+INSERT INTO "annotation_value" VALUES(1042,446,363);
+INSERT INTO "annotation_value" VALUES(1043,446,355);
+INSERT INTO "annotation_value" VALUES(1044,446,358);
+INSERT INTO "annotation_value" VALUES(1045,446,382);
+INSERT INTO "annotation_value" VALUES(1046,446,378);
+INSERT INTO "annotation_value" VALUES(1047,446,357);
+INSERT INTO "annotation_value" VALUES(1048,446,390);
+INSERT INTO "annotation_value" VALUES(1049,446,354);
+INSERT INTO "annotation_value" VALUES(1050,446,371);
+INSERT INTO "annotation_value" VALUES(1051,446,372);
+INSERT INTO "annotation_value" VALUES(1052,446,367);
+INSERT INTO "annotation_value" VALUES(1053,446,351);
+INSERT INTO "annotation_value" VALUES(1054,446,379);
+INSERT INTO "annotation_value" VALUES(1055,446,350);
+INSERT INTO "annotation_value" VALUES(1056,446,386);
+INSERT INTO "annotation_value" VALUES(1057,446,139);
+INSERT INTO "annotation_value" VALUES(1058,446,388);
+INSERT INTO "annotation_value" VALUES(1059,446,352);
+INSERT INTO "annotation_value" VALUES(1060,446,360);
+INSERT INTO "annotation_value" VALUES(1061,446,361);
+INSERT INTO "annotation_value" VALUES(1062,446,365);
+INSERT INTO "annotation_value" VALUES(1063,446,349);
+INSERT INTO "annotation_value" VALUES(1064,446,385);
+INSERT INTO "annotation_value" VALUES(1065,446,348);
+INSERT INTO "annotation_value" VALUES(1066,446,364);
+INSERT INTO "annotation_value" VALUES(1067,446,366);
+INSERT INTO "annotation_value" VALUES(1068,446,377);
+INSERT INTO "annotation_value" VALUES(1069,446,387);
+INSERT INTO "annotation_value" VALUES(1070,446,375);
+INSERT INTO "annotation_value" VALUES(1071,446,347);
+INSERT INTO "annotation_value" VALUES(1072,446,389);
+INSERT INTO "annotation_value" VALUES(1073,446,368);
+INSERT INTO "annotation_value" VALUES(1074,446,370);
+INSERT INTO "annotation_value" VALUES(1075,446,369);
+INSERT INTO "annotation_value" VALUES(1076,446,376);
+INSERT INTO "annotation_value" VALUES(1077,446,384);
+INSERT INTO "annotation_value" VALUES(1078,447,370);
+INSERT INTO "annotation_value" VALUES(1079,447,371);
+INSERT INTO "annotation_value" VALUES(1080,447,360);
+INSERT INTO "annotation_value" VALUES(1081,447,376);
+INSERT INTO "annotation_value" VALUES(1082,447,387);
+INSERT INTO "annotation_value" VALUES(1083,447,373);
+INSERT INTO "annotation_value" VALUES(1084,447,348);
+INSERT INTO "annotation_value" VALUES(1085,447,375);
+INSERT INTO "annotation_value" VALUES(1086,447,356);
+INSERT INTO "annotation_value" VALUES(1087,447,384);
+INSERT INTO "annotation_value" VALUES(1088,447,381);
+INSERT INTO "annotation_value" VALUES(1089,447,351);
+INSERT INTO "annotation_value" VALUES(1090,447,353);
+INSERT INTO "annotation_value" VALUES(1091,447,385);
+INSERT INTO "annotation_value" VALUES(1092,447,368);
+INSERT INTO "annotation_value" VALUES(1093,447,389);
+INSERT INTO "annotation_value" VALUES(1094,447,352);
+INSERT INTO "annotation_value" VALUES(1095,447,349);
+INSERT INTO "annotation_value" VALUES(1096,447,380);
+INSERT INTO "annotation_value" VALUES(1097,447,359);
+INSERT INTO "annotation_value" VALUES(1098,447,364);
+INSERT INTO "annotation_value" VALUES(1099,447,363);
+INSERT INTO "annotation_value" VALUES(1100,447,377);
+INSERT INTO "annotation_value" VALUES(1101,447,139);
+INSERT INTO "annotation_value" VALUES(1102,447,355);
+INSERT INTO "annotation_value" VALUES(1103,447,383);
+INSERT INTO "annotation_value" VALUES(1104,447,382);
+INSERT INTO "annotation_value" VALUES(1105,447,350);
+INSERT INTO "annotation_value" VALUES(1106,447,354);
+INSERT INTO "annotation_value" VALUES(1107,447,357);
+INSERT INTO "annotation_value" VALUES(1108,447,379);
+INSERT INTO "annotation_value" VALUES(1109,447,367);
+INSERT INTO "annotation_value" VALUES(1110,447,378);
+INSERT INTO "annotation_value" VALUES(1111,447,361);
+INSERT INTO "annotation_value" VALUES(1112,447,362);
+INSERT INTO "annotation_value" VALUES(1113,447,366);
+INSERT INTO "annotation_value" VALUES(1114,447,374);
+INSERT INTO "annotation_value" VALUES(1115,447,390);
+INSERT INTO "annotation_value" VALUES(1116,447,386);
+INSERT INTO "annotation_value" VALUES(1117,447,358);
+INSERT INTO "annotation_value" VALUES(1118,447,369);
+INSERT INTO "annotation_value" VALUES(1119,447,347);
+INSERT INTO "annotation_value" VALUES(1120,447,388);
+INSERT INTO "annotation_value" VALUES(1121,447,365);
+INSERT INTO "annotation_value" VALUES(1122,447,372);
+INSERT INTO "annotation_value" VALUES(1123,448,370);
+INSERT INTO "annotation_value" VALUES(1124,448,367);
+INSERT INTO "annotation_value" VALUES(1125,448,351);
+INSERT INTO "annotation_value" VALUES(1126,448,364);
+INSERT INTO "annotation_value" VALUES(1127,448,357);
+INSERT INTO "annotation_value" VALUES(1128,448,385);
+INSERT INTO "annotation_value" VALUES(1129,448,390);
+INSERT INTO "annotation_value" VALUES(1130,448,353);
+INSERT INTO "annotation_value" VALUES(1131,448,384);
+INSERT INTO "annotation_value" VALUES(1132,448,350);
+INSERT INTO "annotation_value" VALUES(1133,448,381);
+INSERT INTO "annotation_value" VALUES(1134,448,376);
+INSERT INTO "annotation_value" VALUES(1135,448,369);
+INSERT INTO "annotation_value" VALUES(1136,448,377);
+INSERT INTO "annotation_value" VALUES(1137,448,139);
+INSERT INTO "annotation_value" VALUES(1138,448,361);
+INSERT INTO "annotation_value" VALUES(1139,448,352);
+INSERT INTO "annotation_value" VALUES(1140,448,388);
+INSERT INTO "annotation_value" VALUES(1141,448,374);
+INSERT INTO "annotation_value" VALUES(1142,448,383);
+INSERT INTO "annotation_value" VALUES(1143,448,371);
+INSERT INTO "annotation_value" VALUES(1144,448,375);
+INSERT INTO "annotation_value" VALUES(1145,448,379);
+INSERT INTO "annotation_value" VALUES(1146,448,386);
+INSERT INTO "annotation_value" VALUES(1147,448,356);
+INSERT INTO "annotation_value" VALUES(1148,448,365);
+INSERT INTO "annotation_value" VALUES(1149,448,387);
+INSERT INTO "annotation_value" VALUES(1150,448,354);
+INSERT INTO "annotation_value" VALUES(1151,448,360);
+INSERT INTO "annotation_value" VALUES(1152,448,359);
+INSERT INTO "annotation_value" VALUES(1153,448,378);
+INSERT INTO "annotation_value" VALUES(1154,448,382);
+INSERT INTO "annotation_value" VALUES(1155,448,366);
+INSERT INTO "annotation_value" VALUES(1156,448,349);
+INSERT INTO "annotation_value" VALUES(1157,448,347);
+INSERT INTO "annotation_value" VALUES(1158,448,380);
+INSERT INTO "annotation_value" VALUES(1159,448,358);
+INSERT INTO "annotation_value" VALUES(1160,448,355);
+INSERT INTO "annotation_value" VALUES(1161,448,348);
+INSERT INTO "annotation_value" VALUES(1162,448,372);
+INSERT INTO "annotation_value" VALUES(1163,448,363);
+INSERT INTO "annotation_value" VALUES(1164,448,389);
+INSERT INTO "annotation_value" VALUES(1165,448,373);
+INSERT INTO "annotation_value" VALUES(1166,448,368);
+INSERT INTO "annotation_value" VALUES(1167,448,362);
+INSERT INTO "annotation_value" VALUES(1168,449,377);
+INSERT INTO "annotation_value" VALUES(1169,449,348);
+INSERT INTO "annotation_value" VALUES(1170,449,382);
+INSERT INTO "annotation_value" VALUES(1171,449,351);
+INSERT INTO "annotation_value" VALUES(1172,449,390);
+INSERT INTO "annotation_value" VALUES(1173,449,361);
+INSERT INTO "annotation_value" VALUES(1174,449,347);
+INSERT INTO "annotation_value" VALUES(1175,449,352);
+INSERT INTO "annotation_value" VALUES(1176,449,379);
+INSERT INTO "annotation_value" VALUES(1177,449,372);
+INSERT INTO "annotation_value" VALUES(1178,449,365);
+INSERT INTO "annotation_value" VALUES(1179,449,376);
+INSERT INTO "annotation_value" VALUES(1180,449,363);
+INSERT INTO "annotation_value" VALUES(1181,449,387);
+INSERT INTO "annotation_value" VALUES(1182,449,368);
+INSERT INTO "annotation_value" VALUES(1183,449,386);
+INSERT INTO "annotation_value" VALUES(1184,449,384);
+INSERT INTO "annotation_value" VALUES(1185,449,389);
+INSERT INTO "annotation_value" VALUES(1186,449,360);
+INSERT INTO "annotation_value" VALUES(1187,449,358);
+INSERT INTO "annotation_value" VALUES(1188,449,139);
+INSERT INTO "annotation_value" VALUES(1189,449,380);
+INSERT INTO "annotation_value" VALUES(1190,449,353);
+INSERT INTO "annotation_value" VALUES(1191,449,359);
+INSERT INTO "annotation_value" VALUES(1192,449,375);
+INSERT INTO "annotation_value" VALUES(1193,449,371);
+INSERT INTO "annotation_value" VALUES(1194,449,355);
+INSERT INTO "annotation_value" VALUES(1195,449,354);
+INSERT INTO "annotation_value" VALUES(1196,449,349);
+INSERT INTO "annotation_value" VALUES(1197,449,385);
+INSERT INTO "annotation_value" VALUES(1198,449,378);
+INSERT INTO "annotation_value" VALUES(1199,449,367);
+INSERT INTO "annotation_value" VALUES(1200,449,374);
+INSERT INTO "annotation_value" VALUES(1201,449,370);
+INSERT INTO "annotation_value" VALUES(1202,449,364);
+INSERT INTO "annotation_value" VALUES(1203,449,366);
+INSERT INTO "annotation_value" VALUES(1204,449,383);
+INSERT INTO "annotation_value" VALUES(1205,449,362);
+INSERT INTO "annotation_value" VALUES(1206,449,381);
+INSERT INTO "annotation_value" VALUES(1207,449,388);
+INSERT INTO "annotation_value" VALUES(1208,449,357);
+INSERT INTO "annotation_value" VALUES(1209,449,350);
+INSERT INTO "annotation_value" VALUES(1210,449,356);
+INSERT INTO "annotation_value" VALUES(1211,449,373);
+INSERT INTO "annotation_value" VALUES(1212,449,369);
+INSERT INTO "annotation_value" VALUES(1213,450,389);
+INSERT INTO "annotation_value" VALUES(1214,450,360);
+INSERT INTO "annotation_value" VALUES(1215,450,370);
+INSERT INTO "annotation_value" VALUES(1216,450,390);
+INSERT INTO "annotation_value" VALUES(1217,450,372);
+INSERT INTO "annotation_value" VALUES(1218,450,375);
+INSERT INTO "annotation_value" VALUES(1219,450,381);
+INSERT INTO "annotation_value" VALUES(1220,450,362);
+INSERT INTO "annotation_value" VALUES(1221,450,353);
+INSERT INTO "annotation_value" VALUES(1222,450,388);
+INSERT INTO "annotation_value" VALUES(1223,450,377);
+INSERT INTO "annotation_value" VALUES(1224,450,351);
+INSERT INTO "annotation_value" VALUES(1225,450,358);
+INSERT INTO "annotation_value" VALUES(1226,450,139);
+INSERT INTO "annotation_value" VALUES(1227,450,357);
+INSERT INTO "annotation_value" VALUES(1228,450,385);
+INSERT INTO "annotation_value" VALUES(1229,450,366);
+INSERT INTO "annotation_value" VALUES(1230,450,365);
+INSERT INTO "annotation_value" VALUES(1231,450,363);
+INSERT INTO "annotation_value" VALUES(1232,450,387);
+INSERT INTO "annotation_value" VALUES(1233,450,364);
+INSERT INTO "annotation_value" VALUES(1234,450,354);
+INSERT INTO "annotation_value" VALUES(1235,450,386);
+INSERT INTO "annotation_value" VALUES(1236,450,382);
+INSERT INTO "annotation_value" VALUES(1237,450,361);
+INSERT INTO "annotation_value" VALUES(1238,450,384);
+INSERT INTO "annotation_value" VALUES(1239,450,368);
+INSERT INTO "annotation_value" VALUES(1240,450,347);
+INSERT INTO "annotation_value" VALUES(1241,450,374);
+INSERT INTO "annotation_value" VALUES(1242,450,369);
+INSERT INTO "annotation_value" VALUES(1243,450,376);
+INSERT INTO "annotation_value" VALUES(1244,450,355);
+INSERT INTO "annotation_value" VALUES(1245,450,349);
+INSERT INTO "annotation_value" VALUES(1246,450,350);
+INSERT INTO "annotation_value" VALUES(1247,450,356);
+INSERT INTO "annotation_value" VALUES(1248,450,378);
+INSERT INTO "annotation_value" VALUES(1249,450,379);
+INSERT INTO "annotation_value" VALUES(1250,450,373);
+INSERT INTO "annotation_value" VALUES(1251,450,352);
+INSERT INTO "annotation_value" VALUES(1252,450,359);
+INSERT INTO "annotation_value" VALUES(1253,450,380);
+INSERT INTO "annotation_value" VALUES(1254,450,371);
+INSERT INTO "annotation_value" VALUES(1255,450,383);
+INSERT INTO "annotation_value" VALUES(1256,450,348);
+INSERT INTO "annotation_value" VALUES(1257,450,367);
+INSERT INTO "annotation_value" VALUES(1258,451,389);
+INSERT INTO "annotation_value" VALUES(1259,451,350);
+INSERT INTO "annotation_value" VALUES(1260,451,381);
+INSERT INTO "annotation_value" VALUES(1261,451,385);
+INSERT INTO "annotation_value" VALUES(1262,451,356);
+INSERT INTO "annotation_value" VALUES(1263,451,372);
+INSERT INTO "annotation_value" VALUES(1264,451,374);
+INSERT INTO "annotation_value" VALUES(1265,451,355);
+INSERT INTO "annotation_value" VALUES(1266,451,354);
+INSERT INTO "annotation_value" VALUES(1267,451,388);
+INSERT INTO "annotation_value" VALUES(1268,451,364);
+INSERT INTO "annotation_value" VALUES(1269,451,361);
+INSERT INTO "annotation_value" VALUES(1270,451,366);
+INSERT INTO "annotation_value" VALUES(1271,451,368);
+INSERT INTO "annotation_value" VALUES(1272,451,373);
+INSERT INTO "annotation_value" VALUES(1273,451,384);
+INSERT INTO "annotation_value" VALUES(1274,451,351);
+INSERT INTO "annotation_value" VALUES(1275,451,369);
+INSERT INTO "annotation_value" VALUES(1276,451,363);
+INSERT INTO "annotation_value" VALUES(1277,451,352);
+INSERT INTO "annotation_value" VALUES(1278,451,376);
+INSERT INTO "annotation_value" VALUES(1279,451,362);
+INSERT INTO "annotation_value" VALUES(1280,451,382);
+INSERT INTO "annotation_value" VALUES(1281,451,378);
+INSERT INTO "annotation_value" VALUES(1282,451,139);
+INSERT INTO "annotation_value" VALUES(1283,451,390);
+INSERT INTO "annotation_value" VALUES(1284,451,359);
+INSERT INTO "annotation_value" VALUES(1285,451,383);
+INSERT INTO "annotation_value" VALUES(1286,451,387);
+INSERT INTO "annotation_value" VALUES(1287,451,371);
+INSERT INTO "annotation_value" VALUES(1288,451,367);
+INSERT INTO "annotation_value" VALUES(1289,451,353);
+INSERT INTO "annotation_value" VALUES(1290,451,349);
+INSERT INTO "annotation_value" VALUES(1291,451,365);
+INSERT INTO "annotation_value" VALUES(1292,451,380);
+INSERT INTO "annotation_value" VALUES(1293,451,358);
+INSERT INTO "annotation_value" VALUES(1294,451,386);
+INSERT INTO "annotation_value" VALUES(1295,451,379);
+INSERT INTO "annotation_value" VALUES(1296,451,347);
+INSERT INTO "annotation_value" VALUES(1297,451,375);
+INSERT INTO "annotation_value" VALUES(1298,451,357);
+INSERT INTO "annotation_value" VALUES(1299,451,348);
+INSERT INTO "annotation_value" VALUES(1300,451,377);
+INSERT INTO "annotation_value" VALUES(1301,451,370);
+INSERT INTO "annotation_value" VALUES(1302,451,360);
diff --git a/src/main/resources/db/lite/V1__annotation_tables.sql b/src/main/resources/db/lite/V1__annotation_tables.sql
new file mode 100644
index 0000000..b0299d1
--- /dev/null
+++ b/src/main/resources/db/lite/V1__annotation_tables.sql
@@ -0,0 +1,53 @@
+CREATE TABLE IF NOT EXISTS annotation(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	code VARCHAR(20) NOT NULL,
+	type VARCHAR(20) NOT NULL,
+	text VARCHAR(20) NULL,
+	description VARCHAR(100) NOT NULL,
+	de_description VARCHAR(100)
+);
+
+CREATE UNIQUE INDEX annotation_index ON annotation (code, type);
+
+CREATE TABLE IF NOT EXISTS annotation_layer(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	foundry_id INTEGER NOT NULL,
+	layer_id INTEGER NOT NULL,
+	description VARCHAR(255) NOT NULL,
+	FOREIGN KEY (foundry_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX annotation_layer_index ON annotation_layer (foundry_id, layer_id);
+
+CREATE TABLE IF NOT EXISTS annotation_key(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	layer_id INTEGER NOT NULL,
+	key_id INTEGER NOT NULL,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation_layer (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (key_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX annotation_key_index ON annotation_key (layer_id, key_id);
+
+CREATE TABLE IF NOT EXISTS annotation_value(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	key_id INTEGER NOT NULL,
+	value_id INTEGER NOT NULL,
+	FOREIGN KEY (key_id)
+		REFERENCES annotation_key (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (key_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX annotation_value_index ON annotation_value (key_id, value_id);
diff --git a/src/main/resources/db/mysql/V1.1__create_virtual_corpus_tables.sql b/src/main/resources/db/mysql/V1.1__create_virtual_corpus_tables.sql
new file mode 100644
index 0000000..590e318
--- /dev/null
+++ b/src/main/resources/db/mysql/V1.1__create_virtual_corpus_tables.sql
@@ -0,0 +1,90 @@
+CREATE TABLE IF NOT EXISTS role (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  name VARCHAR(100) NOT NULL,
+  UNIQUE INDEX name_index(name)
+);
+
+
+CREATE TABLE IF NOT EXISTS privilege (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  name VARCHAR(20) NOT NULL,
+  role_id int NOT NULL,
+  UNIQUE INDEX privilege_index(name, role_id),
+  FOREIGN KEY (role_id) 
+  	REFERENCES role (id)
+  	ON DELETE CASCADE
+);
+
+
+CREATE TABLE IF NOT EXISTS user_group (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  name VARCHAR(100) NOT NULL,
+  description VARCHAR(255) DEFAULT NULL,
+  status VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  deleted_by VARCHAR(100) DEFAULT NULL,
+  INDEX status_index(status),
+  UNIQUE INDEX unique_name(name);
+);
+
+CREATE TABLE IF NOT EXISTS user_group_member (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  user_id VARCHAR(100) NOT NULL,
+  group_id int(11) NOT NULL,
+  status VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  deleted_by VARCHAR(100) DEFAULT NULL,
+  status_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE INDEX unique_index (user_id,group_id),
+  INDEX status_index(status),
+  FOREIGN KEY (group_id) 
+  	REFERENCES user_group (id)
+  	ON DELETE CASCADE
+); 
+
+CREATE TABLE IF NOT EXISTS group_member_role (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  group_member_id int(11) NOT NULL,
+  role_id int NOT NULL,
+  UNIQUE INDEX unique_index (group_member_id,role_id),
+  FOREIGN KEY (group_member_id)
+  	REFERENCES user_group_member (id)
+  	ON DELETE CASCADE,
+  FOREIGN KEY (role_id) 
+  	REFERENCES role (id)
+  	ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS virtual_corpus (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  name VARCHAR(255) NOT NULL,
+  type VARCHAR(100) NOT NULL,
+  required_access VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  description VARCHAR(255) DEFAULT NULL,
+  status VARCHAR(100) DEFAULT NULL,
+  corpus_query TEXT NOT NULL,
+  definition VARCHAR(255) DEFAULT NULL,
+  is_cached BOOLEAN DEFAULT 0,
+  UNIQUE INDEX unique_index (name,created_by),
+  INDEX owner_index (created_by),
+  INDEX type_index (type)
+);
+
+CREATE TABLE IF NOT EXISTS virtual_corpus_access (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  virtual_corpus_id int(11) NOT NULL,
+  user_group_id int(11) NOT NULL,
+  status VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  approved_by VARCHAR(100) DEFAULT NULL,
+  deleted_by VARCHAR(100) DEFAULT NULL,
+  UNIQUE INDEX unique_index (virtual_corpus_id,user_group_id),
+  INDEX status_index(status),
+  FOREIGN KEY (user_group_id) 
+  	REFERENCES user_group (id)
+  	ON DELETE CASCADE,
+  FOREIGN KEY (virtual_corpus_id) 
+  	REFERENCES virtual_corpus (id)
+  	ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/src/main/resources/db/mysql/V1.2__create_admin_table.sql b/src/main/resources/db/mysql/V1.2__create_admin_table.sql
new file mode 100644
index 0000000..39a7114
--- /dev/null
+++ b/src/main/resources/db/mysql/V1.2__create_admin_table.sql
@@ -0,0 +1,5 @@
+CREATE TABLE IF NOT EXISTS admin (
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	user_id VARCHAR(100) NOT NULL,
+	UNIQUE INDEX unique_index (user_id)
+);
\ No newline at end of file
diff --git a/src/main/resources/db/mysql/V1.3__triggers.sql b/src/main/resources/db/mysql/V1.3__triggers.sql
new file mode 100644
index 0000000..7bc25dd
--- /dev/null
+++ b/src/main/resources/db/mysql/V1.3__triggers.sql
@@ -0,0 +1,14 @@
+delimiter |
+
+CREATE TRIGGER delete_member AFTER UPDATE ON user_group
+	FOR EACH ROW 
+	BEGIN
+		UPDATE user_group_member 
+		SET status = "DELETED"
+		WHERE NEW.status = "DELETED" 
+			AND  OLD.status != "DELETED" 
+			AND group_id = NEW.id;
+	END;
+|
+	
+delimiter ;
\ No newline at end of file
diff --git a/src/main/resources/db/mysql/V1.4__oauth2_tables.sql b/src/main/resources/db/mysql/V1.4__oauth2_tables.sql
new file mode 100644
index 0000000..ff0230f
--- /dev/null
+++ b/src/main/resources/db/mysql/V1.4__oauth2_tables.sql
@@ -0,0 +1,96 @@
+-- EM: modified from Michael Hanl version
+
+-- oauth2 db tables
+CREATE TABLE IF NOT EXISTS oauth2_client (
+	id VARCHAR(100) PRIMARY KEY NOT NULL,
+	name VARCHAR(100) NOT NULL,
+	secret VARCHAR(255) DEFAULT NULL,
+	type VARCHAR(50) NOT NULL,
+	super BOOLEAN DEFAULT FALSE,
+	redirect_uri TEXT DEFAULT NULL,
+	description VARCHAR(255) NOT NULL,
+	registered_by VARCHAR(100) NOT NULL,
+	url_hashcode INTEGER NOT NULL,	
+	url TEXT DEFAULT NULL,
+	UNIQUE INDEX unique_url(url_hashcode)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_scope (
+	id VARCHAR(100) PRIMARY KEY NOT NULL
+);
+
+-- authorization tables are not needed if using cache 
+
+--CREATE TABLE IF NOT EXISTS oauth2_authorization (
+--	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+--	code VARCHAR(255) NOT NULL,
+--	client_id VARCHAR(100) NOT NULL,
+--	user_id VARCHAR(100) NOT NULL,
+--	redirect_uri TEXT DEFAULT NULL,
+--	created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+--	expiry_date TIMESTAMP NULL,
+--	is_revoked BOOLEAN DEFAULT 0,
+--	total_attempts INTEGER DEFAULT 0,
+--	user_auth_time TIMESTAMP NULL,
+--	nonce TEXT DEFAULT NULL,
+--	FOREIGN KEY (client_id)
+--	   REFERENCES oauth2_client(id),
+--	UNIQUE INDEX authorization_index(code, client_id)
+--);
+--
+--CREATE TABLE IF NOT EXISTS oauth2_authorization_scope (
+--	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+--	authorization_id INTEGER NOT NULL,
+--	scope_id VARCHAR(100) NOT NULL,
+--	FOREIGN KEY (authorization_id)
+--	   REFERENCES oauth2_authorization(id),
+--	FOREIGN KEY (scope_id)
+--	   REFERENCES oauth2_access_scope(id),
+--	UNIQUE INDEX authorization_scope_index(authorization_id, scope_id)
+--);
+
+CREATE TABLE IF NOT EXISTS oauth2_refresh_token (
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	token VARCHAR(255) NOT NULL,
+	user_id VARCHAR(100) DEFAULT NULL,
+	user_auth_time TIMESTAMP NOT NULL,
+	created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+	expiry_date TIMESTAMP NULL,
+	is_revoked BOOLEAN DEFAULT 0,
+	client VARCHAR(100) NOT NULL,
+	FOREIGN KEY (client)
+	   REFERENCES oauth2_client(id)
+	   -- these will delete all refresh tokens related to the client
+	   ON DELETE CASCADE
+);
+
+CREATE TABLE oauth2_refresh_token_scope (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token (
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	token VARCHAR(255) NOT NULL,
+	user_id VARCHAR(100) DEFAULT NULL,
+	client_id VARCHAR(100) DEFAULT NULL,
+	created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+	expiry_date TIMESTAMP NULL,
+	is_revoked BOOLEAN DEFAULT 0,
+	user_auth_time TIMESTAMP NULL,
+    refresh_token INTEGER DEFAULT NULL,
+	FOREIGN KEY (client_id)
+	   REFERENCES oauth2_client(id)
+	   -- these will delete all access tokens related to the client
+	   ON DELETE CASCADE,
+	FOREIGN KEY (refresh_token)
+	   REFERENCES oauth2_refresh_token(id)
+);
+
+CREATE TABLE oauth2_access_token_scope (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
+);
+
diff --git a/src/main/resources/db/mysql/V1.6__user_tables.sql b/src/main/resources/db/mysql/V1.6__user_tables.sql
new file mode 100644
index 0000000..9df4a20
--- /dev/null
+++ b/src/main/resources/db/mysql/V1.6__user_tables.sql
@@ -0,0 +1,4 @@
+CREATE TABLE IF NOT EXISTS default_setting (
+    username VARCHAR(100) PRIMARY KEY,
+    settings TEXT NOT NULL
+);
\ No newline at end of file
diff --git a/src/main/resources/db/mysql/V1.7__query_references.sql b/src/main/resources/db/mysql/V1.7__query_references.sql
new file mode 100644
index 0000000..6d636da
--- /dev/null
+++ b/src/main/resources/db/mysql/V1.7__query_references.sql
@@ -0,0 +1,14 @@
+CREATE TABLE IF NOT EXISTS query_refernce (
+  id INTEGER PRIMARY KEY AUTO_INCREMENT,
+  name VARCHAR(255) NOT NULL,
+  type VARCHAR(100) NOT NULL,
+  required_access VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  description VARCHAR(255) DEFAULT NULL,
+  status VARCHAR(100) DEFAULT NULL,
+  query TEXT NOT NULL,
+  definition VARCHAR(255) DEFAULT NULL,
+  UNIQUE INDEX unique_index (name,created_by),
+  INDEX owner_index (created_by),
+  INDEX type_index (type)
+);
diff --git a/src/main/resources/db/mysql/V1__create_tables.sql b/src/main/resources/db/mysql/V1__create_tables.sql
new file mode 100644
index 0000000..b4572ec
--- /dev/null
+++ b/src/main/resources/db/mysql/V1__create_tables.sql
@@ -0,0 +1,72 @@
+
+CREATE TABLE IF NOT EXISTS annotation(
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	code VARCHAR(20) NOT NULL,
+	type VARCHAR(20) NOT NULL,
+	text VARCHAR(20) NULL,
+	description VARCHAR(100) NOT NULL,
+	de_description VARCHAR(100),
+	UNIQUE INDEX unique_index (code, type)
+);
+
+CREATE TABLE IF NOT EXISTS annotation_layer(
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	foundry_id INTEGER NOT NULL,
+	layer_id INTEGER NOT NULL,
+	description VARCHAR(300) NOT NULL,
+	UNIQUE INDEX unique_index (foundry_id, layer_id),
+	FOREIGN KEY (foundry_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+	
+);
+
+CREATE TABLE IF NOT EXISTS annotation_key(
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	layer_id INTEGER NOT NULL,
+	key_id INTEGER NOT NULL,
+	UNIQUE INDEX unique_index (layer_id, key_id),
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation_layer (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (key_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS annotation_value(
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	key_id INTEGER NOT NULL,
+	value_id INTEGER NOT NULL,
+	UNIQUE INDEX unique_index(key_id, value_id),
+	FOREIGN KEY (key_id)
+		REFERENCES annotation_key (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (value_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE TABLE resource(
+	id VARCHAR(100) PRIMARY KEY UNIQUE NOT NULL,
+	de_title VARCHAR(100) NOT NULL,
+	en_title VARCHAR(100) NOT NULL,
+	en_description VARCHAR(100)	
+);
+
+CREATE TABLE resource_layer(
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	resource_id VARCHAR(100) NOT NULL,
+	layer_id INTEGER NOT NULL,
+	UNIQUE INDEX pair_index (resource_id, layer_id),
+	FOREIGN KEY (resource_id)
+		REFERENCES resource (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation_layer (id)
+		ON DELETE CASCADE	
+);
+
diff --git a/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql b/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql
new file mode 100644
index 0000000..0c307cb
--- /dev/null
+++ b/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql
@@ -0,0 +1,28 @@
+-- roles
+INSERT INTO role(name) VALUES ("USER_GROUP_ADMIN");
+INSERT INTO role(name) VALUES ("USER_GROUP_MEMBER");
+INSERT INTO role(name) VALUES ("VC_ACCESS_ADMIN");
+INSERT INTO role(name) VALUES ("VC_ACCESS_MEMBER");
+INSERT INTO role(name) VALUES ("QUERY_ACCESS_ADMIN");
+INSERT INTO role(name) VALUES ("QUERY_ACCESS_MEMBER");
+
+-- privileges
+INSERT INTO privilege(name,role_id)
+	VALUES("READ", 1);
+INSERT INTO privilege(name,role_id)
+	VALUES("WRITE", 1);
+INSERT INTO privilege(name,role_id)
+	VALUES("DELETE", 1);
+	
+INSERT INTO privilege(name,role_id)
+	VALUES("DELETE",2);
+	
+INSERT INTO privilege(name,role_id)
+	VALUES("READ",3);
+INSERT INTO privilege(name,role_id)
+	VALUES("WRITE",3);
+INSERT INTO privilege(name,role_id)
+	VALUES("DELETE",3);
+
+INSERT INTO privilege(name,role_id)
+	VALUES("READ",4);	
\ No newline at end of file
diff --git a/src/main/resources/db/predefined/V2.2__annotation.sql b/src/main/resources/db/predefined/V2.2__annotation.sql
new file mode 100644
index 0000000..e81fd53
--- /dev/null
+++ b/src/main/resources/db/predefined/V2.2__annotation.sql
@@ -0,0 +1,391 @@
+INSERT INTO "annotation" VALUES(1,'opennlp','foundry',NULL,'OpenNLP',NULL);
+INSERT INTO "annotation" VALUES(2,'p','layer',NULL,'Part-of-Speech',NULL);
+INSERT INTO "annotation" VALUES(3,'ADJA','key','ADJA ','Attributive Adjective',NULL);
+INSERT INTO "annotation" VALUES(4,'ADJD','key','ADJD ','Predicative Adjective',NULL);
+INSERT INTO "annotation" VALUES(5,'ADV','key','ADV ','Adverb',NULL);
+INSERT INTO "annotation" VALUES(6,'APPO','key','APPO ','Postposition',NULL);
+INSERT INTO "annotation" VALUES(7,'APPR','key','APPR ','Preposition',NULL);
+INSERT INTO "annotation" VALUES(8,'APPRART','key','APPRART ','Preposition with Determiner',NULL);
+INSERT INTO "annotation" VALUES(9,'APZR','key','APZR ','Right Circumposition',NULL);
+INSERT INTO "annotation" VALUES(10,'ART','key','ART ','Determiner',NULL);
+INSERT INTO "annotation" VALUES(11,'CARD','key','CARD ','Cardinal Number',NULL);
+INSERT INTO "annotation" VALUES(12,'FM','key','FM ','Foreign Material',NULL);
+INSERT INTO "annotation" VALUES(13,'ITJ','key','ITJ ','Interjection',NULL);
+INSERT INTO "annotation" VALUES(14,'KOKOM','key','KOKOM ','Comparison Particle',NULL);
+INSERT INTO "annotation" VALUES(15,'KON','key','KON ','Coordinating Conjuncion',NULL);
+INSERT INTO "annotation" VALUES(16,'KOUI','key','KOUI ','Subordinating Conjunction with ''zu''',NULL);
+INSERT INTO "annotation" VALUES(17,'KOUS','key','KOUS ','Subordinating Conjunction with Sentence',NULL);
+INSERT INTO "annotation" VALUES(18,'NE','key','NE ','Named Entity',NULL);
+INSERT INTO "annotation" VALUES(19,'NN','key','NN ','Normal Nomina',NULL);
+INSERT INTO "annotation" VALUES(20,'PAV','key','PAV ','Pronominal Adverb',NULL);
+INSERT INTO "annotation" VALUES(21,'PDAT','key','PDAT ','Attributive Demonstrative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(22,'PDS','key','PDS ','Substitutive Demonstrative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(23,'PIAT','key','PIAT ','Attributive Indefinite Pronoun without Determiner',NULL);
+INSERT INTO "annotation" VALUES(24,'PIDAT','key','PIDAT ','Attributive Indefinite Pronoun with Determiner',NULL);
+INSERT INTO "annotation" VALUES(25,'PIS','key','PIS ','Substitutive Indefinite Pronoun',NULL);
+INSERT INTO "annotation" VALUES(26,'PPER','key','PPER ','Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(27,'PPOSAT','key','PPOSAT ','Attributive Possessive Pronoun',NULL);
+INSERT INTO "annotation" VALUES(28,'PPOSS','key','PPOSS ','Substitutive Possessive Pronoun',NULL);
+INSERT INTO "annotation" VALUES(29,'PRELAT','key','PRELAT ','Attributive Relative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(30,'PRELS','key','PRELS ','Substitutive Relative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(31,'PRF','key','PRF ','Reflexive Pronoun',NULL);
+INSERT INTO "annotation" VALUES(32,'PROAV','key','PROAV ','Pronominal Adverb',NULL);
+INSERT INTO "annotation" VALUES(33,'PTKA','key','PTKA ','Particle with Adjective',NULL);
+INSERT INTO "annotation" VALUES(34,'PTKANT','key','PTKANT ','Answering Particle',NULL);
+INSERT INTO "annotation" VALUES(35,'PTKNEG','key','PTKNEG ','Negation Particle',NULL);
+INSERT INTO "annotation" VALUES(36,'PTKVZ','key','PTKVZ ','Separated Verbal Particle',NULL);
+INSERT INTO "annotation" VALUES(37,'PTKZU','key','PTKZU ','''zu'' Particle',NULL);
+INSERT INTO "annotation" VALUES(38,'PWAT','key','PWAT ','Attributive Interrogative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(39,'PWAV','key','PWAV ','Adverbial Interrogative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(40,'PWS','key','PWS ','Substitutive Interrogative Pronoun',NULL);
+INSERT INTO "annotation" VALUES(41,'TRUNC','key','TRUNC ','Truncated',NULL);
+INSERT INTO "annotation" VALUES(42,'VAFIN','key','VAFIN ','Auxiliary Finite Verb',NULL);
+INSERT INTO "annotation" VALUES(43,'VAIMP','key','VAIMP ','Auxiliary Finite Imperative Verb',NULL);
+INSERT INTO "annotation" VALUES(44,'VAINF','key','VAINF ','Auxiliary Infinite Verb',NULL);
+INSERT INTO "annotation" VALUES(45,'VAPP','key','VAPP ','Auxiliary Perfect Participle',NULL);
+INSERT INTO "annotation" VALUES(46,'VMFIN','key','VMFIN ','Modal Finite Verb',NULL);
+INSERT INTO "annotation" VALUES(47,'VMINF','key','VMINF ','Modal Infinite Verb',NULL);
+INSERT INTO "annotation" VALUES(48,'VMPP','key','VMPP ','Modal Perfect Participle',NULL);
+INSERT INTO "annotation" VALUES(49,'VVFIN','key','VVFIN ','Finite Verb',NULL);
+INSERT INTO "annotation" VALUES(50,'VVIMP','key','VVIMP ','Finite Imperative Verb',NULL);
+INSERT INTO "annotation" VALUES(51,'VVINF','key','VVINF ','Infinite Verb',NULL);
+INSERT INTO "annotation" VALUES(52,'VVIZU','key','VVIZU ','Infinite Verb with ''zu''',NULL);
+INSERT INTO "annotation" VALUES(53,'VVPP','key','VVPP ','Perfect Participle',NULL);
+INSERT INTO "annotation" VALUES(54,'XY','key','XY ','Non-Word',NULL);
+INSERT INTO "annotation" VALUES(55,'mate','foundry',NULL,'Mate',NULL);
+INSERT INTO "annotation" VALUES(56,'l','layer',NULL,'Lemma',NULL);
+INSERT INTO "annotation" VALUES(57,'m','layer',NULL,'Morphology',NULL);
+INSERT INTO "annotation" VALUES(58,'<root-POS>','key','<root-POS>','Root Part of Speech',NULL);
+INSERT INTO "annotation" VALUES(59,'case','key',NULL,'Case',NULL);
+INSERT INTO "annotation" VALUES(60,'degree','key',NULL,'Degree',NULL);
+INSERT INTO "annotation" VALUES(61,'gender','key',NULL,'Gender',NULL);
+INSERT INTO "annotation" VALUES(62,'mood','key',NULL,'Mood',NULL);
+INSERT INTO "annotation" VALUES(63,'number','key',NULL,'Number',NULL);
+INSERT INTO "annotation" VALUES(64,'person','key',NULL,'Person',NULL);
+INSERT INTO "annotation" VALUES(65,'tense','key',NULL,'Tense',NULL);
+INSERT INTO "annotation" VALUES(66,'<no-type> ','key',NULL,'No type',NULL);
+INSERT INTO "annotation" VALUES(67,'acc','value','acc ','Accusative',NULL);
+INSERT INTO "annotation" VALUES(68,'dat','value','dat ','Dative',NULL);
+INSERT INTO "annotation" VALUES(69,'gen','value','gen ','Genitive',NULL);
+INSERT INTO "annotation" VALUES(70,'nom','value','nom ','Nominative',NULL);
+INSERT INTO "annotation" VALUES(71,'*','value','* ','Undefined',NULL);
+INSERT INTO "annotation" VALUES(72,'comp','value','comp ','Comparative',NULL);
+INSERT INTO "annotation" VALUES(73,'pos','value','pos ','Positive',NULL);
+INSERT INTO "annotation" VALUES(74,'sup','value','sup ','Superative',NULL);
+INSERT INTO "annotation" VALUES(75,'fem','value','fem ','Feminium',NULL);
+INSERT INTO "annotation" VALUES(76,'masc','value','masc ','Masculinum',NULL);
+INSERT INTO "annotation" VALUES(77,'neut','value','neut ','Neuter',NULL);
+INSERT INTO "annotation" VALUES(78,'imp','value','imp ','Imperative',NULL);
+INSERT INTO "annotation" VALUES(79,'ind','value','ind ','Indicative',NULL);
+INSERT INTO "annotation" VALUES(80,'subj','value','subj ','Subjunctive',NULL);
+INSERT INTO "annotation" VALUES(81,'pl','value','pl ','Plural',NULL);
+INSERT INTO "annotation" VALUES(82,'sg','value','sg ','Singular',NULL);
+INSERT INTO "annotation" VALUES(83,'1','value','1 ','First Person',NULL);
+INSERT INTO "annotation" VALUES(84,'2','value','2 ','Second Person',NULL);
+INSERT INTO "annotation" VALUES(85,'3','value','3 ','Third Person',NULL);
+INSERT INTO "annotation" VALUES(86,'past','value','past ','Past',NULL);
+INSERT INTO "annotation" VALUES(87,'pres','value','pres ','Present',NULL);
+INSERT INTO "annotation" VALUES(88,'malt','foundry',NULL,'Malt',NULL);
+INSERT INTO "annotation" VALUES(89,'d','layer',NULL,'Dependency',NULL);
+INSERT INTO "annotation" VALUES(90,'-PUNCT-','key','-PUNCT- ','',NULL);
+INSERT INTO "annotation" VALUES(91,'-UNKNOWN-','key','-UNKNOWN- ','',NULL);
+INSERT INTO "annotation" VALUES(92,'APP','key','APP ','',NULL);
+INSERT INTO "annotation" VALUES(93,'ATTR','key','ATTR ','',NULL);
+INSERT INTO "annotation" VALUES(94,'AUX','key','AUX ','',NULL);
+INSERT INTO "annotation" VALUES(95,'AVZ','key','AVZ ','',NULL);
+INSERT INTO "annotation" VALUES(96,'CJ','key','CJ ','',NULL);
+INSERT INTO "annotation" VALUES(97,'DET','key','DET ','',NULL);
+INSERT INTO "annotation" VALUES(98,'EXPL','key','EXPL ','',NULL);
+INSERT INTO "annotation" VALUES(99,'GMOD','key','GMOD ','',NULL);
+INSERT INTO "annotation" VALUES(100,'GRAD','key','GRAD ','',NULL);
+INSERT INTO "annotation" VALUES(101,'KOM','key','KOM ','',NULL);
+INSERT INTO "annotation" VALUES(102,'KONJ','key','KONJ ','',NULL);
+INSERT INTO "annotation" VALUES(103,'NEB','key','NEB ','',NULL);
+INSERT INTO "annotation" VALUES(104,'OBJA','key','OBJA ','',NULL);
+INSERT INTO "annotation" VALUES(105,'OBJC','key','OBJC ','',NULL);
+INSERT INTO "annotation" VALUES(106,'OBJD','key','OBJD ','',NULL);
+INSERT INTO "annotation" VALUES(107,'OBJG','key','OBJG ','',NULL);
+INSERT INTO "annotation" VALUES(108,'OBJI','key','OBJI ','',NULL);
+INSERT INTO "annotation" VALUES(109,'OBJP','key','OBJP ','',NULL);
+INSERT INTO "annotation" VALUES(110,'PAR','key','PAR ','',NULL);
+INSERT INTO "annotation" VALUES(111,'PART','key','PART ','',NULL);
+INSERT INTO "annotation" VALUES(112,'PN','key','PN ','',NULL);
+INSERT INTO "annotation" VALUES(113,'PP','key','PP ','',NULL);
+INSERT INTO "annotation" VALUES(114,'PRED','key','PRED ','',NULL);
+INSERT INTO "annotation" VALUES(115,'REL','key','REL ','',NULL);
+INSERT INTO "annotation" VALUES(116,'ROOT','key','ROOT ','',NULL);
+INSERT INTO "annotation" VALUES(117,'S','key','S ','',NULL);
+INSERT INTO "annotation" VALUES(118,'SUBJ','key','SUBJ ','',NULL);
+INSERT INTO "annotation" VALUES(119,'SUBJC','key','SUBJC ','',NULL);
+INSERT INTO "annotation" VALUES(120,'ZEIT','key','ZEIT ','',NULL);
+INSERT INTO "annotation" VALUES(121,'gmod-app','key','gmod-app ','',NULL);
+INSERT INTO "annotation" VALUES(122,'koord','key','koord ','',NULL);
+INSERT INTO "annotation" VALUES(123,'drukola','foundry',NULL,'DRuKoLa',NULL);
+INSERT INTO "annotation" VALUES(124,'ctag','key',NULL,'CTAG',NULL);
+INSERT INTO "annotation" VALUES(125,'A','value','a ','Adjective',NULL);
+INSERT INTO "annotation" VALUES(126,'Y','value','y ','Abbreviation',NULL);
+INSERT INTO "annotation" VALUES(127,'AN','value','an ','Adjective, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(128,'APRY','value','apry ','Adjective, Plural, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(129,'APN','value','apn ','Adjective, Plural, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(130,'APOY','value','apoy ','Adjective, Plural, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(131,'APON','value','apon ','Adjective, Plural, Oblique, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(132,'ASRY','value','asry ','Adjective, Singular, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(133,'ASN','value','asn ','Adjective, Singular, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(134,'ASOY','value','asoy ','Adjective, Singular, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(135,'ASON','value','ason ','Adjective, Singular, Oblique, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(136,'ASVY','value','asvy ','Adjective, Singular, Vocative, Definite',NULL);
+INSERT INTO "annotation" VALUES(137,'ASVN','value','asvn ','Adjective, Singular, Vocative, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(138,'R','value','r ','Adverb',NULL);
+INSERT INTO "annotation" VALUES(139,'RC','value','rc ','Adverb, Portmanteau',NULL);
+INSERT INTO "annotation" VALUES(140,'TS','value','ts ','Article, Definite or Possessive, Singular',NULL);
+INSERT INTO "annotation" VALUES(141,'TP','value','tp ','Article, Indefinite or Possessive, Plural',NULL);
+INSERT INTO "annotation" VALUES(142,'TPR','value','tpr ','Article, non-Possessive, Plural, Direct',NULL);
+INSERT INTO "annotation" VALUES(143,'TPO','value','tpo ','Article, non-Possessive, Plural, Oblique',NULL);
+INSERT INTO "annotation" VALUES(144,'TSR','value','tsr ','Article, non-Possessive, Singular, Direct',NULL);
+INSERT INTO "annotation" VALUES(145,'TSO','value','tso ','Article, non-Possessive, Singular, Oblique',NULL);
+INSERT INTO "annotation" VALUES(146,'NPRY','value','npry ','Common Noun, Plural, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(147,'NPN','value','npn ','Common Noun, Plural, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(148,'NPOY','value','npoy ','Common Noun, Plural, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(149,'NPVY','value','npvy ','Common Noun, Plural, Vocative, Definite',NULL);
+INSERT INTO "annotation" VALUES(150,'NN','value','nn ','Common Noun, singular',NULL);
+INSERT INTO "annotation" VALUES(151,'NSY','value','nsy ','Common Noun, Singular, Definite',NULL);
+INSERT INTO "annotation" VALUES(152,'NSRY','value','nsry ','Common Noun, Singular, Direct, Definite',NULL);
+INSERT INTO "annotation" VALUES(153,'NSRN','value','nsrn ','Common Noun, Singular, Direct, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(154,'NSN','value','nsn ','Common Noun, Singular, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(155,'NSOY','value','nsoy ','Common Noun, Singular, Oblique, Definite',NULL);
+INSERT INTO "annotation" VALUES(156,'NSON','value','nson ','Common Noun, Singular, Oblique, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(157,'NSVY','value','nsvy ','Common Noun, Singular, Vocative, Definite',NULL);
+INSERT INTO "annotation" VALUES(158,'NSVN','value','nsvn ','Common Noun, Singular, Vocative, Indefinite',NULL);
+INSERT INTO "annotation" VALUES(159,'CR','value','cr ','Conjunctio, portmanteau',NULL);
+INSERT INTO "annotation" VALUES(160,'C','value','c ','Conjunction',NULL);
+INSERT INTO "annotation" VALUES(161,'QF','value','qf ','Future Particle',NULL);
+INSERT INTO "annotation" VALUES(162,'QN','value','qn ','Infinitival Particle',NULL);
+INSERT INTO "annotation" VALUES(163,'I','value','i ','Interjection',NULL);
+INSERT INTO "annotation" VALUES(164,'QZ','value','qz ','Negative Particle',NULL);
+INSERT INTO "annotation" VALUES(165,'M','value','m ','Numeral',NULL);
+INSERT INTO "annotation" VALUES(166,'PP','value','pp ','Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(167,'PPP','value','ppp ','Personal Pronoun, Plural',NULL);
+INSERT INTO "annotation" VALUES(168,'PPPA','value','pppa ','Personal Pronoun, Plural, Acc.',NULL);
+INSERT INTO "annotation" VALUES(169,'PPPD','value','pppd ','Personal Pronoun, Plural, Dative',NULL);
+INSERT INTO "annotation" VALUES(170,'PPPR','value','pppr ','Personal Pronoun, Plural, Direct',NULL);
+INSERT INTO "annotation" VALUES(171,'PPPO','value','pppo ','Personal Pronoun, Plural, Oblique',NULL);
+INSERT INTO "annotation" VALUES(172,'PPS','value','pps ','Personal Pronoun, Singular',NULL);
+INSERT INTO "annotation" VALUES(173,'PPSA','value','ppsa ','Personal Pronoun, Singular, Accusative',NULL);
+INSERT INTO "annotation" VALUES(174,'PPSD','value','ppsd ','Personal Pronoun, Singular, Dative',NULL);
+INSERT INTO "annotation" VALUES(175,'PPSR','value','ppsr ','Personal Pronoun, Singular, Direct',NULL);
+INSERT INTO "annotation" VALUES(176,'PPSN','value','ppsn ','Personal Pronoun, Singular, Nominative',NULL);
+INSERT INTO "annotation" VALUES(177,'PPSO','value','ppso ','Personal Pronoun, Singular, Oblique',NULL);
+INSERT INTO "annotation" VALUES(178,'S','value','s ','Preposition',NULL);
+INSERT INTO "annotation" VALUES(179,'DMPR','value','dmpr ','Pronoun or Determiner, Demonstrative, Plural, Direct',NULL);
+INSERT INTO "annotation" VALUES(180,'DMPO','value','dmpo ','Pronoun or Determiner, Demonstrative, Plural, Oblique',NULL);
+INSERT INTO "annotation" VALUES(181,'DMSR','value','dmsr ','Pronoun or Determiner, Demonstrative, Singular, Direct',NULL);
+INSERT INTO "annotation" VALUES(182,'DMSO','value','dmso ','Pronoun or Determiner, Demonstrative, Singular, Oblique',NULL);
+INSERT INTO "annotation" VALUES(183,'PS','value','ps ','Pronoun or Determiner, Poss or Emph',NULL);
+INSERT INTO "annotation" VALUES(184,'PSS','value','pss ','Pronoun or Determiner, Poss or Emph, Singular',NULL);
+INSERT INTO "annotation" VALUES(185,'RELR','value','relr ','Pronoun or Determiner, Relative, Direct',NULL);
+INSERT INTO "annotation" VALUES(186,'RELO','value','relo ','Pronoun or Determiner, Relative, Oblique',NULL);
+INSERT INTO "annotation" VALUES(187,'NP','value','np ','Proper Noun',NULL);
+INSERT INTO "annotation" VALUES(188,'PI','value','pi ','Quantifier Pronoun or Determiner',NULL);
+INSERT INTO "annotation" VALUES(189,'PXA','value','pxa ','Reflexive Pronoun, Accusative',NULL);
+INSERT INTO "annotation" VALUES(190,'PXD','value','pxd ','Reflexive Pronoun, Dative',NULL);
+INSERT INTO "annotation" VALUES(191,'X','value','x ','Residual',NULL);
+INSERT INTO "annotation" VALUES(192,'QS','value','qs ','Subjunctive Particle',NULL);
+INSERT INTO "annotation" VALUES(193,'VA','value','va ','Verb, Auxiliary',NULL);
+INSERT INTO "annotation" VALUES(194,'VA1','value','va1 ','Verb, Auxiliary, 1st person',NULL);
+INSERT INTO "annotation" VALUES(195,'VA2P','value','va2p ','Verb, Auxiliary, 2nd person, Plural',NULL);
+INSERT INTO "annotation" VALUES(196,'VA2S','value','va2s ','Verb, Auxiliary, 2nd person, Singular',NULL);
+INSERT INTO "annotation" VALUES(197,'VA3','value','va3 ','Verb, Auxiliary, 3rd person',NULL);
+INSERT INTO "annotation" VALUES(198,'VA3P','value','va3p ','Verb, Auxiliary, 3rd person, Plural',NULL);
+INSERT INTO "annotation" VALUES(199,'VA3S','value','va3s ','Verb, Auxiliary, 3rd person, Singular',NULL);
+INSERT INTO "annotation" VALUES(200,'VA1P','value','va1p ','Verb, Auxiliary,1st person, Plural',NULL);
+INSERT INTO "annotation" VALUES(201,'VA1S','value','va1s ','Verb, Auxiliary,1st person, Singular',NULL);
+INSERT INTO "annotation" VALUES(202,'VG','value','vg ','Verb, Gerund',NULL);
+INSERT INTO "annotation" VALUES(203,'VN','value','vn ','Verb, Infinitive',NULL);
+INSERT INTO "annotation" VALUES(204,'V1','value','v1 ','Verb, Main, 1st person',NULL);
+INSERT INTO "annotation" VALUES(205,'V2','value','v2 ','Verb, Main, 2nd person',NULL);
+INSERT INTO "annotation" VALUES(206,'V3','value','v3 ','Verb, Main, 3rd person',NULL);
+INSERT INTO "annotation" VALUES(207,'VPPF','value','vppf ','Verb, Participle, Plural, Feminine',NULL);
+INSERT INTO "annotation" VALUES(208,'VPPM','value','vppm ','Verb, Participle, Plural, Masculine',NULL);
+INSERT INTO "annotation" VALUES(209,'VPSF','value','vpsf ','Verb, Participle, Singular, Feminine',NULL);
+INSERT INTO "annotation" VALUES(210,'VPSM','value','vpsm ','Verb, Participle, Singular, Masculine',NULL);
+INSERT INTO "annotation" VALUES(211,'cnx','foundry',NULL,'Connexor',NULL);
+INSERT INTO "annotation" VALUES(212,'c','layer',NULL,'Constituency',NULL);
+INSERT INTO "annotation" VALUES(213,'syn','layer',NULL,'Syntax',NULL);
+INSERT INTO "annotation" VALUES(214,'np','key','np ','Nominal Phrase',NULL);
+INSERT INTO "annotation" VALUES(215,'Abbr','key','Abbr ','Nouns: Abbreviation',NULL);
+INSERT INTO "annotation" VALUES(216,'CMP','key','CMP ','Adjective: Comparative',NULL);
+INSERT INTO "annotation" VALUES(217,'IMP','key','IMP ','Mood: Imperative',NULL);
+INSERT INTO "annotation" VALUES(218,'IND','key','IND ','Mood: Indicative',NULL);
+INSERT INTO "annotation" VALUES(219,'INF','key','INF ','Infinitive',NULL);
+INSERT INTO "annotation" VALUES(220,'ORD','key','ORD ','Numeral: Ordinal',NULL);
+INSERT INTO "annotation" VALUES(221,'PAST','key','PAST ','Tense: past',NULL);
+INSERT INTO "annotation" VALUES(222,'PCP','key','PCP ','Participle',NULL);
+INSERT INTO "annotation" VALUES(223,'PERF','key','PERF ','Perfective Participle',NULL);
+INSERT INTO "annotation" VALUES(224,'PL','key','PL ','Nouns: Plural',NULL);
+INSERT INTO "annotation" VALUES(225,'PRES','key','PRES ','Tense: present',NULL);
+INSERT INTO "annotation" VALUES(226,'PROG','key','PROG ','Progressive Participle',NULL);
+INSERT INTO "annotation" VALUES(227,'Prop','key','Prop ','Nouns: Proper Noun',NULL);
+INSERT INTO "annotation" VALUES(228,'SUB','key','SUB ','Mood: Subjunctive',NULL);
+INSERT INTO "annotation" VALUES(229,'SUP','key','SUP ','Adjective: Superlative',NULL);
+INSERT INTO "annotation" VALUES(230,'A','key','A ','Adjective',NULL);
+INSERT INTO "annotation" VALUES(231,'CC','key','CC ','Coordination Marker',NULL);
+INSERT INTO "annotation" VALUES(232,'CS','key','CS ','Clause Marker',NULL);
+INSERT INTO "annotation" VALUES(233,'INTERJ','key','INTERJ ','Interjection',NULL);
+INSERT INTO "annotation" VALUES(234,'N','key','N ','Noun',NULL);
+INSERT INTO "annotation" VALUES(235,'NUM','key','NUM ','Numeral',NULL);
+INSERT INTO "annotation" VALUES(236,'PREP','key','PREP ','Preposition',NULL);
+INSERT INTO "annotation" VALUES(237,'PRON','key','PRON ','Pro-Nominal',NULL);
+INSERT INTO "annotation" VALUES(238,'V','key','V ','Verb',NULL);
+INSERT INTO "annotation" VALUES(239,'@ADVL','key','@ADVL ','Adverbial Head',NULL);
+INSERT INTO "annotation" VALUES(240,'@AUX','key','@AUX ','Auxiliary Verb',NULL);
+INSERT INTO "annotation" VALUES(241,'@CC','key','@CC ','Coordination',NULL);
+INSERT INTO "annotation" VALUES(242,'@MAIN','key','@MAIN ','Main Verb',NULL);
+INSERT INTO "annotation" VALUES(243,'@NH','key','@NH ','Nominal Head',NULL);
+INSERT INTO "annotation" VALUES(244,'@POSTMOD','key','@POSTMOD ','Postmodifier',NULL);
+INSERT INTO "annotation" VALUES(245,'@PREMARK','key','@PREMARK ','Preposed Marker',NULL);
+INSERT INTO "annotation" VALUES(246,'@PREMOD','key','@PREMOD ','Premodifier',NULL);
+INSERT INTO "annotation" VALUES(247,'marmot','foundry',NULL,'MarMoT',NULL);
+INSERT INTO "annotation" VALUES(248,'base','foundry',NULL,'Base Annotation',NULL);
+INSERT INTO "annotation" VALUES(249,'s','layer',NULL,'Structure',NULL);
+INSERT INTO "annotation" VALUES(250,'s','key','s','Sentence',NULL);
+INSERT INTO "annotation" VALUES(251,'p','key','p','Paragraph',NULL);
+INSERT INTO "annotation" VALUES(252,'t','key','t','Text',NULL);
+INSERT INTO "annotation" VALUES(253,'xip','foundry',NULL,'Xerox Parser',NULL);
+INSERT INTO "annotation" VALUES(254,'lwc','foundry',NULL,'LWC',NULL);
+INSERT INTO "annotation" VALUES(255,'AC','key','AC ','adpositional case marker',NULL);
+INSERT INTO "annotation" VALUES(256,'ADC','key','ADC ','adjective component',NULL);
+INSERT INTO "annotation" VALUES(257,'AMS','key','AMS ','measure argument of adj',NULL);
+INSERT INTO "annotation" VALUES(258,'AVC','key','AVC ','adverbial phrase component',NULL);
+INSERT INTO "annotation" VALUES(259,'CD','key','CD ','coordinating conjunction',NULL);
+INSERT INTO "annotation" VALUES(260,'CM','key','CM ','comparative concjunction',NULL);
+INSERT INTO "annotation" VALUES(261,'CP','key','CP ','complementizer',NULL);
+INSERT INTO "annotation" VALUES(262,'DA','key','DA ','dative',NULL);
+INSERT INTO "annotation" VALUES(263,'DH','key','DH ','discourse-level head',NULL);
+INSERT INTO "annotation" VALUES(264,'DM','key','DM ','discourse marker',NULL);
+INSERT INTO "annotation" VALUES(265,'GL','key','GL ','prenominal genitive',NULL);
+INSERT INTO "annotation" VALUES(266,'GR','key','GR ','postnominal genitive',NULL);
+INSERT INTO "annotation" VALUES(267,'HD','key','HD ','head',NULL);
+INSERT INTO "annotation" VALUES(268,'JU','key','JU ','junctor',NULL);
+INSERT INTO "annotation" VALUES(269,'MC','key','MC ','comitative',NULL);
+INSERT INTO "annotation" VALUES(270,'MI','key','MI ','instrumental',NULL);
+INSERT INTO "annotation" VALUES(271,'ML','key','ML ','locative',NULL);
+INSERT INTO "annotation" VALUES(272,'MNR','key','MNR ','postnominal modifier',NULL);
+INSERT INTO "annotation" VALUES(273,'MO','key','MO ','modifier',NULL);
+INSERT INTO "annotation" VALUES(274,'MR','key','MR ','rhetorical modifier',NULL);
+INSERT INTO "annotation" VALUES(275,'MW','key','MW ','way (directional modifier)',NULL);
+INSERT INTO "annotation" VALUES(276,'NG','key','NG ','negation',NULL);
+INSERT INTO "annotation" VALUES(277,'NK','key','NK ','noun kernel modifier',NULL);
+INSERT INTO "annotation" VALUES(278,'NMC','key','NMC ','numerical component',NULL);
+INSERT INTO "annotation" VALUES(279,'OA','key','OA ','accusative object',NULL);
+INSERT INTO "annotation" VALUES(280,'OA2','key','OA2 ','second accusative object',NULL);
+INSERT INTO "annotation" VALUES(281,'OC','key','OC ','clausal object',NULL);
+INSERT INTO "annotation" VALUES(282,'OG','key','OG ','genitive object',NULL);
+INSERT INTO "annotation" VALUES(283,'PD','key','PD ','predicate',NULL);
+INSERT INTO "annotation" VALUES(284,'PG','key','PG ','pseudo-genitive',NULL);
+INSERT INTO "annotation" VALUES(285,'PH','key','PH ','placeholder',NULL);
+INSERT INTO "annotation" VALUES(286,'PM','key','PM ','morphological particle',NULL);
+INSERT INTO "annotation" VALUES(287,'PNC','key','PNC ','proper noun component',NULL);
+INSERT INTO "annotation" VALUES(288,'RC','key','RC ','relative clause',NULL);
+INSERT INTO "annotation" VALUES(289,'RE','key','RE ','repeated element',NULL);
+INSERT INTO "annotation" VALUES(290,'RS','key','RS ','reported speech',NULL);
+INSERT INTO "annotation" VALUES(291,'SB','key','SB ','subject',NULL);
+INSERT INTO "annotation" VALUES(292,'SBP','key','SBP ','passivised subject (PP)',NULL);
+INSERT INTO "annotation" VALUES(293,'SP','key','SP ','subject or predicate',NULL);
+INSERT INTO "annotation" VALUES(294,'SVP','key','SVP ','separable verb prefix',NULL);
+INSERT INTO "annotation" VALUES(295,'UC','key','UC ','(idiosyncratic) unit component',NULL);
+INSERT INTO "annotation" VALUES(296,'VO','key','VO ','vocative',NULL);
+INSERT INTO "annotation" VALUES(297,'dereko','foundry',NULL,'DeReKo',NULL);
+INSERT INTO "annotation" VALUES(298,'sgbr','foundry',NULL,'Schreibgebrauch',NULL);
+INSERT INTO "annotation" VALUES(299,'lv','layer',NULL,'Lemma Variants',NULL);
+INSERT INTO "annotation" VALUES(300,'NNE','key','NNE','Normal Nomina with Named Entity',NULL);
+INSERT INTO "annotation" VALUES(301,'ADVART','key','ADVART','Adverb with Article',NULL);
+INSERT INTO "annotation" VALUES(302,'EMOASC','key','EMOASC','ASCII emoticon',NULL);
+INSERT INTO "annotation" VALUES(303,'EMOIMG','key','EMOIMG','Graphic emoticon',NULL);
+INSERT INTO "annotation" VALUES(304,'ERRTOK','key','ERRTOK','Tokenisation Error',NULL);
+INSERT INTO "annotation" VALUES(305,'HST','key','HST','Hashtag',NULL);
+INSERT INTO "annotation" VALUES(306,'KOUSPPER','key','KOUSPPER','Subordinating Conjunction (with Sentence) with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(307,'ONO','key','ONO','Onomatopoeia',NULL);
+INSERT INTO "annotation" VALUES(308,'PPERPPER','key','PPERPPER','Personal Pronoun with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(309,'URL','key','URL','Uniform Resource Locator',NULL);
+INSERT INTO "annotation" VALUES(310,'VAPPER','key','VAPPER','Finite Auxiliary Verb with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(311,'VMPPER','key','VMPPER','Fintite Modal Verb with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(312,'VVPPER','key','VVPPER','Finite Full Verb with Personal Pronoun',NULL);
+INSERT INTO "annotation" VALUES(313,'AW','key','AW','Interaction Word',NULL);
+INSERT INTO "annotation" VALUES(314,'ADR','key','ADR','Addressing Term',NULL);
+INSERT INTO "annotation" VALUES(315,'AWIND','key','AWIND','Punctuation Indicating Addressing Term',NULL);
+INSERT INTO "annotation" VALUES(316,'ERRAW','key','ERRAW','Part of Erroneously Separated Compound',NULL);
+INSERT INTO "annotation" VALUES(317,'corenlp','foundry',NULL,'CoreNLP',NULL);
+INSERT INTO "annotation" VALUES(318,'ne','layer',NULL,'Named Entity',NULL);
+INSERT INTO "annotation" VALUES(319,'ne_dewac_175m_600','layer',NULL,'Named Entity',NULL);
+INSERT INTO "annotation" VALUES(320,'ne_hgc_175m_600','layer',NULL,'Named Entity',NULL);
+INSERT INTO "annotation" VALUES(321,'I-LOC','key','I-LOC ','Location',NULL);
+INSERT INTO "annotation" VALUES(322,'I-MISC','key','I-MISC ','Miscellaneous',NULL);
+INSERT INTO "annotation" VALUES(323,'I-ORG','key','I-ORG ','Organization',NULL);
+INSERT INTO "annotation" VALUES(324,'I-PER','key','I-PER ','Person',NULL);
+INSERT INTO "annotation" VALUES(325,'AA','key','AA','superlative phrase with ''am''',NULL);
+INSERT INTO "annotation" VALUES(326,'AP','key','AP','adjektive phrase',NULL);
+INSERT INTO "annotation" VALUES(327,'AVP','key','AVP','adverbial phrase',NULL);
+INSERT INTO "annotation" VALUES(328,'CAP','key','CAP','coordinated adjektive phrase',NULL);
+INSERT INTO "annotation" VALUES(329,'CAVP','key','CAVP','coordinated adverbial phrase',NULL);
+INSERT INTO "annotation" VALUES(330,'CAC','key','CAC','coordinated adposition',NULL);
+INSERT INTO "annotation" VALUES(331,'CCP','key','CCP','coordinated complementiser',NULL);
+INSERT INTO "annotation" VALUES(332,'CH','key','CH','chunk',NULL);
+INSERT INTO "annotation" VALUES(333,'CNP','key','CNP','coordinated noun phrase',NULL);
+INSERT INTO "annotation" VALUES(334,'CO','key','CO','coordination',NULL);
+INSERT INTO "annotation" VALUES(335,'CPP','key','CPP','coordinated adpositional phrase',NULL);
+INSERT INTO "annotation" VALUES(336,'CVP','key','CVP','coordinated verb phrase (non-finite)',NULL);
+INSERT INTO "annotation" VALUES(337,'CVZ','key','CVZ','coordinated zu-marked infinitive',NULL);
+INSERT INTO "annotation" VALUES(338,'DL','key','DL','discourse level constituent',NULL);
+INSERT INTO "annotation" VALUES(339,'ISU','key','ISU','idiosyncratis unit',NULL);
+INSERT INTO "annotation" VALUES(340,'MPN','key','MPN','multi-word proper noun',NULL);
+INSERT INTO "annotation" VALUES(341,'MTA','key','MTA','multi-token adjective',NULL);
+INSERT INTO "annotation" VALUES(342,'NM','key','NM','multi-token number',NULL);
+INSERT INTO "annotation" VALUES(343,'NP','key','NP','noun phrase',NULL);
+INSERT INTO "annotation" VALUES(344,'QL','key','QL','quasi-languag',NULL);
+INSERT INTO "annotation" VALUES(345,'VP','key','VP','verb phrase (non-finite)',NULL);
+INSERT INTO "annotation" VALUES(346,'VZ','key','VZ','zu-marked infinitive',NULL);
+INSERT INTO "annotation" VALUES(347,'AC','value','AC ','adpositional case marker',NULL);
+INSERT INTO "annotation" VALUES(348,'ADC','value','ADC ','adjective component',NULL);
+INSERT INTO "annotation" VALUES(349,'AMS','value','AMS ','measure argument of adj',NULL);
+INSERT INTO "annotation" VALUES(350,'APP','value','APP ','apposition',NULL);
+INSERT INTO "annotation" VALUES(351,'AVC','value','AVC ','adverbial phrase component',NULL);
+INSERT INTO "annotation" VALUES(352,'CC','value','CC ','comparative complement',NULL);
+INSERT INTO "annotation" VALUES(353,'CD','value','CD ','coordinating conjunction',NULL);
+INSERT INTO "annotation" VALUES(354,'CJ','value','CJ ','conjunct',NULL);
+INSERT INTO "annotation" VALUES(355,'CM','value','CM ','comparative concjunction',NULL);
+INSERT INTO "annotation" VALUES(356,'CP','value','CP ','complementizer',NULL);
+INSERT INTO "annotation" VALUES(357,'DA','value','DA ','dative',NULL);
+INSERT INTO "annotation" VALUES(358,'DH','value','DH ','discourse-level head',NULL);
+INSERT INTO "annotation" VALUES(359,'DM','value','DM ','discourse marker',NULL);
+INSERT INTO "annotation" VALUES(360,'GL','value','GL ','prenominal genitive',NULL);
+INSERT INTO "annotation" VALUES(361,'GR','value','GR ','postnominal genitive',NULL);
+INSERT INTO "annotation" VALUES(362,'HD','value','HD ','head',NULL);
+INSERT INTO "annotation" VALUES(363,'JU','value','JU ','junctor',NULL);
+INSERT INTO "annotation" VALUES(364,'MC','value','MC ','comitative',NULL);
+INSERT INTO "annotation" VALUES(365,'MI','value','MI ','instrumental',NULL);
+INSERT INTO "annotation" VALUES(366,'ML','value','ML ','locative',NULL);
+INSERT INTO "annotation" VALUES(367,'MNR','value','MNR ','postnominal modifier',NULL);
+INSERT INTO "annotation" VALUES(368,'MO','value','MO ','modifier',NULL);
+INSERT INTO "annotation" VALUES(369,'MR','value','MR ','rhetorical modifier',NULL);
+INSERT INTO "annotation" VALUES(370,'MW','value','MW ','way (directional modifier)',NULL);
+INSERT INTO "annotation" VALUES(371,'NG','value','NG ','negation',NULL);
+INSERT INTO "annotation" VALUES(372,'NK','value','NK ','noun kernel modifier',NULL);
+INSERT INTO "annotation" VALUES(373,'NMC','value','NMC ','numerical component',NULL);
+INSERT INTO "annotation" VALUES(374,'OA','value','OA ','accusative object',NULL);
+INSERT INTO "annotation" VALUES(375,'OA2','value','OA2 ','second accusative object',NULL);
+INSERT INTO "annotation" VALUES(376,'OC','value','OC ','clausal object',NULL);
+INSERT INTO "annotation" VALUES(377,'OG','value','OG ','genitive object',NULL);
+INSERT INTO "annotation" VALUES(378,'PD','value','PD ','predicate',NULL);
+INSERT INTO "annotation" VALUES(379,'PG','value','PG ','pseudo-genitive',NULL);
+INSERT INTO "annotation" VALUES(380,'PH','value','PH ','placeholder',NULL);
+INSERT INTO "annotation" VALUES(381,'PM','value','PM ','morphological particle',NULL);
+INSERT INTO "annotation" VALUES(382,'PNC','value','PNC ','proper noun component',NULL);
+INSERT INTO "annotation" VALUES(383,'RE','value','RE ','repeated element',NULL);
+INSERT INTO "annotation" VALUES(384,'RS','value','RS ','reported speech',NULL);
+INSERT INTO "annotation" VALUES(385,'SB','value','SB ','subject',NULL);
+INSERT INTO "annotation" VALUES(386,'SBP','value','SBP ','passivised subject (PP)',NULL);
+INSERT INTO "annotation" VALUES(387,'SP','value','SP ','subject or predicate',NULL);
+INSERT INTO "annotation" VALUES(388,'SVP','value','SVP ','separable verb prefix',NULL);
+INSERT INTO "annotation" VALUES(389,'UC','value','UC ','(idiosyncratic) unit component',NULL);
+INSERT INTO "annotation" VALUES(390,'VO','value','VO ','vocative',NULL);
+INSERT INTO "annotation" VALUES(391,'tt','foundry',NULL,'TreeTagger',NULL);
diff --git a/src/main/resources/db/predefined/V2.3__annotation_layer.sql b/src/main/resources/db/predefined/V2.3__annotation_layer.sql
new file mode 100644
index 0000000..03ae9fc
--- /dev/null
+++ b/src/main/resources/db/predefined/V2.3__annotation_layer.sql
@@ -0,0 +1,31 @@
+INSERT INTO "annotation_layer" VALUES(1,1,2,'OpenNLP Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(2,55,56,'Mate Lemma');
+INSERT INTO "annotation_layer" VALUES(3,55,57,'Mate Morphology');
+INSERT INTO "annotation_layer" VALUES(4,55,2,'Mate Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(5,88,89,'Malt Dependency');
+INSERT INTO "annotation_layer" VALUES(6,123,56,'DRuKoLa Lemma');
+INSERT INTO "annotation_layer" VALUES(7,123,57,'DRuKoLa Morphology');
+INSERT INTO "annotation_layer" VALUES(8,123,2,'DRuKoLa Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(9,211,212,'Connexor Constituency');
+INSERT INTO "annotation_layer" VALUES(10,211,56,'Connexor Lemma');
+INSERT INTO "annotation_layer" VALUES(11,211,57,'Connexor Morphology');
+INSERT INTO "annotation_layer" VALUES(12,211,2,'Connexor Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(13,211,213,'Connexor Syntax');
+INSERT INTO "annotation_layer" VALUES(14,247,57,'MarMoT Morphology');
+INSERT INTO "annotation_layer" VALUES(15,247,2,'MarMoT Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(16,248,249,'Base Annotation Structure');
+INSERT INTO "annotation_layer" VALUES(17,253,212,'Xerox Parser Constituency');
+INSERT INTO "annotation_layer" VALUES(18,253,56,'Xerox Parser Lemma');
+INSERT INTO "annotation_layer" VALUES(19,253,2,'Xerox Parser Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(20,254,89,'LWC Dependency');
+INSERT INTO "annotation_layer" VALUES(21,297,249,'DeReKo Structure');
+INSERT INTO "annotation_layer" VALUES(22,298,56,'Schreibgebrauch Lemma');
+INSERT INTO "annotation_layer" VALUES(23,298,299,'Schreibgebrauch Lemma Variants');
+INSERT INTO "annotation_layer" VALUES(24,298,2,'Schreibgebrauch Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(25,317,212,'CoreNLP Constituency');
+INSERT INTO "annotation_layer" VALUES(26,317,318,'CoreNLP Named Entity');
+INSERT INTO "annotation_layer" VALUES(27,317,319,'CoreNLP Named Entity');
+INSERT INTO "annotation_layer" VALUES(28,317,320,'CoreNLP Named Entity');
+INSERT INTO "annotation_layer" VALUES(29,317,2,'CoreNLP Part-of-Speech');
+INSERT INTO "annotation_layer" VALUES(30,391,56,'TreeTagger Lemma');
+INSERT INTO "annotation_layer" VALUES(31,391,2,'TreeTagger Part-of-Speech');
diff --git a/src/main/resources/db/predefined/V2.4__annotation_key.sql b/src/main/resources/db/predefined/V2.4__annotation_key.sql
new file mode 100644
index 0000000..f713062
--- /dev/null
+++ b/src/main/resources/db/predefined/V2.4__annotation_key.sql
@@ -0,0 +1,503 @@
+INSERT INTO "annotation_key" VALUES(1,1,3);
+INSERT INTO "annotation_key" VALUES(2,1,4);
+INSERT INTO "annotation_key" VALUES(3,1,5);
+INSERT INTO "annotation_key" VALUES(4,1,6);
+INSERT INTO "annotation_key" VALUES(5,1,7);
+INSERT INTO "annotation_key" VALUES(6,1,8);
+INSERT INTO "annotation_key" VALUES(7,1,9);
+INSERT INTO "annotation_key" VALUES(8,1,10);
+INSERT INTO "annotation_key" VALUES(9,1,11);
+INSERT INTO "annotation_key" VALUES(10,1,12);
+INSERT INTO "annotation_key" VALUES(11,1,13);
+INSERT INTO "annotation_key" VALUES(12,1,14);
+INSERT INTO "annotation_key" VALUES(13,1,15);
+INSERT INTO "annotation_key" VALUES(14,1,16);
+INSERT INTO "annotation_key" VALUES(15,1,17);
+INSERT INTO "annotation_key" VALUES(16,1,18);
+INSERT INTO "annotation_key" VALUES(17,1,19);
+INSERT INTO "annotation_key" VALUES(18,1,20);
+INSERT INTO "annotation_key" VALUES(19,1,21);
+INSERT INTO "annotation_key" VALUES(20,1,22);
+INSERT INTO "annotation_key" VALUES(21,1,23);
+INSERT INTO "annotation_key" VALUES(22,1,24);
+INSERT INTO "annotation_key" VALUES(23,1,25);
+INSERT INTO "annotation_key" VALUES(24,1,26);
+INSERT INTO "annotation_key" VALUES(25,1,27);
+INSERT INTO "annotation_key" VALUES(26,1,28);
+INSERT INTO "annotation_key" VALUES(27,1,29);
+INSERT INTO "annotation_key" VALUES(28,1,30);
+INSERT INTO "annotation_key" VALUES(29,1,31);
+INSERT INTO "annotation_key" VALUES(30,1,32);
+INSERT INTO "annotation_key" VALUES(31,1,33);
+INSERT INTO "annotation_key" VALUES(32,1,34);
+INSERT INTO "annotation_key" VALUES(33,1,35);
+INSERT INTO "annotation_key" VALUES(34,1,36);
+INSERT INTO "annotation_key" VALUES(35,1,37);
+INSERT INTO "annotation_key" VALUES(36,1,38);
+INSERT INTO "annotation_key" VALUES(37,1,39);
+INSERT INTO "annotation_key" VALUES(38,1,40);
+INSERT INTO "annotation_key" VALUES(39,1,41);
+INSERT INTO "annotation_key" VALUES(40,1,42);
+INSERT INTO "annotation_key" VALUES(41,1,43);
+INSERT INTO "annotation_key" VALUES(42,1,44);
+INSERT INTO "annotation_key" VALUES(43,1,45);
+INSERT INTO "annotation_key" VALUES(44,1,46);
+INSERT INTO "annotation_key" VALUES(45,1,47);
+INSERT INTO "annotation_key" VALUES(46,1,48);
+INSERT INTO "annotation_key" VALUES(47,1,49);
+INSERT INTO "annotation_key" VALUES(48,1,50);
+INSERT INTO "annotation_key" VALUES(49,1,51);
+INSERT INTO "annotation_key" VALUES(50,1,52);
+INSERT INTO "annotation_key" VALUES(51,1,53);
+INSERT INTO "annotation_key" VALUES(52,1,54);
+INSERT INTO "annotation_key" VALUES(53,4,3);
+INSERT INTO "annotation_key" VALUES(54,4,4);
+INSERT INTO "annotation_key" VALUES(55,4,5);
+INSERT INTO "annotation_key" VALUES(56,4,6);
+INSERT INTO "annotation_key" VALUES(57,4,7);
+INSERT INTO "annotation_key" VALUES(58,4,8);
+INSERT INTO "annotation_key" VALUES(59,4,9);
+INSERT INTO "annotation_key" VALUES(60,4,10);
+INSERT INTO "annotation_key" VALUES(61,4,11);
+INSERT INTO "annotation_key" VALUES(62,4,12);
+INSERT INTO "annotation_key" VALUES(63,4,13);
+INSERT INTO "annotation_key" VALUES(64,4,14);
+INSERT INTO "annotation_key" VALUES(65,4,15);
+INSERT INTO "annotation_key" VALUES(66,4,16);
+INSERT INTO "annotation_key" VALUES(67,4,17);
+INSERT INTO "annotation_key" VALUES(68,4,18);
+INSERT INTO "annotation_key" VALUES(69,4,19);
+INSERT INTO "annotation_key" VALUES(70,4,20);
+INSERT INTO "annotation_key" VALUES(71,4,21);
+INSERT INTO "annotation_key" VALUES(72,4,22);
+INSERT INTO "annotation_key" VALUES(73,4,23);
+INSERT INTO "annotation_key" VALUES(74,4,24);
+INSERT INTO "annotation_key" VALUES(75,4,25);
+INSERT INTO "annotation_key" VALUES(76,4,26);
+INSERT INTO "annotation_key" VALUES(77,4,27);
+INSERT INTO "annotation_key" VALUES(78,4,28);
+INSERT INTO "annotation_key" VALUES(79,4,29);
+INSERT INTO "annotation_key" VALUES(80,4,30);
+INSERT INTO "annotation_key" VALUES(81,4,31);
+INSERT INTO "annotation_key" VALUES(82,4,32);
+INSERT INTO "annotation_key" VALUES(83,4,33);
+INSERT INTO "annotation_key" VALUES(84,4,34);
+INSERT INTO "annotation_key" VALUES(85,4,35);
+INSERT INTO "annotation_key" VALUES(86,4,36);
+INSERT INTO "annotation_key" VALUES(87,4,37);
+INSERT INTO "annotation_key" VALUES(88,4,38);
+INSERT INTO "annotation_key" VALUES(89,4,39);
+INSERT INTO "annotation_key" VALUES(90,4,40);
+INSERT INTO "annotation_key" VALUES(91,4,41);
+INSERT INTO "annotation_key" VALUES(92,4,42);
+INSERT INTO "annotation_key" VALUES(93,4,43);
+INSERT INTO "annotation_key" VALUES(94,4,44);
+INSERT INTO "annotation_key" VALUES(95,4,45);
+INSERT INTO "annotation_key" VALUES(96,4,46);
+INSERT INTO "annotation_key" VALUES(97,4,47);
+INSERT INTO "annotation_key" VALUES(98,4,48);
+INSERT INTO "annotation_key" VALUES(99,4,49);
+INSERT INTO "annotation_key" VALUES(100,4,50);
+INSERT INTO "annotation_key" VALUES(101,4,51);
+INSERT INTO "annotation_key" VALUES(102,4,52);
+INSERT INTO "annotation_key" VALUES(103,4,53);
+INSERT INTO "annotation_key" VALUES(104,4,54);
+INSERT INTO "annotation_key" VALUES(105,4,58);
+INSERT INTO "annotation_key" VALUES(106,3,59);
+INSERT INTO "annotation_key" VALUES(107,3,60);
+INSERT INTO "annotation_key" VALUES(108,3,61);
+INSERT INTO "annotation_key" VALUES(109,3,62);
+INSERT INTO "annotation_key" VALUES(110,3,63);
+INSERT INTO "annotation_key" VALUES(111,3,64);
+INSERT INTO "annotation_key" VALUES(112,3,65);
+INSERT INTO "annotation_key" VALUES(113,3,66);
+INSERT INTO "annotation_key" VALUES(114,5,90);
+INSERT INTO "annotation_key" VALUES(115,5,91);
+INSERT INTO "annotation_key" VALUES(116,5,5);
+INSERT INTO "annotation_key" VALUES(117,5,92);
+INSERT INTO "annotation_key" VALUES(118,5,93);
+INSERT INTO "annotation_key" VALUES(119,5,94);
+INSERT INTO "annotation_key" VALUES(120,5,95);
+INSERT INTO "annotation_key" VALUES(121,5,96);
+INSERT INTO "annotation_key" VALUES(122,5,97);
+INSERT INTO "annotation_key" VALUES(123,5,98);
+INSERT INTO "annotation_key" VALUES(124,5,99);
+INSERT INTO "annotation_key" VALUES(125,5,100);
+INSERT INTO "annotation_key" VALUES(126,5,101);
+INSERT INTO "annotation_key" VALUES(127,5,15);
+INSERT INTO "annotation_key" VALUES(128,5,102);
+INSERT INTO "annotation_key" VALUES(129,5,103);
+INSERT INTO "annotation_key" VALUES(130,5,104);
+INSERT INTO "annotation_key" VALUES(131,5,105);
+INSERT INTO "annotation_key" VALUES(132,5,106);
+INSERT INTO "annotation_key" VALUES(133,5,107);
+INSERT INTO "annotation_key" VALUES(134,5,108);
+INSERT INTO "annotation_key" VALUES(135,5,109);
+INSERT INTO "annotation_key" VALUES(136,5,110);
+INSERT INTO "annotation_key" VALUES(137,5,111);
+INSERT INTO "annotation_key" VALUES(138,5,112);
+INSERT INTO "annotation_key" VALUES(139,5,113);
+INSERT INTO "annotation_key" VALUES(140,5,114);
+INSERT INTO "annotation_key" VALUES(141,5,115);
+INSERT INTO "annotation_key" VALUES(142,5,116);
+INSERT INTO "annotation_key" VALUES(143,5,117);
+INSERT INTO "annotation_key" VALUES(144,5,118);
+INSERT INTO "annotation_key" VALUES(145,5,119);
+INSERT INTO "annotation_key" VALUES(146,5,120);
+INSERT INTO "annotation_key" VALUES(147,5,121);
+INSERT INTO "annotation_key" VALUES(148,5,122);
+INSERT INTO "annotation_key" VALUES(149,7,124);
+INSERT INTO "annotation_key" VALUES(150,9,214);
+INSERT INTO "annotation_key" VALUES(151,11,215);
+INSERT INTO "annotation_key" VALUES(152,11,216);
+INSERT INTO "annotation_key" VALUES(153,11,217);
+INSERT INTO "annotation_key" VALUES(154,11,218);
+INSERT INTO "annotation_key" VALUES(155,11,219);
+INSERT INTO "annotation_key" VALUES(156,11,220);
+INSERT INTO "annotation_key" VALUES(157,11,221);
+INSERT INTO "annotation_key" VALUES(158,11,222);
+INSERT INTO "annotation_key" VALUES(159,11,223);
+INSERT INTO "annotation_key" VALUES(160,11,224);
+INSERT INTO "annotation_key" VALUES(161,11,225);
+INSERT INTO "annotation_key" VALUES(162,11,226);
+INSERT INTO "annotation_key" VALUES(163,11,227);
+INSERT INTO "annotation_key" VALUES(164,11,228);
+INSERT INTO "annotation_key" VALUES(165,11,229);
+INSERT INTO "annotation_key" VALUES(166,12,230);
+INSERT INTO "annotation_key" VALUES(167,12,5);
+INSERT INTO "annotation_key" VALUES(168,12,231);
+INSERT INTO "annotation_key" VALUES(169,12,232);
+INSERT INTO "annotation_key" VALUES(170,12,97);
+INSERT INTO "annotation_key" VALUES(171,12,233);
+INSERT INTO "annotation_key" VALUES(172,12,234);
+INSERT INTO "annotation_key" VALUES(173,12,235);
+INSERT INTO "annotation_key" VALUES(174,12,236);
+INSERT INTO "annotation_key" VALUES(175,12,237);
+INSERT INTO "annotation_key" VALUES(176,12,238);
+INSERT INTO "annotation_key" VALUES(177,13,239);
+INSERT INTO "annotation_key" VALUES(178,13,240);
+INSERT INTO "annotation_key" VALUES(179,13,241);
+INSERT INTO "annotation_key" VALUES(180,13,242);
+INSERT INTO "annotation_key" VALUES(181,13,243);
+INSERT INTO "annotation_key" VALUES(182,13,244);
+INSERT INTO "annotation_key" VALUES(183,13,245);
+INSERT INTO "annotation_key" VALUES(184,13,246);
+INSERT INTO "annotation_key" VALUES(185,15,3);
+INSERT INTO "annotation_key" VALUES(186,15,4);
+INSERT INTO "annotation_key" VALUES(187,15,5);
+INSERT INTO "annotation_key" VALUES(188,15,6);
+INSERT INTO "annotation_key" VALUES(189,15,7);
+INSERT INTO "annotation_key" VALUES(190,15,8);
+INSERT INTO "annotation_key" VALUES(191,15,9);
+INSERT INTO "annotation_key" VALUES(192,15,10);
+INSERT INTO "annotation_key" VALUES(193,15,11);
+INSERT INTO "annotation_key" VALUES(194,15,12);
+INSERT INTO "annotation_key" VALUES(195,15,13);
+INSERT INTO "annotation_key" VALUES(196,15,14);
+INSERT INTO "annotation_key" VALUES(197,15,15);
+INSERT INTO "annotation_key" VALUES(198,15,16);
+INSERT INTO "annotation_key" VALUES(199,15,17);
+INSERT INTO "annotation_key" VALUES(200,15,18);
+INSERT INTO "annotation_key" VALUES(201,15,19);
+INSERT INTO "annotation_key" VALUES(202,15,20);
+INSERT INTO "annotation_key" VALUES(203,15,21);
+INSERT INTO "annotation_key" VALUES(204,15,22);
+INSERT INTO "annotation_key" VALUES(205,15,23);
+INSERT INTO "annotation_key" VALUES(206,15,24);
+INSERT INTO "annotation_key" VALUES(207,15,25);
+INSERT INTO "annotation_key" VALUES(208,15,26);
+INSERT INTO "annotation_key" VALUES(209,15,27);
+INSERT INTO "annotation_key" VALUES(210,15,28);
+INSERT INTO "annotation_key" VALUES(211,15,29);
+INSERT INTO "annotation_key" VALUES(212,15,30);
+INSERT INTO "annotation_key" VALUES(213,15,31);
+INSERT INTO "annotation_key" VALUES(214,15,32);
+INSERT INTO "annotation_key" VALUES(215,15,33);
+INSERT INTO "annotation_key" VALUES(216,15,34);
+INSERT INTO "annotation_key" VALUES(217,15,35);
+INSERT INTO "annotation_key" VALUES(218,15,36);
+INSERT INTO "annotation_key" VALUES(219,15,37);
+INSERT INTO "annotation_key" VALUES(220,15,38);
+INSERT INTO "annotation_key" VALUES(221,15,39);
+INSERT INTO "annotation_key" VALUES(222,15,40);
+INSERT INTO "annotation_key" VALUES(223,15,41);
+INSERT INTO "annotation_key" VALUES(224,15,42);
+INSERT INTO "annotation_key" VALUES(225,15,43);
+INSERT INTO "annotation_key" VALUES(226,15,44);
+INSERT INTO "annotation_key" VALUES(227,15,45);
+INSERT INTO "annotation_key" VALUES(228,15,46);
+INSERT INTO "annotation_key" VALUES(229,15,47);
+INSERT INTO "annotation_key" VALUES(230,15,48);
+INSERT INTO "annotation_key" VALUES(231,15,49);
+INSERT INTO "annotation_key" VALUES(232,15,50);
+INSERT INTO "annotation_key" VALUES(233,15,51);
+INSERT INTO "annotation_key" VALUES(234,15,52);
+INSERT INTO "annotation_key" VALUES(235,15,53);
+INSERT INTO "annotation_key" VALUES(236,15,54);
+INSERT INTO "annotation_key" VALUES(237,14,59);
+INSERT INTO "annotation_key" VALUES(238,14,60);
+INSERT INTO "annotation_key" VALUES(239,14,61);
+INSERT INTO "annotation_key" VALUES(240,14,62);
+INSERT INTO "annotation_key" VALUES(241,14,63);
+INSERT INTO "annotation_key" VALUES(242,14,64);
+INSERT INTO "annotation_key" VALUES(243,14,65);
+INSERT INTO "annotation_key" VALUES(244,14,66);
+INSERT INTO "annotation_key" VALUES(245,16,250);
+INSERT INTO "annotation_key" VALUES(246,16,251);
+INSERT INTO "annotation_key" VALUES(247,16,252);
+INSERT INTO "annotation_key" VALUES(248,20,255);
+INSERT INTO "annotation_key" VALUES(249,20,256);
+INSERT INTO "annotation_key" VALUES(250,20,257);
+INSERT INTO "annotation_key" VALUES(251,20,92);
+INSERT INTO "annotation_key" VALUES(252,20,258);
+INSERT INTO "annotation_key" VALUES(253,20,231);
+INSERT INTO "annotation_key" VALUES(254,20,259);
+INSERT INTO "annotation_key" VALUES(255,20,96);
+INSERT INTO "annotation_key" VALUES(256,20,260);
+INSERT INTO "annotation_key" VALUES(257,20,261);
+INSERT INTO "annotation_key" VALUES(258,20,262);
+INSERT INTO "annotation_key" VALUES(259,20,263);
+INSERT INTO "annotation_key" VALUES(260,20,264);
+INSERT INTO "annotation_key" VALUES(261,20,265);
+INSERT INTO "annotation_key" VALUES(262,20,266);
+INSERT INTO "annotation_key" VALUES(263,20,267);
+INSERT INTO "annotation_key" VALUES(264,20,268);
+INSERT INTO "annotation_key" VALUES(265,20,269);
+INSERT INTO "annotation_key" VALUES(266,20,270);
+INSERT INTO "annotation_key" VALUES(267,20,271);
+INSERT INTO "annotation_key" VALUES(268,20,272);
+INSERT INTO "annotation_key" VALUES(269,20,273);
+INSERT INTO "annotation_key" VALUES(270,20,274);
+INSERT INTO "annotation_key" VALUES(271,20,275);
+INSERT INTO "annotation_key" VALUES(272,20,276);
+INSERT INTO "annotation_key" VALUES(273,20,277);
+INSERT INTO "annotation_key" VALUES(274,20,278);
+INSERT INTO "annotation_key" VALUES(275,20,279);
+INSERT INTO "annotation_key" VALUES(276,20,280);
+INSERT INTO "annotation_key" VALUES(277,20,281);
+INSERT INTO "annotation_key" VALUES(278,20,282);
+INSERT INTO "annotation_key" VALUES(279,20,283);
+INSERT INTO "annotation_key" VALUES(280,20,284);
+INSERT INTO "annotation_key" VALUES(281,20,285);
+INSERT INTO "annotation_key" VALUES(282,20,286);
+INSERT INTO "annotation_key" VALUES(283,20,287);
+INSERT INTO "annotation_key" VALUES(284,20,288);
+INSERT INTO "annotation_key" VALUES(285,20,289);
+INSERT INTO "annotation_key" VALUES(286,20,290);
+INSERT INTO "annotation_key" VALUES(287,20,291);
+INSERT INTO "annotation_key" VALUES(288,20,292);
+INSERT INTO "annotation_key" VALUES(289,20,293);
+INSERT INTO "annotation_key" VALUES(290,20,294);
+INSERT INTO "annotation_key" VALUES(291,20,295);
+INSERT INTO "annotation_key" VALUES(292,20,296);
+INSERT INTO "annotation_key" VALUES(293,24,3);
+INSERT INTO "annotation_key" VALUES(294,24,4);
+INSERT INTO "annotation_key" VALUES(295,24,5);
+INSERT INTO "annotation_key" VALUES(296,24,6);
+INSERT INTO "annotation_key" VALUES(297,24,7);
+INSERT INTO "annotation_key" VALUES(298,24,8);
+INSERT INTO "annotation_key" VALUES(299,24,9);
+INSERT INTO "annotation_key" VALUES(300,24,10);
+INSERT INTO "annotation_key" VALUES(301,24,11);
+INSERT INTO "annotation_key" VALUES(302,24,12);
+INSERT INTO "annotation_key" VALUES(303,24,13);
+INSERT INTO "annotation_key" VALUES(304,24,14);
+INSERT INTO "annotation_key" VALUES(305,24,15);
+INSERT INTO "annotation_key" VALUES(306,24,16);
+INSERT INTO "annotation_key" VALUES(307,24,17);
+INSERT INTO "annotation_key" VALUES(308,24,18);
+INSERT INTO "annotation_key" VALUES(309,24,19);
+INSERT INTO "annotation_key" VALUES(310,24,20);
+INSERT INTO "annotation_key" VALUES(311,24,21);
+INSERT INTO "annotation_key" VALUES(312,24,22);
+INSERT INTO "annotation_key" VALUES(313,24,23);
+INSERT INTO "annotation_key" VALUES(314,24,24);
+INSERT INTO "annotation_key" VALUES(315,24,25);
+INSERT INTO "annotation_key" VALUES(316,24,26);
+INSERT INTO "annotation_key" VALUES(317,24,27);
+INSERT INTO "annotation_key" VALUES(318,24,28);
+INSERT INTO "annotation_key" VALUES(319,24,29);
+INSERT INTO "annotation_key" VALUES(320,24,30);
+INSERT INTO "annotation_key" VALUES(321,24,31);
+INSERT INTO "annotation_key" VALUES(322,24,32);
+INSERT INTO "annotation_key" VALUES(323,24,33);
+INSERT INTO "annotation_key" VALUES(324,24,34);
+INSERT INTO "annotation_key" VALUES(325,24,35);
+INSERT INTO "annotation_key" VALUES(326,24,36);
+INSERT INTO "annotation_key" VALUES(327,24,37);
+INSERT INTO "annotation_key" VALUES(328,24,38);
+INSERT INTO "annotation_key" VALUES(329,24,39);
+INSERT INTO "annotation_key" VALUES(330,24,40);
+INSERT INTO "annotation_key" VALUES(331,24,41);
+INSERT INTO "annotation_key" VALUES(332,24,42);
+INSERT INTO "annotation_key" VALUES(333,24,43);
+INSERT INTO "annotation_key" VALUES(334,24,44);
+INSERT INTO "annotation_key" VALUES(335,24,45);
+INSERT INTO "annotation_key" VALUES(336,24,46);
+INSERT INTO "annotation_key" VALUES(337,24,47);
+INSERT INTO "annotation_key" VALUES(338,24,48);
+INSERT INTO "annotation_key" VALUES(339,24,49);
+INSERT INTO "annotation_key" VALUES(340,24,50);
+INSERT INTO "annotation_key" VALUES(341,24,51);
+INSERT INTO "annotation_key" VALUES(342,24,52);
+INSERT INTO "annotation_key" VALUES(343,24,53);
+INSERT INTO "annotation_key" VALUES(344,24,54);
+INSERT INTO "annotation_key" VALUES(345,24,300);
+INSERT INTO "annotation_key" VALUES(346,24,301);
+INSERT INTO "annotation_key" VALUES(347,24,302);
+INSERT INTO "annotation_key" VALUES(348,24,303);
+INSERT INTO "annotation_key" VALUES(349,24,304);
+INSERT INTO "annotation_key" VALUES(350,24,305);
+INSERT INTO "annotation_key" VALUES(351,24,306);
+INSERT INTO "annotation_key" VALUES(352,24,307);
+INSERT INTO "annotation_key" VALUES(353,24,308);
+INSERT INTO "annotation_key" VALUES(354,24,309);
+INSERT INTO "annotation_key" VALUES(355,24,310);
+INSERT INTO "annotation_key" VALUES(356,24,311);
+INSERT INTO "annotation_key" VALUES(357,24,312);
+INSERT INTO "annotation_key" VALUES(358,24,313);
+INSERT INTO "annotation_key" VALUES(359,24,314);
+INSERT INTO "annotation_key" VALUES(360,24,315);
+INSERT INTO "annotation_key" VALUES(361,24,316);
+INSERT INTO "annotation_key" VALUES(362,26,321);
+INSERT INTO "annotation_key" VALUES(363,26,322);
+INSERT INTO "annotation_key" VALUES(364,26,323);
+INSERT INTO "annotation_key" VALUES(365,26,324);
+INSERT INTO "annotation_key" VALUES(366,27,321);
+INSERT INTO "annotation_key" VALUES(367,27,322);
+INSERT INTO "annotation_key" VALUES(368,27,323);
+INSERT INTO "annotation_key" VALUES(369,27,324);
+INSERT INTO "annotation_key" VALUES(370,28,321);
+INSERT INTO "annotation_key" VALUES(371,28,322);
+INSERT INTO "annotation_key" VALUES(372,28,323);
+INSERT INTO "annotation_key" VALUES(373,28,324);
+INSERT INTO "annotation_key" VALUES(374,29,3);
+INSERT INTO "annotation_key" VALUES(375,29,4);
+INSERT INTO "annotation_key" VALUES(376,29,5);
+INSERT INTO "annotation_key" VALUES(377,29,6);
+INSERT INTO "annotation_key" VALUES(378,29,7);
+INSERT INTO "annotation_key" VALUES(379,29,8);
+INSERT INTO "annotation_key" VALUES(380,29,9);
+INSERT INTO "annotation_key" VALUES(381,29,10);
+INSERT INTO "annotation_key" VALUES(382,29,11);
+INSERT INTO "annotation_key" VALUES(383,29,12);
+INSERT INTO "annotation_key" VALUES(384,29,13);
+INSERT INTO "annotation_key" VALUES(385,29,14);
+INSERT INTO "annotation_key" VALUES(386,29,15);
+INSERT INTO "annotation_key" VALUES(387,29,16);
+INSERT INTO "annotation_key" VALUES(388,29,17);
+INSERT INTO "annotation_key" VALUES(389,29,18);
+INSERT INTO "annotation_key" VALUES(390,29,19);
+INSERT INTO "annotation_key" VALUES(391,29,20);
+INSERT INTO "annotation_key" VALUES(392,29,21);
+INSERT INTO "annotation_key" VALUES(393,29,22);
+INSERT INTO "annotation_key" VALUES(394,29,23);
+INSERT INTO "annotation_key" VALUES(395,29,24);
+INSERT INTO "annotation_key" VALUES(396,29,25);
+INSERT INTO "annotation_key" VALUES(397,29,26);
+INSERT INTO "annotation_key" VALUES(398,29,27);
+INSERT INTO "annotation_key" VALUES(399,29,28);
+INSERT INTO "annotation_key" VALUES(400,29,29);
+INSERT INTO "annotation_key" VALUES(401,29,30);
+INSERT INTO "annotation_key" VALUES(402,29,31);
+INSERT INTO "annotation_key" VALUES(403,29,32);
+INSERT INTO "annotation_key" VALUES(404,29,33);
+INSERT INTO "annotation_key" VALUES(405,29,34);
+INSERT INTO "annotation_key" VALUES(406,29,35);
+INSERT INTO "annotation_key" VALUES(407,29,36);
+INSERT INTO "annotation_key" VALUES(408,29,37);
+INSERT INTO "annotation_key" VALUES(409,29,38);
+INSERT INTO "annotation_key" VALUES(410,29,39);
+INSERT INTO "annotation_key" VALUES(411,29,40);
+INSERT INTO "annotation_key" VALUES(412,29,41);
+INSERT INTO "annotation_key" VALUES(413,29,42);
+INSERT INTO "annotation_key" VALUES(414,29,43);
+INSERT INTO "annotation_key" VALUES(415,29,44);
+INSERT INTO "annotation_key" VALUES(416,29,45);
+INSERT INTO "annotation_key" VALUES(417,29,46);
+INSERT INTO "annotation_key" VALUES(418,29,47);
+INSERT INTO "annotation_key" VALUES(419,29,48);
+INSERT INTO "annotation_key" VALUES(420,29,49);
+INSERT INTO "annotation_key" VALUES(421,29,50);
+INSERT INTO "annotation_key" VALUES(422,29,51);
+INSERT INTO "annotation_key" VALUES(423,29,52);
+INSERT INTO "annotation_key" VALUES(424,29,53);
+INSERT INTO "annotation_key" VALUES(425,29,54);
+INSERT INTO "annotation_key" VALUES(426,25,325);
+INSERT INTO "annotation_key" VALUES(427,25,326);
+INSERT INTO "annotation_key" VALUES(428,25,327);
+INSERT INTO "annotation_key" VALUES(429,25,328);
+INSERT INTO "annotation_key" VALUES(430,25,329);
+INSERT INTO "annotation_key" VALUES(431,25,330);
+INSERT INTO "annotation_key" VALUES(432,25,331);
+INSERT INTO "annotation_key" VALUES(433,25,332);
+INSERT INTO "annotation_key" VALUES(434,25,333);
+INSERT INTO "annotation_key" VALUES(435,25,334);
+INSERT INTO "annotation_key" VALUES(436,25,335);
+INSERT INTO "annotation_key" VALUES(437,25,232);
+INSERT INTO "annotation_key" VALUES(438,25,336);
+INSERT INTO "annotation_key" VALUES(439,25,337);
+INSERT INTO "annotation_key" VALUES(440,25,338);
+INSERT INTO "annotation_key" VALUES(441,25,339);
+INSERT INTO "annotation_key" VALUES(442,25,340);
+INSERT INTO "annotation_key" VALUES(443,25,341);
+INSERT INTO "annotation_key" VALUES(444,25,342);
+INSERT INTO "annotation_key" VALUES(445,25,343);
+INSERT INTO "annotation_key" VALUES(446,25,113);
+INSERT INTO "annotation_key" VALUES(447,25,344);
+INSERT INTO "annotation_key" VALUES(448,25,116);
+INSERT INTO "annotation_key" VALUES(449,25,117);
+INSERT INTO "annotation_key" VALUES(450,25,345);
+INSERT INTO "annotation_key" VALUES(451,25,346);
+INSERT INTO "annotation_key" VALUES(452,31,3);
+INSERT INTO "annotation_key" VALUES(453,31,4);
+INSERT INTO "annotation_key" VALUES(454,31,5);
+INSERT INTO "annotation_key" VALUES(455,31,6);
+INSERT INTO "annotation_key" VALUES(456,31,7);
+INSERT INTO "annotation_key" VALUES(457,31,8);
+INSERT INTO "annotation_key" VALUES(458,31,9);
+INSERT INTO "annotation_key" VALUES(459,31,10);
+INSERT INTO "annotation_key" VALUES(460,31,11);
+INSERT INTO "annotation_key" VALUES(461,31,12);
+INSERT INTO "annotation_key" VALUES(462,31,13);
+INSERT INTO "annotation_key" VALUES(463,31,14);
+INSERT INTO "annotation_key" VALUES(464,31,15);
+INSERT INTO "annotation_key" VALUES(465,31,16);
+INSERT INTO "annotation_key" VALUES(466,31,17);
+INSERT INTO "annotation_key" VALUES(467,31,18);
+INSERT INTO "annotation_key" VALUES(468,31,19);
+INSERT INTO "annotation_key" VALUES(469,31,20);
+INSERT INTO "annotation_key" VALUES(470,31,21);
+INSERT INTO "annotation_key" VALUES(471,31,22);
+INSERT INTO "annotation_key" VALUES(472,31,23);
+INSERT INTO "annotation_key" VALUES(473,31,24);
+INSERT INTO "annotation_key" VALUES(474,31,25);
+INSERT INTO "annotation_key" VALUES(475,31,26);
+INSERT INTO "annotation_key" VALUES(476,31,27);
+INSERT INTO "annotation_key" VALUES(477,31,28);
+INSERT INTO "annotation_key" VALUES(478,31,29);
+INSERT INTO "annotation_key" VALUES(479,31,30);
+INSERT INTO "annotation_key" VALUES(480,31,31);
+INSERT INTO "annotation_key" VALUES(481,31,32);
+INSERT INTO "annotation_key" VALUES(482,31,33);
+INSERT INTO "annotation_key" VALUES(483,31,34);
+INSERT INTO "annotation_key" VALUES(484,31,35);
+INSERT INTO "annotation_key" VALUES(485,31,36);
+INSERT INTO "annotation_key" VALUES(486,31,37);
+INSERT INTO "annotation_key" VALUES(487,31,38);
+INSERT INTO "annotation_key" VALUES(488,31,39);
+INSERT INTO "annotation_key" VALUES(489,31,40);
+INSERT INTO "annotation_key" VALUES(490,31,41);
+INSERT INTO "annotation_key" VALUES(491,31,42);
+INSERT INTO "annotation_key" VALUES(492,31,43);
+INSERT INTO "annotation_key" VALUES(493,31,44);
+INSERT INTO "annotation_key" VALUES(494,31,45);
+INSERT INTO "annotation_key" VALUES(495,31,46);
+INSERT INTO "annotation_key" VALUES(496,31,47);
+INSERT INTO "annotation_key" VALUES(497,31,48);
+INSERT INTO "annotation_key" VALUES(498,31,49);
+INSERT INTO "annotation_key" VALUES(499,31,50);
+INSERT INTO "annotation_key" VALUES(500,31,51);
+INSERT INTO "annotation_key" VALUES(501,31,52);
+INSERT INTO "annotation_key" VALUES(502,31,53);
+INSERT INTO "annotation_key" VALUES(503,31,54);
\ No newline at end of file
diff --git a/src/main/resources/db/predefined/V2.5__annotation_value.sql b/src/main/resources/db/predefined/V2.5__annotation_value.sql
new file mode 100644
index 0000000..256cb83
--- /dev/null
+++ b/src/main/resources/db/predefined/V2.5__annotation_value.sql
@@ -0,0 +1,1302 @@
+INSERT INTO "annotation_value" VALUES(1,106,70);
+INSERT INTO "annotation_value" VALUES(2,106,68);
+INSERT INTO "annotation_value" VALUES(3,106,71);
+INSERT INTO "annotation_value" VALUES(4,106,69);
+INSERT INTO "annotation_value" VALUES(5,106,67);
+INSERT INTO "annotation_value" VALUES(6,107,73);
+INSERT INTO "annotation_value" VALUES(7,107,74);
+INSERT INTO "annotation_value" VALUES(8,107,72);
+INSERT INTO "annotation_value" VALUES(9,108,77);
+INSERT INTO "annotation_value" VALUES(10,108,76);
+INSERT INTO "annotation_value" VALUES(11,108,75);
+INSERT INTO "annotation_value" VALUES(12,108,71);
+INSERT INTO "annotation_value" VALUES(13,109,78);
+INSERT INTO "annotation_value" VALUES(14,109,79);
+INSERT INTO "annotation_value" VALUES(15,109,80);
+INSERT INTO "annotation_value" VALUES(16,110,71);
+INSERT INTO "annotation_value" VALUES(17,110,82);
+INSERT INTO "annotation_value" VALUES(18,110,81);
+INSERT INTO "annotation_value" VALUES(19,111,84);
+INSERT INTO "annotation_value" VALUES(20,111,83);
+INSERT INTO "annotation_value" VALUES(21,111,85);
+INSERT INTO "annotation_value" VALUES(22,112,87);
+INSERT INTO "annotation_value" VALUES(23,112,86);
+INSERT INTO "annotation_value" VALUES(24,149,197);
+INSERT INTO "annotation_value" VALUES(25,149,179);
+INSERT INTO "annotation_value" VALUES(26,149,181);
+INSERT INTO "annotation_value" VALUES(27,149,206);
+INSERT INTO "annotation_value" VALUES(28,149,190);
+INSERT INTO "annotation_value" VALUES(29,149,146);
+INSERT INTO "annotation_value" VALUES(30,149,134);
+INSERT INTO "annotation_value" VALUES(31,149,180);
+INSERT INTO "annotation_value" VALUES(32,149,130);
+INSERT INTO "annotation_value" VALUES(33,149,148);
+INSERT INTO "annotation_value" VALUES(34,149,126);
+INSERT INTO "annotation_value" VALUES(35,149,184);
+INSERT INTO "annotation_value" VALUES(36,149,159);
+INSERT INTO "annotation_value" VALUES(37,149,153);
+INSERT INTO "annotation_value" VALUES(38,149,149);
+INSERT INTO "annotation_value" VALUES(39,149,141);
+INSERT INTO "annotation_value" VALUES(40,149,132);
+INSERT INTO "annotation_value" VALUES(41,149,133);
+INSERT INTO "annotation_value" VALUES(42,149,173);
+INSERT INTO "annotation_value" VALUES(43,149,207);
+INSERT INTO "annotation_value" VALUES(44,149,185);
+INSERT INTO "annotation_value" VALUES(45,149,140);
+INSERT INTO "annotation_value" VALUES(46,149,193);
+INSERT INTO "annotation_value" VALUES(47,149,189);
+INSERT INTO "annotation_value" VALUES(48,149,142);
+INSERT INTO "annotation_value" VALUES(49,149,161);
+INSERT INTO "annotation_value" VALUES(50,149,175);
+INSERT INTO "annotation_value" VALUES(51,149,170);
+INSERT INTO "annotation_value" VALUES(52,149,137);
+INSERT INTO "annotation_value" VALUES(53,149,160);
+INSERT INTO "annotation_value" VALUES(54,149,195);
+INSERT INTO "annotation_value" VALUES(55,149,187);
+INSERT INTO "annotation_value" VALUES(56,149,177);
+INSERT INTO "annotation_value" VALUES(57,149,194);
+INSERT INTO "annotation_value" VALUES(58,149,171);
+INSERT INTO "annotation_value" VALUES(59,149,183);
+INSERT INTO "annotation_value" VALUES(60,149,152);
+INSERT INTO "annotation_value" VALUES(61,149,154);
+INSERT INTO "annotation_value" VALUES(62,149,157);
+INSERT INTO "annotation_value" VALUES(63,149,156);
+INSERT INTO "annotation_value" VALUES(64,149,151);
+INSERT INTO "annotation_value" VALUES(65,149,209);
+INSERT INTO "annotation_value" VALUES(66,149,208);
+INSERT INTO "annotation_value" VALUES(67,149,176);
+INSERT INTO "annotation_value" VALUES(68,149,198);
+INSERT INTO "annotation_value" VALUES(69,149,155);
+INSERT INTO "annotation_value" VALUES(70,149,204);
+INSERT INTO "annotation_value" VALUES(71,149,174);
+INSERT INTO "annotation_value" VALUES(72,149,210);
+INSERT INTO "annotation_value" VALUES(73,149,196);
+INSERT INTO "annotation_value" VALUES(74,149,188);
+INSERT INTO "annotation_value" VALUES(75,149,158);
+INSERT INTO "annotation_value" VALUES(76,149,135);
+INSERT INTO "annotation_value" VALUES(77,149,172);
+INSERT INTO "annotation_value" VALUES(78,149,200);
+INSERT INTO "annotation_value" VALUES(79,149,164);
+INSERT INTO "annotation_value" VALUES(80,149,186);
+INSERT INTO "annotation_value" VALUES(81,149,191);
+INSERT INTO "annotation_value" VALUES(82,149,125);
+INSERT INTO "annotation_value" VALUES(83,149,165);
+INSERT INTO "annotation_value" VALUES(84,149,145);
+INSERT INTO "annotation_value" VALUES(85,149,182);
+INSERT INTO "annotation_value" VALUES(86,149,192);
+INSERT INTO "annotation_value" VALUES(87,149,162);
+INSERT INTO "annotation_value" VALUES(88,149,168);
+INSERT INTO "annotation_value" VALUES(89,149,129);
+INSERT INTO "annotation_value" VALUES(90,149,201);
+INSERT INTO "annotation_value" VALUES(91,149,203);
+INSERT INTO "annotation_value" VALUES(92,149,128);
+INSERT INTO "annotation_value" VALUES(93,149,166);
+INSERT INTO "annotation_value" VALUES(94,149,139);
+INSERT INTO "annotation_value" VALUES(95,149,147);
+INSERT INTO "annotation_value" VALUES(96,149,150);
+INSERT INTO "annotation_value" VALUES(97,149,178);
+INSERT INTO "annotation_value" VALUES(98,149,143);
+INSERT INTO "annotation_value" VALUES(99,149,205);
+INSERT INTO "annotation_value" VALUES(100,149,127);
+INSERT INTO "annotation_value" VALUES(101,149,144);
+INSERT INTO "annotation_value" VALUES(102,149,169);
+INSERT INTO "annotation_value" VALUES(103,149,163);
+INSERT INTO "annotation_value" VALUES(104,149,202);
+INSERT INTO "annotation_value" VALUES(105,149,167);
+INSERT INTO "annotation_value" VALUES(106,149,131);
+INSERT INTO "annotation_value" VALUES(107,149,199);
+INSERT INTO "annotation_value" VALUES(108,149,138);
+INSERT INTO "annotation_value" VALUES(109,149,136);
+INSERT INTO "annotation_value" VALUES(110,237,68);
+INSERT INTO "annotation_value" VALUES(111,237,70);
+INSERT INTO "annotation_value" VALUES(112,237,71);
+INSERT INTO "annotation_value" VALUES(113,237,67);
+INSERT INTO "annotation_value" VALUES(114,237,69);
+INSERT INTO "annotation_value" VALUES(115,238,73);
+INSERT INTO "annotation_value" VALUES(116,238,72);
+INSERT INTO "annotation_value" VALUES(117,238,74);
+INSERT INTO "annotation_value" VALUES(118,239,77);
+INSERT INTO "annotation_value" VALUES(119,239,75);
+INSERT INTO "annotation_value" VALUES(120,239,76);
+INSERT INTO "annotation_value" VALUES(121,239,71);
+INSERT INTO "annotation_value" VALUES(122,240,78);
+INSERT INTO "annotation_value" VALUES(123,240,79);
+INSERT INTO "annotation_value" VALUES(124,240,80);
+INSERT INTO "annotation_value" VALUES(125,241,81);
+INSERT INTO "annotation_value" VALUES(126,241,82);
+INSERT INTO "annotation_value" VALUES(127,241,71);
+INSERT INTO "annotation_value" VALUES(128,242,84);
+INSERT INTO "annotation_value" VALUES(129,242,83);
+INSERT INTO "annotation_value" VALUES(130,242,85);
+INSERT INTO "annotation_value" VALUES(131,243,87);
+INSERT INTO "annotation_value" VALUES(132,243,86);
+INSERT INTO "annotation_value" VALUES(133,426,385);
+INSERT INTO "annotation_value" VALUES(134,426,379);
+INSERT INTO "annotation_value" VALUES(135,426,381);
+INSERT INTO "annotation_value" VALUES(136,426,370);
+INSERT INTO "annotation_value" VALUES(137,426,364);
+INSERT INTO "annotation_value" VALUES(138,426,365);
+INSERT INTO "annotation_value" VALUES(139,426,377);
+INSERT INTO "annotation_value" VALUES(140,426,373);
+INSERT INTO "annotation_value" VALUES(141,426,351);
+INSERT INTO "annotation_value" VALUES(142,426,355);
+INSERT INTO "annotation_value" VALUES(143,426,374);
+INSERT INTO "annotation_value" VALUES(144,426,380);
+INSERT INTO "annotation_value" VALUES(145,426,359);
+INSERT INTO "annotation_value" VALUES(146,426,349);
+INSERT INTO "annotation_value" VALUES(147,426,363);
+INSERT INTO "annotation_value" VALUES(148,426,378);
+INSERT INTO "annotation_value" VALUES(149,426,368);
+INSERT INTO "annotation_value" VALUES(150,426,347);
+INSERT INTO "annotation_value" VALUES(151,426,350);
+INSERT INTO "annotation_value" VALUES(152,426,356);
+INSERT INTO "annotation_value" VALUES(153,426,375);
+INSERT INTO "annotation_value" VALUES(154,426,386);
+INSERT INTO "annotation_value" VALUES(155,426,358);
+INSERT INTO "annotation_value" VALUES(156,426,348);
+INSERT INTO "annotation_value" VALUES(157,426,389);
+INSERT INTO "annotation_value" VALUES(158,426,352);
+INSERT INTO "annotation_value" VALUES(159,426,388);
+INSERT INTO "annotation_value" VALUES(160,426,372);
+INSERT INTO "annotation_value" VALUES(161,426,360);
+INSERT INTO "annotation_value" VALUES(162,426,376);
+INSERT INTO "annotation_value" VALUES(163,426,366);
+INSERT INTO "annotation_value" VALUES(164,426,382);
+INSERT INTO "annotation_value" VALUES(165,426,357);
+INSERT INTO "annotation_value" VALUES(166,426,384);
+INSERT INTO "annotation_value" VALUES(167,426,354);
+INSERT INTO "annotation_value" VALUES(168,426,383);
+INSERT INTO "annotation_value" VALUES(169,426,361);
+INSERT INTO "annotation_value" VALUES(170,426,390);
+INSERT INTO "annotation_value" VALUES(171,426,362);
+INSERT INTO "annotation_value" VALUES(172,426,367);
+INSERT INTO "annotation_value" VALUES(173,426,369);
+INSERT INTO "annotation_value" VALUES(174,426,371);
+INSERT INTO "annotation_value" VALUES(175,426,139);
+INSERT INTO "annotation_value" VALUES(176,426,353);
+INSERT INTO "annotation_value" VALUES(177,426,387);
+INSERT INTO "annotation_value" VALUES(178,427,376);
+INSERT INTO "annotation_value" VALUES(179,427,369);
+INSERT INTO "annotation_value" VALUES(180,427,347);
+INSERT INTO "annotation_value" VALUES(181,427,371);
+INSERT INTO "annotation_value" VALUES(182,427,373);
+INSERT INTO "annotation_value" VALUES(183,427,360);
+INSERT INTO "annotation_value" VALUES(184,427,389);
+INSERT INTO "annotation_value" VALUES(185,427,386);
+INSERT INTO "annotation_value" VALUES(186,427,384);
+INSERT INTO "annotation_value" VALUES(187,427,361);
+INSERT INTO "annotation_value" VALUES(188,427,365);
+INSERT INTO "annotation_value" VALUES(189,427,356);
+INSERT INTO "annotation_value" VALUES(190,427,351);
+INSERT INTO "annotation_value" VALUES(191,427,355);
+INSERT INTO "annotation_value" VALUES(192,427,390);
+INSERT INTO "annotation_value" VALUES(193,427,350);
+INSERT INTO "annotation_value" VALUES(194,427,370);
+INSERT INTO "annotation_value" VALUES(195,427,388);
+INSERT INTO "annotation_value" VALUES(196,427,377);
+INSERT INTO "annotation_value" VALUES(197,427,139);
+INSERT INTO "annotation_value" VALUES(198,427,387);
+INSERT INTO "annotation_value" VALUES(199,427,381);
+INSERT INTO "annotation_value" VALUES(200,427,364);
+INSERT INTO "annotation_value" VALUES(201,427,354);
+INSERT INTO "annotation_value" VALUES(202,427,375);
+INSERT INTO "annotation_value" VALUES(203,427,382);
+INSERT INTO "annotation_value" VALUES(204,427,362);
+INSERT INTO "annotation_value" VALUES(205,427,359);
+INSERT INTO "annotation_value" VALUES(206,427,358);
+INSERT INTO "annotation_value" VALUES(207,427,366);
+INSERT INTO "annotation_value" VALUES(208,427,357);
+INSERT INTO "annotation_value" VALUES(209,427,368);
+INSERT INTO "annotation_value" VALUES(210,427,353);
+INSERT INTO "annotation_value" VALUES(211,427,372);
+INSERT INTO "annotation_value" VALUES(212,427,349);
+INSERT INTO "annotation_value" VALUES(213,427,378);
+INSERT INTO "annotation_value" VALUES(214,427,383);
+INSERT INTO "annotation_value" VALUES(215,427,348);
+INSERT INTO "annotation_value" VALUES(216,427,367);
+INSERT INTO "annotation_value" VALUES(217,427,379);
+INSERT INTO "annotation_value" VALUES(218,427,374);
+INSERT INTO "annotation_value" VALUES(219,427,385);
+INSERT INTO "annotation_value" VALUES(220,427,380);
+INSERT INTO "annotation_value" VALUES(221,427,363);
+INSERT INTO "annotation_value" VALUES(222,427,352);
+INSERT INTO "annotation_value" VALUES(223,428,390);
+INSERT INTO "annotation_value" VALUES(224,428,379);
+INSERT INTO "annotation_value" VALUES(225,428,348);
+INSERT INTO "annotation_value" VALUES(226,428,362);
+INSERT INTO "annotation_value" VALUES(227,428,384);
+INSERT INTO "annotation_value" VALUES(228,428,369);
+INSERT INTO "annotation_value" VALUES(229,428,353);
+INSERT INTO "annotation_value" VALUES(230,428,367);
+INSERT INTO "annotation_value" VALUES(231,428,360);
+INSERT INTO "annotation_value" VALUES(232,428,382);
+INSERT INTO "annotation_value" VALUES(233,428,356);
+INSERT INTO "annotation_value" VALUES(234,428,378);
+INSERT INTO "annotation_value" VALUES(235,428,357);
+INSERT INTO "annotation_value" VALUES(236,428,352);
+INSERT INTO "annotation_value" VALUES(237,428,381);
+INSERT INTO "annotation_value" VALUES(238,428,370);
+INSERT INTO "annotation_value" VALUES(239,428,373);
+INSERT INTO "annotation_value" VALUES(240,428,349);
+INSERT INTO "annotation_value" VALUES(241,428,372);
+INSERT INTO "annotation_value" VALUES(242,428,376);
+INSERT INTO "annotation_value" VALUES(243,428,366);
+INSERT INTO "annotation_value" VALUES(244,428,368);
+INSERT INTO "annotation_value" VALUES(245,428,139);
+INSERT INTO "annotation_value" VALUES(246,428,347);
+INSERT INTO "annotation_value" VALUES(247,428,389);
+INSERT INTO "annotation_value" VALUES(248,428,363);
+INSERT INTO "annotation_value" VALUES(249,428,383);
+INSERT INTO "annotation_value" VALUES(250,428,365);
+INSERT INTO "annotation_value" VALUES(251,428,364);
+INSERT INTO "annotation_value" VALUES(252,428,371);
+INSERT INTO "annotation_value" VALUES(253,428,359);
+INSERT INTO "annotation_value" VALUES(254,428,355);
+INSERT INTO "annotation_value" VALUES(255,428,351);
+INSERT INTO "annotation_value" VALUES(256,428,387);
+INSERT INTO "annotation_value" VALUES(257,428,358);
+INSERT INTO "annotation_value" VALUES(258,428,361);
+INSERT INTO "annotation_value" VALUES(259,428,385);
+INSERT INTO "annotation_value" VALUES(260,428,374);
+INSERT INTO "annotation_value" VALUES(261,428,350);
+INSERT INTO "annotation_value" VALUES(262,428,377);
+INSERT INTO "annotation_value" VALUES(263,428,354);
+INSERT INTO "annotation_value" VALUES(264,428,388);
+INSERT INTO "annotation_value" VALUES(265,428,380);
+INSERT INTO "annotation_value" VALUES(266,428,386);
+INSERT INTO "annotation_value" VALUES(267,428,375);
+INSERT INTO "annotation_value" VALUES(268,429,382);
+INSERT INTO "annotation_value" VALUES(269,429,383);
+INSERT INTO "annotation_value" VALUES(270,429,374);
+INSERT INTO "annotation_value" VALUES(271,429,364);
+INSERT INTO "annotation_value" VALUES(272,429,380);
+INSERT INTO "annotation_value" VALUES(273,429,365);
+INSERT INTO "annotation_value" VALUES(274,429,377);
+INSERT INTO "annotation_value" VALUES(275,429,357);
+INSERT INTO "annotation_value" VALUES(276,429,371);
+INSERT INTO "annotation_value" VALUES(277,429,362);
+INSERT INTO "annotation_value" VALUES(278,429,376);
+INSERT INTO "annotation_value" VALUES(279,429,381);
+INSERT INTO "annotation_value" VALUES(280,429,355);
+INSERT INTO "annotation_value" VALUES(281,429,385);
+INSERT INTO "annotation_value" VALUES(282,429,378);
+INSERT INTO "annotation_value" VALUES(283,429,384);
+INSERT INTO "annotation_value" VALUES(284,429,361);
+INSERT INTO "annotation_value" VALUES(285,429,388);
+INSERT INTO "annotation_value" VALUES(286,429,379);
+INSERT INTO "annotation_value" VALUES(287,429,369);
+INSERT INTO "annotation_value" VALUES(288,429,356);
+INSERT INTO "annotation_value" VALUES(289,429,351);
+INSERT INTO "annotation_value" VALUES(290,429,358);
+INSERT INTO "annotation_value" VALUES(291,429,373);
+INSERT INTO "annotation_value" VALUES(292,429,354);
+INSERT INTO "annotation_value" VALUES(293,429,389);
+INSERT INTO "annotation_value" VALUES(294,429,372);
+INSERT INTO "annotation_value" VALUES(295,429,386);
+INSERT INTO "annotation_value" VALUES(296,429,390);
+INSERT INTO "annotation_value" VALUES(297,429,375);
+INSERT INTO "annotation_value" VALUES(298,429,353);
+INSERT INTO "annotation_value" VALUES(299,429,139);
+INSERT INTO "annotation_value" VALUES(300,429,366);
+INSERT INTO "annotation_value" VALUES(301,429,349);
+INSERT INTO "annotation_value" VALUES(302,429,368);
+INSERT INTO "annotation_value" VALUES(303,429,348);
+INSERT INTO "annotation_value" VALUES(304,429,363);
+INSERT INTO "annotation_value" VALUES(305,429,360);
+INSERT INTO "annotation_value" VALUES(306,429,367);
+INSERT INTO "annotation_value" VALUES(307,429,352);
+INSERT INTO "annotation_value" VALUES(308,429,359);
+INSERT INTO "annotation_value" VALUES(309,429,370);
+INSERT INTO "annotation_value" VALUES(310,429,387);
+INSERT INTO "annotation_value" VALUES(311,429,350);
+INSERT INTO "annotation_value" VALUES(312,429,347);
+INSERT INTO "annotation_value" VALUES(313,430,374);
+INSERT INTO "annotation_value" VALUES(314,430,377);
+INSERT INTO "annotation_value" VALUES(315,430,356);
+INSERT INTO "annotation_value" VALUES(316,430,372);
+INSERT INTO "annotation_value" VALUES(317,430,371);
+INSERT INTO "annotation_value" VALUES(318,430,383);
+INSERT INTO "annotation_value" VALUES(319,430,382);
+INSERT INTO "annotation_value" VALUES(320,430,388);
+INSERT INTO "annotation_value" VALUES(321,430,354);
+INSERT INTO "annotation_value" VALUES(322,430,386);
+INSERT INTO "annotation_value" VALUES(323,430,380);
+INSERT INTO "annotation_value" VALUES(324,430,360);
+INSERT INTO "annotation_value" VALUES(325,430,358);
+INSERT INTO "annotation_value" VALUES(326,430,359);
+INSERT INTO "annotation_value" VALUES(327,430,381);
+INSERT INTO "annotation_value" VALUES(328,430,375);
+INSERT INTO "annotation_value" VALUES(329,430,363);
+INSERT INTO "annotation_value" VALUES(330,430,139);
+INSERT INTO "annotation_value" VALUES(331,430,350);
+INSERT INTO "annotation_value" VALUES(332,430,347);
+INSERT INTO "annotation_value" VALUES(333,430,373);
+INSERT INTO "annotation_value" VALUES(334,430,365);
+INSERT INTO "annotation_value" VALUES(335,430,355);
+INSERT INTO "annotation_value" VALUES(336,430,364);
+INSERT INTO "annotation_value" VALUES(337,430,370);
+INSERT INTO "annotation_value" VALUES(338,430,369);
+INSERT INTO "annotation_value" VALUES(339,430,366);
+INSERT INTO "annotation_value" VALUES(340,430,367);
+INSERT INTO "annotation_value" VALUES(341,430,376);
+INSERT INTO "annotation_value" VALUES(342,430,384);
+INSERT INTO "annotation_value" VALUES(343,430,352);
+INSERT INTO "annotation_value" VALUES(344,430,389);
+INSERT INTO "annotation_value" VALUES(345,430,361);
+INSERT INTO "annotation_value" VALUES(346,430,387);
+INSERT INTO "annotation_value" VALUES(347,430,351);
+INSERT INTO "annotation_value" VALUES(348,430,385);
+INSERT INTO "annotation_value" VALUES(349,430,390);
+INSERT INTO "annotation_value" VALUES(350,430,349);
+INSERT INTO "annotation_value" VALUES(351,430,378);
+INSERT INTO "annotation_value" VALUES(352,430,348);
+INSERT INTO "annotation_value" VALUES(353,430,368);
+INSERT INTO "annotation_value" VALUES(354,430,379);
+INSERT INTO "annotation_value" VALUES(355,430,362);
+INSERT INTO "annotation_value" VALUES(356,430,357);
+INSERT INTO "annotation_value" VALUES(357,430,353);
+INSERT INTO "annotation_value" VALUES(358,431,363);
+INSERT INTO "annotation_value" VALUES(359,431,385);
+INSERT INTO "annotation_value" VALUES(360,431,368);
+INSERT INTO "annotation_value" VALUES(361,431,387);
+INSERT INTO "annotation_value" VALUES(362,431,375);
+INSERT INTO "annotation_value" VALUES(363,431,367);
+INSERT INTO "annotation_value" VALUES(364,431,356);
+INSERT INTO "annotation_value" VALUES(365,431,390);
+INSERT INTO "annotation_value" VALUES(366,431,388);
+INSERT INTO "annotation_value" VALUES(367,431,359);
+INSERT INTO "annotation_value" VALUES(368,431,371);
+INSERT INTO "annotation_value" VALUES(369,431,373);
+INSERT INTO "annotation_value" VALUES(370,431,386);
+INSERT INTO "annotation_value" VALUES(371,431,357);
+INSERT INTO "annotation_value" VALUES(372,431,377);
+INSERT INTO "annotation_value" VALUES(373,431,362);
+INSERT INTO "annotation_value" VALUES(374,431,381);
+INSERT INTO "annotation_value" VALUES(375,431,382);
+INSERT INTO "annotation_value" VALUES(376,431,350);
+INSERT INTO "annotation_value" VALUES(377,431,376);
+INSERT INTO "annotation_value" VALUES(378,431,139);
+INSERT INTO "annotation_value" VALUES(379,431,358);
+INSERT INTO "annotation_value" VALUES(380,431,379);
+INSERT INTO "annotation_value" VALUES(381,431,384);
+INSERT INTO "annotation_value" VALUES(382,431,380);
+INSERT INTO "annotation_value" VALUES(383,431,360);
+INSERT INTO "annotation_value" VALUES(384,431,366);
+INSERT INTO "annotation_value" VALUES(385,431,378);
+INSERT INTO "annotation_value" VALUES(386,431,361);
+INSERT INTO "annotation_value" VALUES(387,431,353);
+INSERT INTO "annotation_value" VALUES(388,431,374);
+INSERT INTO "annotation_value" VALUES(389,431,352);
+INSERT INTO "annotation_value" VALUES(390,431,347);
+INSERT INTO "annotation_value" VALUES(391,431,389);
+INSERT INTO "annotation_value" VALUES(392,431,351);
+INSERT INTO "annotation_value" VALUES(393,431,349);
+INSERT INTO "annotation_value" VALUES(394,431,370);
+INSERT INTO "annotation_value" VALUES(395,431,369);
+INSERT INTO "annotation_value" VALUES(396,431,372);
+INSERT INTO "annotation_value" VALUES(397,431,354);
+INSERT INTO "annotation_value" VALUES(398,431,365);
+INSERT INTO "annotation_value" VALUES(399,431,348);
+INSERT INTO "annotation_value" VALUES(400,431,355);
+INSERT INTO "annotation_value" VALUES(401,431,364);
+INSERT INTO "annotation_value" VALUES(402,431,383);
+INSERT INTO "annotation_value" VALUES(403,432,388);
+INSERT INTO "annotation_value" VALUES(404,432,387);
+INSERT INTO "annotation_value" VALUES(405,432,359);
+INSERT INTO "annotation_value" VALUES(406,432,382);
+INSERT INTO "annotation_value" VALUES(407,432,348);
+INSERT INTO "annotation_value" VALUES(408,432,369);
+INSERT INTO "annotation_value" VALUES(409,432,350);
+INSERT INTO "annotation_value" VALUES(410,432,372);
+INSERT INTO "annotation_value" VALUES(411,432,375);
+INSERT INTO "annotation_value" VALUES(412,432,363);
+INSERT INTO "annotation_value" VALUES(413,432,139);
+INSERT INTO "annotation_value" VALUES(414,432,384);
+INSERT INTO "annotation_value" VALUES(415,432,358);
+INSERT INTO "annotation_value" VALUES(416,432,381);
+INSERT INTO "annotation_value" VALUES(417,432,390);
+INSERT INTO "annotation_value" VALUES(418,432,360);
+INSERT INTO "annotation_value" VALUES(419,432,377);
+INSERT INTO "annotation_value" VALUES(420,432,379);
+INSERT INTO "annotation_value" VALUES(421,432,364);
+INSERT INTO "annotation_value" VALUES(422,432,385);
+INSERT INTO "annotation_value" VALUES(423,432,371);
+INSERT INTO "annotation_value" VALUES(424,432,353);
+INSERT INTO "annotation_value" VALUES(425,432,365);
+INSERT INTO "annotation_value" VALUES(426,432,376);
+INSERT INTO "annotation_value" VALUES(427,432,352);
+INSERT INTO "annotation_value" VALUES(428,432,370);
+INSERT INTO "annotation_value" VALUES(429,432,347);
+INSERT INTO "annotation_value" VALUES(430,432,373);
+INSERT INTO "annotation_value" VALUES(431,432,357);
+INSERT INTO "annotation_value" VALUES(432,432,356);
+INSERT INTO "annotation_value" VALUES(433,432,366);
+INSERT INTO "annotation_value" VALUES(434,432,389);
+INSERT INTO "annotation_value" VALUES(435,432,349);
+INSERT INTO "annotation_value" VALUES(436,432,383);
+INSERT INTO "annotation_value" VALUES(437,432,355);
+INSERT INTO "annotation_value" VALUES(438,432,378);
+INSERT INTO "annotation_value" VALUES(439,432,351);
+INSERT INTO "annotation_value" VALUES(440,432,386);
+INSERT INTO "annotation_value" VALUES(441,432,362);
+INSERT INTO "annotation_value" VALUES(442,432,380);
+INSERT INTO "annotation_value" VALUES(443,432,374);
+INSERT INTO "annotation_value" VALUES(444,432,354);
+INSERT INTO "annotation_value" VALUES(445,432,361);
+INSERT INTO "annotation_value" VALUES(446,432,368);
+INSERT INTO "annotation_value" VALUES(447,432,367);
+INSERT INTO "annotation_value" VALUES(448,433,372);
+INSERT INTO "annotation_value" VALUES(449,433,370);
+INSERT INTO "annotation_value" VALUES(450,433,382);
+INSERT INTO "annotation_value" VALUES(451,433,377);
+INSERT INTO "annotation_value" VALUES(452,433,368);
+INSERT INTO "annotation_value" VALUES(453,433,386);
+INSERT INTO "annotation_value" VALUES(454,433,355);
+INSERT INTO "annotation_value" VALUES(455,433,347);
+INSERT INTO "annotation_value" VALUES(456,433,349);
+INSERT INTO "annotation_value" VALUES(457,433,363);
+INSERT INTO "annotation_value" VALUES(458,433,139);
+INSERT INTO "annotation_value" VALUES(459,433,348);
+INSERT INTO "annotation_value" VALUES(460,433,359);
+INSERT INTO "annotation_value" VALUES(461,433,378);
+INSERT INTO "annotation_value" VALUES(462,433,371);
+INSERT INTO "annotation_value" VALUES(463,433,376);
+INSERT INTO "annotation_value" VALUES(464,433,373);
+INSERT INTO "annotation_value" VALUES(465,433,384);
+INSERT INTO "annotation_value" VALUES(466,433,356);
+INSERT INTO "annotation_value" VALUES(467,433,387);
+INSERT INTO "annotation_value" VALUES(468,433,362);
+INSERT INTO "annotation_value" VALUES(469,433,390);
+INSERT INTO "annotation_value" VALUES(470,433,380);
+INSERT INTO "annotation_value" VALUES(471,433,351);
+INSERT INTO "annotation_value" VALUES(472,433,374);
+INSERT INTO "annotation_value" VALUES(473,433,350);
+INSERT INTO "annotation_value" VALUES(474,433,366);
+INSERT INTO "annotation_value" VALUES(475,433,381);
+INSERT INTO "annotation_value" VALUES(476,433,385);
+INSERT INTO "annotation_value" VALUES(477,433,375);
+INSERT INTO "annotation_value" VALUES(478,433,352);
+INSERT INTO "annotation_value" VALUES(479,433,364);
+INSERT INTO "annotation_value" VALUES(480,433,365);
+INSERT INTO "annotation_value" VALUES(481,433,389);
+INSERT INTO "annotation_value" VALUES(482,433,360);
+INSERT INTO "annotation_value" VALUES(483,433,388);
+INSERT INTO "annotation_value" VALUES(484,433,357);
+INSERT INTO "annotation_value" VALUES(485,433,379);
+INSERT INTO "annotation_value" VALUES(486,433,358);
+INSERT INTO "annotation_value" VALUES(487,433,367);
+INSERT INTO "annotation_value" VALUES(488,433,354);
+INSERT INTO "annotation_value" VALUES(489,433,353);
+INSERT INTO "annotation_value" VALUES(490,433,369);
+INSERT INTO "annotation_value" VALUES(491,433,361);
+INSERT INTO "annotation_value" VALUES(492,433,383);
+INSERT INTO "annotation_value" VALUES(493,434,382);
+INSERT INTO "annotation_value" VALUES(494,434,356);
+INSERT INTO "annotation_value" VALUES(495,434,380);
+INSERT INTO "annotation_value" VALUES(496,434,352);
+INSERT INTO "annotation_value" VALUES(497,434,354);
+INSERT INTO "annotation_value" VALUES(498,434,361);
+INSERT INTO "annotation_value" VALUES(499,434,384);
+INSERT INTO "annotation_value" VALUES(500,434,367);
+INSERT INTO "annotation_value" VALUES(501,434,377);
+INSERT INTO "annotation_value" VALUES(502,434,365);
+INSERT INTO "annotation_value" VALUES(503,434,370);
+INSERT INTO "annotation_value" VALUES(504,434,357);
+INSERT INTO "annotation_value" VALUES(505,434,351);
+INSERT INTO "annotation_value" VALUES(506,434,376);
+INSERT INTO "annotation_value" VALUES(507,434,348);
+INSERT INTO "annotation_value" VALUES(508,434,387);
+INSERT INTO "annotation_value" VALUES(509,434,366);
+INSERT INTO "annotation_value" VALUES(510,434,375);
+INSERT INTO "annotation_value" VALUES(511,434,360);
+INSERT INTO "annotation_value" VALUES(512,434,358);
+INSERT INTO "annotation_value" VALUES(513,434,381);
+INSERT INTO "annotation_value" VALUES(514,434,363);
+INSERT INTO "annotation_value" VALUES(515,434,139);
+INSERT INTO "annotation_value" VALUES(516,434,373);
+INSERT INTO "annotation_value" VALUES(517,434,372);
+INSERT INTO "annotation_value" VALUES(518,434,386);
+INSERT INTO "annotation_value" VALUES(519,434,362);
+INSERT INTO "annotation_value" VALUES(520,434,371);
+INSERT INTO "annotation_value" VALUES(521,434,350);
+INSERT INTO "annotation_value" VALUES(522,434,379);
+INSERT INTO "annotation_value" VALUES(523,434,355);
+INSERT INTO "annotation_value" VALUES(524,434,347);
+INSERT INTO "annotation_value" VALUES(525,434,385);
+INSERT INTO "annotation_value" VALUES(526,434,349);
+INSERT INTO "annotation_value" VALUES(527,434,389);
+INSERT INTO "annotation_value" VALUES(528,434,374);
+INSERT INTO "annotation_value" VALUES(529,434,364);
+INSERT INTO "annotation_value" VALUES(530,434,378);
+INSERT INTO "annotation_value" VALUES(531,434,359);
+INSERT INTO "annotation_value" VALUES(532,434,368);
+INSERT INTO "annotation_value" VALUES(533,434,383);
+INSERT INTO "annotation_value" VALUES(534,434,388);
+INSERT INTO "annotation_value" VALUES(535,434,390);
+INSERT INTO "annotation_value" VALUES(536,434,369);
+INSERT INTO "annotation_value" VALUES(537,434,353);
+INSERT INTO "annotation_value" VALUES(538,435,374);
+INSERT INTO "annotation_value" VALUES(539,435,368);
+INSERT INTO "annotation_value" VALUES(540,435,383);
+INSERT INTO "annotation_value" VALUES(541,435,350);
+INSERT INTO "annotation_value" VALUES(542,435,379);
+INSERT INTO "annotation_value" VALUES(543,435,387);
+INSERT INTO "annotation_value" VALUES(544,435,381);
+INSERT INTO "annotation_value" VALUES(545,435,351);
+INSERT INTO "annotation_value" VALUES(546,435,389);
+INSERT INTO "annotation_value" VALUES(547,435,366);
+INSERT INTO "annotation_value" VALUES(548,435,388);
+INSERT INTO "annotation_value" VALUES(549,435,376);
+INSERT INTO "annotation_value" VALUES(550,435,361);
+INSERT INTO "annotation_value" VALUES(551,435,380);
+INSERT INTO "annotation_value" VALUES(552,435,364);
+INSERT INTO "annotation_value" VALUES(553,435,354);
+INSERT INTO "annotation_value" VALUES(554,435,355);
+INSERT INTO "annotation_value" VALUES(555,435,359);
+INSERT INTO "annotation_value" VALUES(556,435,370);
+INSERT INTO "annotation_value" VALUES(557,435,352);
+INSERT INTO "annotation_value" VALUES(558,435,385);
+INSERT INTO "annotation_value" VALUES(559,435,367);
+INSERT INTO "annotation_value" VALUES(560,435,386);
+INSERT INTO "annotation_value" VALUES(561,435,390);
+INSERT INTO "annotation_value" VALUES(562,435,363);
+INSERT INTO "annotation_value" VALUES(563,435,349);
+INSERT INTO "annotation_value" VALUES(564,435,378);
+INSERT INTO "annotation_value" VALUES(565,435,357);
+INSERT INTO "annotation_value" VALUES(566,435,139);
+INSERT INTO "annotation_value" VALUES(567,435,373);
+INSERT INTO "annotation_value" VALUES(568,435,365);
+INSERT INTO "annotation_value" VALUES(569,435,353);
+INSERT INTO "annotation_value" VALUES(570,435,362);
+INSERT INTO "annotation_value" VALUES(571,435,382);
+INSERT INTO "annotation_value" VALUES(572,435,377);
+INSERT INTO "annotation_value" VALUES(573,435,371);
+INSERT INTO "annotation_value" VALUES(574,435,372);
+INSERT INTO "annotation_value" VALUES(575,435,358);
+INSERT INTO "annotation_value" VALUES(576,435,384);
+INSERT INTO "annotation_value" VALUES(577,435,356);
+INSERT INTO "annotation_value" VALUES(578,435,369);
+INSERT INTO "annotation_value" VALUES(579,435,347);
+INSERT INTO "annotation_value" VALUES(580,435,348);
+INSERT INTO "annotation_value" VALUES(581,435,375);
+INSERT INTO "annotation_value" VALUES(582,435,360);
+INSERT INTO "annotation_value" VALUES(583,436,359);
+INSERT INTO "annotation_value" VALUES(584,436,358);
+INSERT INTO "annotation_value" VALUES(585,436,347);
+INSERT INTO "annotation_value" VALUES(586,436,368);
+INSERT INTO "annotation_value" VALUES(587,436,352);
+INSERT INTO "annotation_value" VALUES(588,436,387);
+INSERT INTO "annotation_value" VALUES(589,436,383);
+INSERT INTO "annotation_value" VALUES(590,436,390);
+INSERT INTO "annotation_value" VALUES(591,436,366);
+INSERT INTO "annotation_value" VALUES(592,436,362);
+INSERT INTO "annotation_value" VALUES(593,436,350);
+INSERT INTO "annotation_value" VALUES(594,436,375);
+INSERT INTO "annotation_value" VALUES(595,436,389);
+INSERT INTO "annotation_value" VALUES(596,436,357);
+INSERT INTO "annotation_value" VALUES(597,436,373);
+INSERT INTO "annotation_value" VALUES(598,436,385);
+INSERT INTO "annotation_value" VALUES(599,436,354);
+INSERT INTO "annotation_value" VALUES(600,436,364);
+INSERT INTO "annotation_value" VALUES(601,436,382);
+INSERT INTO "annotation_value" VALUES(602,436,349);
+INSERT INTO "annotation_value" VALUES(603,436,372);
+INSERT INTO "annotation_value" VALUES(604,436,351);
+INSERT INTO "annotation_value" VALUES(605,436,378);
+INSERT INTO "annotation_value" VALUES(606,436,348);
+INSERT INTO "annotation_value" VALUES(607,436,371);
+INSERT INTO "annotation_value" VALUES(608,436,369);
+INSERT INTO "annotation_value" VALUES(609,436,388);
+INSERT INTO "annotation_value" VALUES(610,436,360);
+INSERT INTO "annotation_value" VALUES(611,436,376);
+INSERT INTO "annotation_value" VALUES(612,436,356);
+INSERT INTO "annotation_value" VALUES(613,436,384);
+INSERT INTO "annotation_value" VALUES(614,436,386);
+INSERT INTO "annotation_value" VALUES(615,436,379);
+INSERT INTO "annotation_value" VALUES(616,436,377);
+INSERT INTO "annotation_value" VALUES(617,436,367);
+INSERT INTO "annotation_value" VALUES(618,436,353);
+INSERT INTO "annotation_value" VALUES(619,436,370);
+INSERT INTO "annotation_value" VALUES(620,436,139);
+INSERT INTO "annotation_value" VALUES(621,436,374);
+INSERT INTO "annotation_value" VALUES(622,436,363);
+INSERT INTO "annotation_value" VALUES(623,436,380);
+INSERT INTO "annotation_value" VALUES(624,436,381);
+INSERT INTO "annotation_value" VALUES(625,436,355);
+INSERT INTO "annotation_value" VALUES(626,436,361);
+INSERT INTO "annotation_value" VALUES(627,436,365);
+INSERT INTO "annotation_value" VALUES(628,437,365);
+INSERT INTO "annotation_value" VALUES(629,437,347);
+INSERT INTO "annotation_value" VALUES(630,437,388);
+INSERT INTO "annotation_value" VALUES(631,437,357);
+INSERT INTO "annotation_value" VALUES(632,437,373);
+INSERT INTO "annotation_value" VALUES(633,437,386);
+INSERT INTO "annotation_value" VALUES(634,437,367);
+INSERT INTO "annotation_value" VALUES(635,437,366);
+INSERT INTO "annotation_value" VALUES(636,437,383);
+INSERT INTO "annotation_value" VALUES(637,437,368);
+INSERT INTO "annotation_value" VALUES(638,437,377);
+INSERT INTO "annotation_value" VALUES(639,437,376);
+INSERT INTO "annotation_value" VALUES(640,437,374);
+INSERT INTO "annotation_value" VALUES(641,437,358);
+INSERT INTO "annotation_value" VALUES(642,437,375);
+INSERT INTO "annotation_value" VALUES(643,437,349);
+INSERT INTO "annotation_value" VALUES(644,437,379);
+INSERT INTO "annotation_value" VALUES(645,437,381);
+INSERT INTO "annotation_value" VALUES(646,437,359);
+INSERT INTO "annotation_value" VALUES(647,437,139);
+INSERT INTO "annotation_value" VALUES(648,437,352);
+INSERT INTO "annotation_value" VALUES(649,437,387);
+INSERT INTO "annotation_value" VALUES(650,437,369);
+INSERT INTO "annotation_value" VALUES(651,437,348);
+INSERT INTO "annotation_value" VALUES(652,437,361);
+INSERT INTO "annotation_value" VALUES(653,437,363);
+INSERT INTO "annotation_value" VALUES(654,437,362);
+INSERT INTO "annotation_value" VALUES(655,437,380);
+INSERT INTO "annotation_value" VALUES(656,437,382);
+INSERT INTO "annotation_value" VALUES(657,437,353);
+INSERT INTO "annotation_value" VALUES(658,437,372);
+INSERT INTO "annotation_value" VALUES(659,437,356);
+INSERT INTO "annotation_value" VALUES(660,437,350);
+INSERT INTO "annotation_value" VALUES(661,437,378);
+INSERT INTO "annotation_value" VALUES(662,437,354);
+INSERT INTO "annotation_value" VALUES(663,437,370);
+INSERT INTO "annotation_value" VALUES(664,437,371);
+INSERT INTO "annotation_value" VALUES(665,437,384);
+INSERT INTO "annotation_value" VALUES(666,437,390);
+INSERT INTO "annotation_value" VALUES(667,437,385);
+INSERT INTO "annotation_value" VALUES(668,437,351);
+INSERT INTO "annotation_value" VALUES(669,437,389);
+INSERT INTO "annotation_value" VALUES(670,437,360);
+INSERT INTO "annotation_value" VALUES(671,437,355);
+INSERT INTO "annotation_value" VALUES(672,437,364);
+INSERT INTO "annotation_value" VALUES(673,438,347);
+INSERT INTO "annotation_value" VALUES(674,438,383);
+INSERT INTO "annotation_value" VALUES(675,438,364);
+INSERT INTO "annotation_value" VALUES(676,438,359);
+INSERT INTO "annotation_value" VALUES(677,438,380);
+INSERT INTO "annotation_value" VALUES(678,438,378);
+INSERT INTO "annotation_value" VALUES(679,438,356);
+INSERT INTO "annotation_value" VALUES(680,438,355);
+INSERT INTO "annotation_value" VALUES(681,438,376);
+INSERT INTO "annotation_value" VALUES(682,438,139);
+INSERT INTO "annotation_value" VALUES(683,438,379);
+INSERT INTO "annotation_value" VALUES(684,438,362);
+INSERT INTO "annotation_value" VALUES(685,438,352);
+INSERT INTO "annotation_value" VALUES(686,438,374);
+INSERT INTO "annotation_value" VALUES(687,438,371);
+INSERT INTO "annotation_value" VALUES(688,438,367);
+INSERT INTO "annotation_value" VALUES(689,438,386);
+INSERT INTO "annotation_value" VALUES(690,438,389);
+INSERT INTO "annotation_value" VALUES(691,438,361);
+INSERT INTO "annotation_value" VALUES(692,438,385);
+INSERT INTO "annotation_value" VALUES(693,438,373);
+INSERT INTO "annotation_value" VALUES(694,438,375);
+INSERT INTO "annotation_value" VALUES(695,438,387);
+INSERT INTO "annotation_value" VALUES(696,438,363);
+INSERT INTO "annotation_value" VALUES(697,438,354);
+INSERT INTO "annotation_value" VALUES(698,438,370);
+INSERT INTO "annotation_value" VALUES(699,438,390);
+INSERT INTO "annotation_value" VALUES(700,438,358);
+INSERT INTO "annotation_value" VALUES(701,438,366);
+INSERT INTO "annotation_value" VALUES(702,438,372);
+INSERT INTO "annotation_value" VALUES(703,438,351);
+INSERT INTO "annotation_value" VALUES(704,438,381);
+INSERT INTO "annotation_value" VALUES(705,438,357);
+INSERT INTO "annotation_value" VALUES(706,438,368);
+INSERT INTO "annotation_value" VALUES(707,438,377);
+INSERT INTO "annotation_value" VALUES(708,438,388);
+INSERT INTO "annotation_value" VALUES(709,438,353);
+INSERT INTO "annotation_value" VALUES(710,438,360);
+INSERT INTO "annotation_value" VALUES(711,438,349);
+INSERT INTO "annotation_value" VALUES(712,438,348);
+INSERT INTO "annotation_value" VALUES(713,438,350);
+INSERT INTO "annotation_value" VALUES(714,438,384);
+INSERT INTO "annotation_value" VALUES(715,438,365);
+INSERT INTO "annotation_value" VALUES(716,438,382);
+INSERT INTO "annotation_value" VALUES(717,438,369);
+INSERT INTO "annotation_value" VALUES(718,439,389);
+INSERT INTO "annotation_value" VALUES(719,439,378);
+INSERT INTO "annotation_value" VALUES(720,439,347);
+INSERT INTO "annotation_value" VALUES(721,439,373);
+INSERT INTO "annotation_value" VALUES(722,439,366);
+INSERT INTO "annotation_value" VALUES(723,439,384);
+INSERT INTO "annotation_value" VALUES(724,439,349);
+INSERT INTO "annotation_value" VALUES(725,439,361);
+INSERT INTO "annotation_value" VALUES(726,439,360);
+INSERT INTO "annotation_value" VALUES(727,439,370);
+INSERT INTO "annotation_value" VALUES(728,439,351);
+INSERT INTO "annotation_value" VALUES(729,439,376);
+INSERT INTO "annotation_value" VALUES(730,439,372);
+INSERT INTO "annotation_value" VALUES(731,439,358);
+INSERT INTO "annotation_value" VALUES(732,439,353);
+INSERT INTO "annotation_value" VALUES(733,439,363);
+INSERT INTO "annotation_value" VALUES(734,439,365);
+INSERT INTO "annotation_value" VALUES(735,439,364);
+INSERT INTO "annotation_value" VALUES(736,439,350);
+INSERT INTO "annotation_value" VALUES(737,439,382);
+INSERT INTO "annotation_value" VALUES(738,439,359);
+INSERT INTO "annotation_value" VALUES(739,439,355);
+INSERT INTO "annotation_value" VALUES(740,439,381);
+INSERT INTO "annotation_value" VALUES(741,439,386);
+INSERT INTO "annotation_value" VALUES(742,439,375);
+INSERT INTO "annotation_value" VALUES(743,439,354);
+INSERT INTO "annotation_value" VALUES(744,439,369);
+INSERT INTO "annotation_value" VALUES(745,439,367);
+INSERT INTO "annotation_value" VALUES(746,439,357);
+INSERT INTO "annotation_value" VALUES(747,439,348);
+INSERT INTO "annotation_value" VALUES(748,439,362);
+INSERT INTO "annotation_value" VALUES(749,439,352);
+INSERT INTO "annotation_value" VALUES(750,439,368);
+INSERT INTO "annotation_value" VALUES(751,439,374);
+INSERT INTO "annotation_value" VALUES(752,439,383);
+INSERT INTO "annotation_value" VALUES(753,439,388);
+INSERT INTO "annotation_value" VALUES(754,439,387);
+INSERT INTO "annotation_value" VALUES(755,439,380);
+INSERT INTO "annotation_value" VALUES(756,439,139);
+INSERT INTO "annotation_value" VALUES(757,439,390);
+INSERT INTO "annotation_value" VALUES(758,439,371);
+INSERT INTO "annotation_value" VALUES(759,439,385);
+INSERT INTO "annotation_value" VALUES(760,439,379);
+INSERT INTO "annotation_value" VALUES(761,439,377);
+INSERT INTO "annotation_value" VALUES(762,439,356);
+INSERT INTO "annotation_value" VALUES(763,440,377);
+INSERT INTO "annotation_value" VALUES(764,440,365);
+INSERT INTO "annotation_value" VALUES(765,440,350);
+INSERT INTO "annotation_value" VALUES(766,440,363);
+INSERT INTO "annotation_value" VALUES(767,440,382);
+INSERT INTO "annotation_value" VALUES(768,440,356);
+INSERT INTO "annotation_value" VALUES(769,440,379);
+INSERT INTO "annotation_value" VALUES(770,440,347);
+INSERT INTO "annotation_value" VALUES(771,440,376);
+INSERT INTO "annotation_value" VALUES(772,440,364);
+INSERT INTO "annotation_value" VALUES(773,440,372);
+INSERT INTO "annotation_value" VALUES(774,440,374);
+INSERT INTO "annotation_value" VALUES(775,440,367);
+INSERT INTO "annotation_value" VALUES(776,440,370);
+INSERT INTO "annotation_value" VALUES(777,440,373);
+INSERT INTO "annotation_value" VALUES(778,440,387);
+INSERT INTO "annotation_value" VALUES(779,440,355);
+INSERT INTO "annotation_value" VALUES(780,440,383);
+INSERT INTO "annotation_value" VALUES(781,440,368);
+INSERT INTO "annotation_value" VALUES(782,440,390);
+INSERT INTO "annotation_value" VALUES(783,440,371);
+INSERT INTO "annotation_value" VALUES(784,440,380);
+INSERT INTO "annotation_value" VALUES(785,440,352);
+INSERT INTO "annotation_value" VALUES(786,440,369);
+INSERT INTO "annotation_value" VALUES(787,440,362);
+INSERT INTO "annotation_value" VALUES(788,440,349);
+INSERT INTO "annotation_value" VALUES(789,440,139);
+INSERT INTO "annotation_value" VALUES(790,440,378);
+INSERT INTO "annotation_value" VALUES(791,440,354);
+INSERT INTO "annotation_value" VALUES(792,440,385);
+INSERT INTO "annotation_value" VALUES(793,440,351);
+INSERT INTO "annotation_value" VALUES(794,440,375);
+INSERT INTO "annotation_value" VALUES(795,440,388);
+INSERT INTO "annotation_value" VALUES(796,440,381);
+INSERT INTO "annotation_value" VALUES(797,440,389);
+INSERT INTO "annotation_value" VALUES(798,440,361);
+INSERT INTO "annotation_value" VALUES(799,440,348);
+INSERT INTO "annotation_value" VALUES(800,440,357);
+INSERT INTO "annotation_value" VALUES(801,440,353);
+INSERT INTO "annotation_value" VALUES(802,440,359);
+INSERT INTO "annotation_value" VALUES(803,440,386);
+INSERT INTO "annotation_value" VALUES(804,440,366);
+INSERT INTO "annotation_value" VALUES(805,440,358);
+INSERT INTO "annotation_value" VALUES(806,440,384);
+INSERT INTO "annotation_value" VALUES(807,440,360);
+INSERT INTO "annotation_value" VALUES(808,441,383);
+INSERT INTO "annotation_value" VALUES(809,441,355);
+INSERT INTO "annotation_value" VALUES(810,441,374);
+INSERT INTO "annotation_value" VALUES(811,441,350);
+INSERT INTO "annotation_value" VALUES(812,441,364);
+INSERT INTO "annotation_value" VALUES(813,441,363);
+INSERT INTO "annotation_value" VALUES(814,441,361);
+INSERT INTO "annotation_value" VALUES(815,441,376);
+INSERT INTO "annotation_value" VALUES(816,441,386);
+INSERT INTO "annotation_value" VALUES(817,441,387);
+INSERT INTO "annotation_value" VALUES(818,441,385);
+INSERT INTO "annotation_value" VALUES(819,441,369);
+INSERT INTO "annotation_value" VALUES(820,441,358);
+INSERT INTO "annotation_value" VALUES(821,441,381);
+INSERT INTO "annotation_value" VALUES(822,441,360);
+INSERT INTO "annotation_value" VALUES(823,441,348);
+INSERT INTO "annotation_value" VALUES(824,441,388);
+INSERT INTO "annotation_value" VALUES(825,441,377);
+INSERT INTO "annotation_value" VALUES(826,441,373);
+INSERT INTO "annotation_value" VALUES(827,441,382);
+INSERT INTO "annotation_value" VALUES(828,441,389);
+INSERT INTO "annotation_value" VALUES(829,441,379);
+INSERT INTO "annotation_value" VALUES(830,441,384);
+INSERT INTO "annotation_value" VALUES(831,441,371);
+INSERT INTO "annotation_value" VALUES(832,441,356);
+INSERT INTO "annotation_value" VALUES(833,441,359);
+INSERT INTO "annotation_value" VALUES(834,441,353);
+INSERT INTO "annotation_value" VALUES(835,441,372);
+INSERT INTO "annotation_value" VALUES(836,441,366);
+INSERT INTO "annotation_value" VALUES(837,441,349);
+INSERT INTO "annotation_value" VALUES(838,441,390);
+INSERT INTO "annotation_value" VALUES(839,441,139);
+INSERT INTO "annotation_value" VALUES(840,441,351);
+INSERT INTO "annotation_value" VALUES(841,441,367);
+INSERT INTO "annotation_value" VALUES(842,441,370);
+INSERT INTO "annotation_value" VALUES(843,441,380);
+INSERT INTO "annotation_value" VALUES(844,441,357);
+INSERT INTO "annotation_value" VALUES(845,441,352);
+INSERT INTO "annotation_value" VALUES(846,441,347);
+INSERT INTO "annotation_value" VALUES(847,441,354);
+INSERT INTO "annotation_value" VALUES(848,441,365);
+INSERT INTO "annotation_value" VALUES(849,441,368);
+INSERT INTO "annotation_value" VALUES(850,441,378);
+INSERT INTO "annotation_value" VALUES(851,441,375);
+INSERT INTO "annotation_value" VALUES(852,441,362);
+INSERT INTO "annotation_value" VALUES(853,442,353);
+INSERT INTO "annotation_value" VALUES(854,442,389);
+INSERT INTO "annotation_value" VALUES(855,442,378);
+INSERT INTO "annotation_value" VALUES(856,442,382);
+INSERT INTO "annotation_value" VALUES(857,442,380);
+INSERT INTO "annotation_value" VALUES(858,442,381);
+INSERT INTO "annotation_value" VALUES(859,442,355);
+INSERT INTO "annotation_value" VALUES(860,442,384);
+INSERT INTO "annotation_value" VALUES(861,442,139);
+INSERT INTO "annotation_value" VALUES(862,442,358);
+INSERT INTO "annotation_value" VALUES(863,442,354);
+INSERT INTO "annotation_value" VALUES(864,442,365);
+INSERT INTO "annotation_value" VALUES(865,442,360);
+INSERT INTO "annotation_value" VALUES(866,442,361);
+INSERT INTO "annotation_value" VALUES(867,442,387);
+INSERT INTO "annotation_value" VALUES(868,442,356);
+INSERT INTO "annotation_value" VALUES(869,442,367);
+INSERT INTO "annotation_value" VALUES(870,442,364);
+INSERT INTO "annotation_value" VALUES(871,442,348);
+INSERT INTO "annotation_value" VALUES(872,442,383);
+INSERT INTO "annotation_value" VALUES(873,442,371);
+INSERT INTO "annotation_value" VALUES(874,442,369);
+INSERT INTO "annotation_value" VALUES(875,442,368);
+INSERT INTO "annotation_value" VALUES(876,442,374);
+INSERT INTO "annotation_value" VALUES(877,442,377);
+INSERT INTO "annotation_value" VALUES(878,442,351);
+INSERT INTO "annotation_value" VALUES(879,442,350);
+INSERT INTO "annotation_value" VALUES(880,442,372);
+INSERT INTO "annotation_value" VALUES(881,442,379);
+INSERT INTO "annotation_value" VALUES(882,442,349);
+INSERT INTO "annotation_value" VALUES(883,442,390);
+INSERT INTO "annotation_value" VALUES(884,442,363);
+INSERT INTO "annotation_value" VALUES(885,442,385);
+INSERT INTO "annotation_value" VALUES(886,442,347);
+INSERT INTO "annotation_value" VALUES(887,442,388);
+INSERT INTO "annotation_value" VALUES(888,442,362);
+INSERT INTO "annotation_value" VALUES(889,442,376);
+INSERT INTO "annotation_value" VALUES(890,442,373);
+INSERT INTO "annotation_value" VALUES(891,442,357);
+INSERT INTO "annotation_value" VALUES(892,442,386);
+INSERT INTO "annotation_value" VALUES(893,442,359);
+INSERT INTO "annotation_value" VALUES(894,442,370);
+INSERT INTO "annotation_value" VALUES(895,442,375);
+INSERT INTO "annotation_value" VALUES(896,442,366);
+INSERT INTO "annotation_value" VALUES(897,442,352);
+INSERT INTO "annotation_value" VALUES(898,443,364);
+INSERT INTO "annotation_value" VALUES(899,443,382);
+INSERT INTO "annotation_value" VALUES(900,443,388);
+INSERT INTO "annotation_value" VALUES(901,443,374);
+INSERT INTO "annotation_value" VALUES(902,443,353);
+INSERT INTO "annotation_value" VALUES(903,443,375);
+INSERT INTO "annotation_value" VALUES(904,443,363);
+INSERT INTO "annotation_value" VALUES(905,443,366);
+INSERT INTO "annotation_value" VALUES(906,443,379);
+INSERT INTO "annotation_value" VALUES(907,443,368);
+INSERT INTO "annotation_value" VALUES(908,443,354);
+INSERT INTO "annotation_value" VALUES(909,443,389);
+INSERT INTO "annotation_value" VALUES(910,443,361);
+INSERT INTO "annotation_value" VALUES(911,443,347);
+INSERT INTO "annotation_value" VALUES(912,443,383);
+INSERT INTO "annotation_value" VALUES(913,443,378);
+INSERT INTO "annotation_value" VALUES(914,443,358);
+INSERT INTO "annotation_value" VALUES(915,443,352);
+INSERT INTO "annotation_value" VALUES(916,443,362);
+INSERT INTO "annotation_value" VALUES(917,443,385);
+INSERT INTO "annotation_value" VALUES(918,443,372);
+INSERT INTO "annotation_value" VALUES(919,443,390);
+INSERT INTO "annotation_value" VALUES(920,443,351);
+INSERT INTO "annotation_value" VALUES(921,443,386);
+INSERT INTO "annotation_value" VALUES(922,443,384);
+INSERT INTO "annotation_value" VALUES(923,443,376);
+INSERT INTO "annotation_value" VALUES(924,443,139);
+INSERT INTO "annotation_value" VALUES(925,443,387);
+INSERT INTO "annotation_value" VALUES(926,443,357);
+INSERT INTO "annotation_value" VALUES(927,443,381);
+INSERT INTO "annotation_value" VALUES(928,443,355);
+INSERT INTO "annotation_value" VALUES(929,443,373);
+INSERT INTO "annotation_value" VALUES(930,443,356);
+INSERT INTO "annotation_value" VALUES(931,443,377);
+INSERT INTO "annotation_value" VALUES(932,443,349);
+INSERT INTO "annotation_value" VALUES(933,443,350);
+INSERT INTO "annotation_value" VALUES(934,443,359);
+INSERT INTO "annotation_value" VALUES(935,443,369);
+INSERT INTO "annotation_value" VALUES(936,443,380);
+INSERT INTO "annotation_value" VALUES(937,443,370);
+INSERT INTO "annotation_value" VALUES(938,443,360);
+INSERT INTO "annotation_value" VALUES(939,443,367);
+INSERT INTO "annotation_value" VALUES(940,443,371);
+INSERT INTO "annotation_value" VALUES(941,443,365);
+INSERT INTO "annotation_value" VALUES(942,443,348);
+INSERT INTO "annotation_value" VALUES(943,444,383);
+INSERT INTO "annotation_value" VALUES(944,444,351);
+INSERT INTO "annotation_value" VALUES(945,444,350);
+INSERT INTO "annotation_value" VALUES(946,444,353);
+INSERT INTO "annotation_value" VALUES(947,444,374);
+INSERT INTO "annotation_value" VALUES(948,444,368);
+INSERT INTO "annotation_value" VALUES(949,444,348);
+INSERT INTO "annotation_value" VALUES(950,444,380);
+INSERT INTO "annotation_value" VALUES(951,444,359);
+INSERT INTO "annotation_value" VALUES(952,444,360);
+INSERT INTO "annotation_value" VALUES(953,444,357);
+INSERT INTO "annotation_value" VALUES(954,444,370);
+INSERT INTO "annotation_value" VALUES(955,444,361);
+INSERT INTO "annotation_value" VALUES(956,444,366);
+INSERT INTO "annotation_value" VALUES(957,444,385);
+INSERT INTO "annotation_value" VALUES(958,444,388);
+INSERT INTO "annotation_value" VALUES(959,444,367);
+INSERT INTO "annotation_value" VALUES(960,444,378);
+INSERT INTO "annotation_value" VALUES(961,444,373);
+INSERT INTO "annotation_value" VALUES(962,444,382);
+INSERT INTO "annotation_value" VALUES(963,444,377);
+INSERT INTO "annotation_value" VALUES(964,444,389);
+INSERT INTO "annotation_value" VALUES(965,444,355);
+INSERT INTO "annotation_value" VALUES(966,444,364);
+INSERT INTO "annotation_value" VALUES(967,444,371);
+INSERT INTO "annotation_value" VALUES(968,444,358);
+INSERT INTO "annotation_value" VALUES(969,444,376);
+INSERT INTO "annotation_value" VALUES(970,444,369);
+INSERT INTO "annotation_value" VALUES(971,444,347);
+INSERT INTO "annotation_value" VALUES(972,444,375);
+INSERT INTO "annotation_value" VALUES(973,444,349);
+INSERT INTO "annotation_value" VALUES(974,444,362);
+INSERT INTO "annotation_value" VALUES(975,444,352);
+INSERT INTO "annotation_value" VALUES(976,444,356);
+INSERT INTO "annotation_value" VALUES(977,444,381);
+INSERT INTO "annotation_value" VALUES(978,444,384);
+INSERT INTO "annotation_value" VALUES(979,444,372);
+INSERT INTO "annotation_value" VALUES(980,444,139);
+INSERT INTO "annotation_value" VALUES(981,444,363);
+INSERT INTO "annotation_value" VALUES(982,444,379);
+INSERT INTO "annotation_value" VALUES(983,444,386);
+INSERT INTO "annotation_value" VALUES(984,444,354);
+INSERT INTO "annotation_value" VALUES(985,444,387);
+INSERT INTO "annotation_value" VALUES(986,444,390);
+INSERT INTO "annotation_value" VALUES(987,444,365);
+INSERT INTO "annotation_value" VALUES(988,445,348);
+INSERT INTO "annotation_value" VALUES(989,445,383);
+INSERT INTO "annotation_value" VALUES(990,445,364);
+INSERT INTO "annotation_value" VALUES(991,445,347);
+INSERT INTO "annotation_value" VALUES(992,445,361);
+INSERT INTO "annotation_value" VALUES(993,445,376);
+INSERT INTO "annotation_value" VALUES(994,445,370);
+INSERT INTO "annotation_value" VALUES(995,445,363);
+INSERT INTO "annotation_value" VALUES(996,445,352);
+INSERT INTO "annotation_value" VALUES(997,445,356);
+INSERT INTO "annotation_value" VALUES(998,445,372);
+INSERT INTO "annotation_value" VALUES(999,445,386);
+INSERT INTO "annotation_value" VALUES(1000,445,378);
+INSERT INTO "annotation_value" VALUES(1001,445,390);
+INSERT INTO "annotation_value" VALUES(1002,445,355);
+INSERT INTO "annotation_value" VALUES(1003,445,366);
+INSERT INTO "annotation_value" VALUES(1004,445,139);
+INSERT INTO "annotation_value" VALUES(1005,445,385);
+INSERT INTO "annotation_value" VALUES(1006,445,368);
+INSERT INTO "annotation_value" VALUES(1007,445,365);
+INSERT INTO "annotation_value" VALUES(1008,445,360);
+INSERT INTO "annotation_value" VALUES(1009,445,362);
+INSERT INTO "annotation_value" VALUES(1010,445,367);
+INSERT INTO "annotation_value" VALUES(1011,445,357);
+INSERT INTO "annotation_value" VALUES(1012,445,388);
+INSERT INTO "annotation_value" VALUES(1013,445,382);
+INSERT INTO "annotation_value" VALUES(1014,445,379);
+INSERT INTO "annotation_value" VALUES(1015,445,381);
+INSERT INTO "annotation_value" VALUES(1016,445,359);
+INSERT INTO "annotation_value" VALUES(1017,445,350);
+INSERT INTO "annotation_value" VALUES(1018,445,387);
+INSERT INTO "annotation_value" VALUES(1019,445,358);
+INSERT INTO "annotation_value" VALUES(1020,445,375);
+INSERT INTO "annotation_value" VALUES(1021,445,374);
+INSERT INTO "annotation_value" VALUES(1022,445,371);
+INSERT INTO "annotation_value" VALUES(1023,445,349);
+INSERT INTO "annotation_value" VALUES(1024,445,377);
+INSERT INTO "annotation_value" VALUES(1025,445,380);
+INSERT INTO "annotation_value" VALUES(1026,445,354);
+INSERT INTO "annotation_value" VALUES(1027,445,384);
+INSERT INTO "annotation_value" VALUES(1028,445,351);
+INSERT INTO "annotation_value" VALUES(1029,445,369);
+INSERT INTO "annotation_value" VALUES(1030,445,353);
+INSERT INTO "annotation_value" VALUES(1031,445,373);
+INSERT INTO "annotation_value" VALUES(1032,445,389);
+INSERT INTO "annotation_value" VALUES(1033,446,374);
+INSERT INTO "annotation_value" VALUES(1034,446,373);
+INSERT INTO "annotation_value" VALUES(1035,446,381);
+INSERT INTO "annotation_value" VALUES(1036,446,380);
+INSERT INTO "annotation_value" VALUES(1037,446,383);
+INSERT INTO "annotation_value" VALUES(1038,446,362);
+INSERT INTO "annotation_value" VALUES(1039,446,356);
+INSERT INTO "annotation_value" VALUES(1040,446,353);
+INSERT INTO "annotation_value" VALUES(1041,446,359);
+INSERT INTO "annotation_value" VALUES(1042,446,363);
+INSERT INTO "annotation_value" VALUES(1043,446,355);
+INSERT INTO "annotation_value" VALUES(1044,446,358);
+INSERT INTO "annotation_value" VALUES(1045,446,382);
+INSERT INTO "annotation_value" VALUES(1046,446,378);
+INSERT INTO "annotation_value" VALUES(1047,446,357);
+INSERT INTO "annotation_value" VALUES(1048,446,390);
+INSERT INTO "annotation_value" VALUES(1049,446,354);
+INSERT INTO "annotation_value" VALUES(1050,446,371);
+INSERT INTO "annotation_value" VALUES(1051,446,372);
+INSERT INTO "annotation_value" VALUES(1052,446,367);
+INSERT INTO "annotation_value" VALUES(1053,446,351);
+INSERT INTO "annotation_value" VALUES(1054,446,379);
+INSERT INTO "annotation_value" VALUES(1055,446,350);
+INSERT INTO "annotation_value" VALUES(1056,446,386);
+INSERT INTO "annotation_value" VALUES(1057,446,139);
+INSERT INTO "annotation_value" VALUES(1058,446,388);
+INSERT INTO "annotation_value" VALUES(1059,446,352);
+INSERT INTO "annotation_value" VALUES(1060,446,360);
+INSERT INTO "annotation_value" VALUES(1061,446,361);
+INSERT INTO "annotation_value" VALUES(1062,446,365);
+INSERT INTO "annotation_value" VALUES(1063,446,349);
+INSERT INTO "annotation_value" VALUES(1064,446,385);
+INSERT INTO "annotation_value" VALUES(1065,446,348);
+INSERT INTO "annotation_value" VALUES(1066,446,364);
+INSERT INTO "annotation_value" VALUES(1067,446,366);
+INSERT INTO "annotation_value" VALUES(1068,446,377);
+INSERT INTO "annotation_value" VALUES(1069,446,387);
+INSERT INTO "annotation_value" VALUES(1070,446,375);
+INSERT INTO "annotation_value" VALUES(1071,446,347);
+INSERT INTO "annotation_value" VALUES(1072,446,389);
+INSERT INTO "annotation_value" VALUES(1073,446,368);
+INSERT INTO "annotation_value" VALUES(1074,446,370);
+INSERT INTO "annotation_value" VALUES(1075,446,369);
+INSERT INTO "annotation_value" VALUES(1076,446,376);
+INSERT INTO "annotation_value" VALUES(1077,446,384);
+INSERT INTO "annotation_value" VALUES(1078,447,370);
+INSERT INTO "annotation_value" VALUES(1079,447,371);
+INSERT INTO "annotation_value" VALUES(1080,447,360);
+INSERT INTO "annotation_value" VALUES(1081,447,376);
+INSERT INTO "annotation_value" VALUES(1082,447,387);
+INSERT INTO "annotation_value" VALUES(1083,447,373);
+INSERT INTO "annotation_value" VALUES(1084,447,348);
+INSERT INTO "annotation_value" VALUES(1085,447,375);
+INSERT INTO "annotation_value" VALUES(1086,447,356);
+INSERT INTO "annotation_value" VALUES(1087,447,384);
+INSERT INTO "annotation_value" VALUES(1088,447,381);
+INSERT INTO "annotation_value" VALUES(1089,447,351);
+INSERT INTO "annotation_value" VALUES(1090,447,353);
+INSERT INTO "annotation_value" VALUES(1091,447,385);
+INSERT INTO "annotation_value" VALUES(1092,447,368);
+INSERT INTO "annotation_value" VALUES(1093,447,389);
+INSERT INTO "annotation_value" VALUES(1094,447,352);
+INSERT INTO "annotation_value" VALUES(1095,447,349);
+INSERT INTO "annotation_value" VALUES(1096,447,380);
+INSERT INTO "annotation_value" VALUES(1097,447,359);
+INSERT INTO "annotation_value" VALUES(1098,447,364);
+INSERT INTO "annotation_value" VALUES(1099,447,363);
+INSERT INTO "annotation_value" VALUES(1100,447,377);
+INSERT INTO "annotation_value" VALUES(1101,447,139);
+INSERT INTO "annotation_value" VALUES(1102,447,355);
+INSERT INTO "annotation_value" VALUES(1103,447,383);
+INSERT INTO "annotation_value" VALUES(1104,447,382);
+INSERT INTO "annotation_value" VALUES(1105,447,350);
+INSERT INTO "annotation_value" VALUES(1106,447,354);
+INSERT INTO "annotation_value" VALUES(1107,447,357);
+INSERT INTO "annotation_value" VALUES(1108,447,379);
+INSERT INTO "annotation_value" VALUES(1109,447,367);
+INSERT INTO "annotation_value" VALUES(1110,447,378);
+INSERT INTO "annotation_value" VALUES(1111,447,361);
+INSERT INTO "annotation_value" VALUES(1112,447,362);
+INSERT INTO "annotation_value" VALUES(1113,447,366);
+INSERT INTO "annotation_value" VALUES(1114,447,374);
+INSERT INTO "annotation_value" VALUES(1115,447,390);
+INSERT INTO "annotation_value" VALUES(1116,447,386);
+INSERT INTO "annotation_value" VALUES(1117,447,358);
+INSERT INTO "annotation_value" VALUES(1118,447,369);
+INSERT INTO "annotation_value" VALUES(1119,447,347);
+INSERT INTO "annotation_value" VALUES(1120,447,388);
+INSERT INTO "annotation_value" VALUES(1121,447,365);
+INSERT INTO "annotation_value" VALUES(1122,447,372);
+INSERT INTO "annotation_value" VALUES(1123,448,370);
+INSERT INTO "annotation_value" VALUES(1124,448,367);
+INSERT INTO "annotation_value" VALUES(1125,448,351);
+INSERT INTO "annotation_value" VALUES(1126,448,364);
+INSERT INTO "annotation_value" VALUES(1127,448,357);
+INSERT INTO "annotation_value" VALUES(1128,448,385);
+INSERT INTO "annotation_value" VALUES(1129,448,390);
+INSERT INTO "annotation_value" VALUES(1130,448,353);
+INSERT INTO "annotation_value" VALUES(1131,448,384);
+INSERT INTO "annotation_value" VALUES(1132,448,350);
+INSERT INTO "annotation_value" VALUES(1133,448,381);
+INSERT INTO "annotation_value" VALUES(1134,448,376);
+INSERT INTO "annotation_value" VALUES(1135,448,369);
+INSERT INTO "annotation_value" VALUES(1136,448,377);
+INSERT INTO "annotation_value" VALUES(1137,448,139);
+INSERT INTO "annotation_value" VALUES(1138,448,361);
+INSERT INTO "annotation_value" VALUES(1139,448,352);
+INSERT INTO "annotation_value" VALUES(1140,448,388);
+INSERT INTO "annotation_value" VALUES(1141,448,374);
+INSERT INTO "annotation_value" VALUES(1142,448,383);
+INSERT INTO "annotation_value" VALUES(1143,448,371);
+INSERT INTO "annotation_value" VALUES(1144,448,375);
+INSERT INTO "annotation_value" VALUES(1145,448,379);
+INSERT INTO "annotation_value" VALUES(1146,448,386);
+INSERT INTO "annotation_value" VALUES(1147,448,356);
+INSERT INTO "annotation_value" VALUES(1148,448,365);
+INSERT INTO "annotation_value" VALUES(1149,448,387);
+INSERT INTO "annotation_value" VALUES(1150,448,354);
+INSERT INTO "annotation_value" VALUES(1151,448,360);
+INSERT INTO "annotation_value" VALUES(1152,448,359);
+INSERT INTO "annotation_value" VALUES(1153,448,378);
+INSERT INTO "annotation_value" VALUES(1154,448,382);
+INSERT INTO "annotation_value" VALUES(1155,448,366);
+INSERT INTO "annotation_value" VALUES(1156,448,349);
+INSERT INTO "annotation_value" VALUES(1157,448,347);
+INSERT INTO "annotation_value" VALUES(1158,448,380);
+INSERT INTO "annotation_value" VALUES(1159,448,358);
+INSERT INTO "annotation_value" VALUES(1160,448,355);
+INSERT INTO "annotation_value" VALUES(1161,448,348);
+INSERT INTO "annotation_value" VALUES(1162,448,372);
+INSERT INTO "annotation_value" VALUES(1163,448,363);
+INSERT INTO "annotation_value" VALUES(1164,448,389);
+INSERT INTO "annotation_value" VALUES(1165,448,373);
+INSERT INTO "annotation_value" VALUES(1166,448,368);
+INSERT INTO "annotation_value" VALUES(1167,448,362);
+INSERT INTO "annotation_value" VALUES(1168,449,377);
+INSERT INTO "annotation_value" VALUES(1169,449,348);
+INSERT INTO "annotation_value" VALUES(1170,449,382);
+INSERT INTO "annotation_value" VALUES(1171,449,351);
+INSERT INTO "annotation_value" VALUES(1172,449,390);
+INSERT INTO "annotation_value" VALUES(1173,449,361);
+INSERT INTO "annotation_value" VALUES(1174,449,347);
+INSERT INTO "annotation_value" VALUES(1175,449,352);
+INSERT INTO "annotation_value" VALUES(1176,449,379);
+INSERT INTO "annotation_value" VALUES(1177,449,372);
+INSERT INTO "annotation_value" VALUES(1178,449,365);
+INSERT INTO "annotation_value" VALUES(1179,449,376);
+INSERT INTO "annotation_value" VALUES(1180,449,363);
+INSERT INTO "annotation_value" VALUES(1181,449,387);
+INSERT INTO "annotation_value" VALUES(1182,449,368);
+INSERT INTO "annotation_value" VALUES(1183,449,386);
+INSERT INTO "annotation_value" VALUES(1184,449,384);
+INSERT INTO "annotation_value" VALUES(1185,449,389);
+INSERT INTO "annotation_value" VALUES(1186,449,360);
+INSERT INTO "annotation_value" VALUES(1187,449,358);
+INSERT INTO "annotation_value" VALUES(1188,449,139);
+INSERT INTO "annotation_value" VALUES(1189,449,380);
+INSERT INTO "annotation_value" VALUES(1190,449,353);
+INSERT INTO "annotation_value" VALUES(1191,449,359);
+INSERT INTO "annotation_value" VALUES(1192,449,375);
+INSERT INTO "annotation_value" VALUES(1193,449,371);
+INSERT INTO "annotation_value" VALUES(1194,449,355);
+INSERT INTO "annotation_value" VALUES(1195,449,354);
+INSERT INTO "annotation_value" VALUES(1196,449,349);
+INSERT INTO "annotation_value" VALUES(1197,449,385);
+INSERT INTO "annotation_value" VALUES(1198,449,378);
+INSERT INTO "annotation_value" VALUES(1199,449,367);
+INSERT INTO "annotation_value" VALUES(1200,449,374);
+INSERT INTO "annotation_value" VALUES(1201,449,370);
+INSERT INTO "annotation_value" VALUES(1202,449,364);
+INSERT INTO "annotation_value" VALUES(1203,449,366);
+INSERT INTO "annotation_value" VALUES(1204,449,383);
+INSERT INTO "annotation_value" VALUES(1205,449,362);
+INSERT INTO "annotation_value" VALUES(1206,449,381);
+INSERT INTO "annotation_value" VALUES(1207,449,388);
+INSERT INTO "annotation_value" VALUES(1208,449,357);
+INSERT INTO "annotation_value" VALUES(1209,449,350);
+INSERT INTO "annotation_value" VALUES(1210,449,356);
+INSERT INTO "annotation_value" VALUES(1211,449,373);
+INSERT INTO "annotation_value" VALUES(1212,449,369);
+INSERT INTO "annotation_value" VALUES(1213,450,389);
+INSERT INTO "annotation_value" VALUES(1214,450,360);
+INSERT INTO "annotation_value" VALUES(1215,450,370);
+INSERT INTO "annotation_value" VALUES(1216,450,390);
+INSERT INTO "annotation_value" VALUES(1217,450,372);
+INSERT INTO "annotation_value" VALUES(1218,450,375);
+INSERT INTO "annotation_value" VALUES(1219,450,381);
+INSERT INTO "annotation_value" VALUES(1220,450,362);
+INSERT INTO "annotation_value" VALUES(1221,450,353);
+INSERT INTO "annotation_value" VALUES(1222,450,388);
+INSERT INTO "annotation_value" VALUES(1223,450,377);
+INSERT INTO "annotation_value" VALUES(1224,450,351);
+INSERT INTO "annotation_value" VALUES(1225,450,358);
+INSERT INTO "annotation_value" VALUES(1226,450,139);
+INSERT INTO "annotation_value" VALUES(1227,450,357);
+INSERT INTO "annotation_value" VALUES(1228,450,385);
+INSERT INTO "annotation_value" VALUES(1229,450,366);
+INSERT INTO "annotation_value" VALUES(1230,450,365);
+INSERT INTO "annotation_value" VALUES(1231,450,363);
+INSERT INTO "annotation_value" VALUES(1232,450,387);
+INSERT INTO "annotation_value" VALUES(1233,450,364);
+INSERT INTO "annotation_value" VALUES(1234,450,354);
+INSERT INTO "annotation_value" VALUES(1235,450,386);
+INSERT INTO "annotation_value" VALUES(1236,450,382);
+INSERT INTO "annotation_value" VALUES(1237,450,361);
+INSERT INTO "annotation_value" VALUES(1238,450,384);
+INSERT INTO "annotation_value" VALUES(1239,450,368);
+INSERT INTO "annotation_value" VALUES(1240,450,347);
+INSERT INTO "annotation_value" VALUES(1241,450,374);
+INSERT INTO "annotation_value" VALUES(1242,450,369);
+INSERT INTO "annotation_value" VALUES(1243,450,376);
+INSERT INTO "annotation_value" VALUES(1244,450,355);
+INSERT INTO "annotation_value" VALUES(1245,450,349);
+INSERT INTO "annotation_value" VALUES(1246,450,350);
+INSERT INTO "annotation_value" VALUES(1247,450,356);
+INSERT INTO "annotation_value" VALUES(1248,450,378);
+INSERT INTO "annotation_value" VALUES(1249,450,379);
+INSERT INTO "annotation_value" VALUES(1250,450,373);
+INSERT INTO "annotation_value" VALUES(1251,450,352);
+INSERT INTO "annotation_value" VALUES(1252,450,359);
+INSERT INTO "annotation_value" VALUES(1253,450,380);
+INSERT INTO "annotation_value" VALUES(1254,450,371);
+INSERT INTO "annotation_value" VALUES(1255,450,383);
+INSERT INTO "annotation_value" VALUES(1256,450,348);
+INSERT INTO "annotation_value" VALUES(1257,450,367);
+INSERT INTO "annotation_value" VALUES(1258,451,389);
+INSERT INTO "annotation_value" VALUES(1259,451,350);
+INSERT INTO "annotation_value" VALUES(1260,451,381);
+INSERT INTO "annotation_value" VALUES(1261,451,385);
+INSERT INTO "annotation_value" VALUES(1262,451,356);
+INSERT INTO "annotation_value" VALUES(1263,451,372);
+INSERT INTO "annotation_value" VALUES(1264,451,374);
+INSERT INTO "annotation_value" VALUES(1265,451,355);
+INSERT INTO "annotation_value" VALUES(1266,451,354);
+INSERT INTO "annotation_value" VALUES(1267,451,388);
+INSERT INTO "annotation_value" VALUES(1268,451,364);
+INSERT INTO "annotation_value" VALUES(1269,451,361);
+INSERT INTO "annotation_value" VALUES(1270,451,366);
+INSERT INTO "annotation_value" VALUES(1271,451,368);
+INSERT INTO "annotation_value" VALUES(1272,451,373);
+INSERT INTO "annotation_value" VALUES(1273,451,384);
+INSERT INTO "annotation_value" VALUES(1274,451,351);
+INSERT INTO "annotation_value" VALUES(1275,451,369);
+INSERT INTO "annotation_value" VALUES(1276,451,363);
+INSERT INTO "annotation_value" VALUES(1277,451,352);
+INSERT INTO "annotation_value" VALUES(1278,451,376);
+INSERT INTO "annotation_value" VALUES(1279,451,362);
+INSERT INTO "annotation_value" VALUES(1280,451,382);
+INSERT INTO "annotation_value" VALUES(1281,451,378);
+INSERT INTO "annotation_value" VALUES(1282,451,139);
+INSERT INTO "annotation_value" VALUES(1283,451,390);
+INSERT INTO "annotation_value" VALUES(1284,451,359);
+INSERT INTO "annotation_value" VALUES(1285,451,383);
+INSERT INTO "annotation_value" VALUES(1286,451,387);
+INSERT INTO "annotation_value" VALUES(1287,451,371);
+INSERT INTO "annotation_value" VALUES(1288,451,367);
+INSERT INTO "annotation_value" VALUES(1289,451,353);
+INSERT INTO "annotation_value" VALUES(1290,451,349);
+INSERT INTO "annotation_value" VALUES(1291,451,365);
+INSERT INTO "annotation_value" VALUES(1292,451,380);
+INSERT INTO "annotation_value" VALUES(1293,451,358);
+INSERT INTO "annotation_value" VALUES(1294,451,386);
+INSERT INTO "annotation_value" VALUES(1295,451,379);
+INSERT INTO "annotation_value" VALUES(1296,451,347);
+INSERT INTO "annotation_value" VALUES(1297,451,375);
+INSERT INTO "annotation_value" VALUES(1298,451,357);
+INSERT INTO "annotation_value" VALUES(1299,451,348);
+INSERT INTO "annotation_value" VALUES(1300,451,377);
+INSERT INTO "annotation_value" VALUES(1301,451,370);
+INSERT INTO "annotation_value" VALUES(1302,451,360);
diff --git a/src/main/resources/db/sqlite/V1.10__oauth2_alteration.sql b/src/main/resources/db/sqlite/V1.10__oauth2_alteration.sql
new file mode 100644
index 0000000..2cedafa
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.10__oauth2_alteration.sql
@@ -0,0 +1,36 @@
+CREATE TABLE IF NOT EXISTS oauth2_refresh_token_scope_new (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	FOREIGN KEY (token_id)
+	   REFERENCES oauth2_refresh_token(id)
+	   ON DELETE CASCADE
+	FOREIGN KEY (scope_id)
+	   REFERENCES oauth2_access_scope(id)
+	   ON DELETE CASCADE
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
+);
+
+INSERT INTO oauth2_refresh_token_scope_new SELECT * FROM oauth2_refresh_token_scope;
+
+DROP TABLE oauth2_refresh_token_scope;
+
+ALTER TABLE oauth2_refresh_token_scope_new RENAME TO oauth2_refresh_token_scope;
+
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token_scope_new (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	FOREIGN KEY (token_id)
+	   REFERENCES oauth2_access_token(id)
+	   ON DELETE CASCADE
+	FOREIGN KEY (scope_id)
+	   REFERENCES oauth2_access_scope(id)
+	   ON DELETE CASCADE
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
+);
+
+INSERT INTO oauth2_access_token_scope_new SELECT * FROM oauth2_access_token_scope;
+
+DROP TABLE oauth2_access_token_scope;
+
+ALTER TABLE oauth2_access_token_scope_new RENAME TO oauth2_access_token_scope;
diff --git a/src/main/resources/db/sqlite/V1.11__oauth2_client_alteration.sql b/src/main/resources/db/sqlite/V1.11__oauth2_client_alteration.sql
new file mode 100644
index 0000000..7bcde43
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.11__oauth2_client_alteration.sql
@@ -0,0 +1,25 @@
+CREATE TABLE IF NOT EXISTS oauth2_client_new (
+	id VARCHAR(100) PRIMARY KEY NOT NULL,
+	name VARCHAR(100) NOT NULL,
+	secret VARCHAR(255) DEFAULT NULL,
+	type VARCHAR(50) NOT NULL,
+	super BOOLEAN DEFAULT FALSE,
+	redirect_uri TEXT DEFAULT NULL,
+	description VARCHAR(255) NOT NULL,
+	registered_by VARCHAR(100) NOT NULL,
+	--url_hashcode INTEGER,	
+	url TEXT DEFAULT NULL,
+	registration_date TIMESTAMP,
+	refresh_token_expiry INTEGER DEFAULT 31536000,
+	source BLOB DEFAULT NULL,
+	is_permitted BOOLEAN DEFAULT FALSE
+);
+
+INSERT INTO oauth2_client_new(id,name,secret,type,super,redirect_uri,description,registered_by,url) 
+	SELECT id,name,secret,type,super,redirect_uri,description,registered_by,url FROM oauth2_client;
+
+UPDATE oauth2_client_new SET registration_date = CURRENT_TIMESTAMP;	
+	
+DROP TABLE oauth2_client;
+
+ALTER TABLE oauth2_client_new RENAME TO oauth2_client;
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.12__plugins.sql b/src/main/resources/db/sqlite/V1.12__plugins.sql
new file mode 100644
index 0000000..1de97b4
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.12__plugins.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS installed_plugin (
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	installed_by VARCHAR(100) NOT NULL,
+	installed_date TIMESTAMP NOT NULL,
+	client_id VARCHAR(100) NOT NULL,
+	super_client_id VARCHAR(100) NOT NULL,
+	FOREIGN KEY (client_id)
+	   REFERENCES oauth2_client(id)
+	   ON DELETE CASCADE
+	FOREIGN KEY (super_client_id)
+	   REFERENCES oauth2_client(id)
+	   ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX IF NOT EXISTS unique_installed_plugin 
+	on installed_plugin(installed_by,client_id,super_client_id);
diff --git a/src/main/resources/db/sqlite/V1.1__create_virtual_corpus_tables.sql b/src/main/resources/db/sqlite/V1.1__create_virtual_corpus_tables.sql
new file mode 100644
index 0000000..2a62319
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.1__create_virtual_corpus_tables.sql
@@ -0,0 +1,106 @@
+CREATE TABLE IF NOT EXISTS role (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  name VARCHAR(100) NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS role_index on role(name);
+
+CREATE TABLE IF NOT EXISTS privilege (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  name VARCHAR(20) NOT NULL,
+  role_id INTEGER NOT NULL,
+  FOREIGN KEY (role_id) 
+  	REFERENCES role (id)
+  	ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS privilege_index on privilege(name, role_id);
+
+
+CREATE TABLE IF NOT EXISTS user_group (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  name VARCHAR(100) NOT NULL,
+  description VARCHAR(255)DEFAULT NULL,
+  status VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  deleted_by VARCHAR(100) DEFAULT NULL
+);
+
+CREATE INDEX IF NOT EXISTS user_group_index ON user_group(status);
+CREATE UNIQUE INDEX IF NOT EXISTS user_group_name on user_group(name);
+
+
+CREATE TABLE IF NOT EXISTS user_group_member (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  user_id VARCHAR(100) NOT NULL,
+  group_id INTEGER NOT NULL,
+  status VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  deleted_by VARCHAR(100) DEFAULT NULL,
+-- interprets now as localtime and save it as UTC
+  status_date timestamp DEFAULT (datetime('now','localtime')),
+  FOREIGN KEY (group_id) 
+  	REFERENCES user_group (id)
+  	ON DELETE CASCADE
+); 
+
+CREATE UNIQUE INDEX IF NOT EXISTS  user_group_member_index 
+	ON user_group_member(user_id,group_id);
+CREATE INDEX IF NOT EXISTS user_group_member_status_index 
+	ON user_group_member(status);
+
+CREATE TABLE IF NOT EXISTS group_member_role (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  group_member_id INTEGER NOT NULL,
+  role_id INTEGER NOT NULL,
+  FOREIGN KEY (group_member_id)
+  	REFERENCES user_group_member (id)
+  	ON DELETE CASCADE,
+  FOREIGN KEY (role_id) 
+  	REFERENCES role (id)
+  	ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS group_member_role_index 
+	ON group_member_role(group_member_id,role_id);
+
+
+CREATE TABLE IF NOT EXISTS virtual_corpus (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  name VARCHAR(255) NOT NULL,
+  type VARCHAR(100) NOT NULL,
+  required_access VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  description VARCHAR(255) DEFAULT NULL,
+  status VARCHAR(100) DEFAULT NULL,
+  corpus_query TEXT NOT NULL,
+  definition VARCHAR(255) DEFAULT NULL,
+  is_cached BOOLEAN DEFAULT 0
+);
+
+--CREATE INDEX IF NOT EXISTS virtual_corpus_owner_index ON virtual_corpus(created_by);
+--CREATE INDEX IF NOT EXISTS virtual_corpus_type_index ON virtual_corpus(type);
+--CREATE UNIQUE INDEX IF NOT EXISTS  virtual_corpus_unique_name 
+--	ON virtual_corpus(name,created_by);
+
+CREATE TABLE IF NOT EXISTS virtual_corpus_access (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  virtual_corpus_id INTEGER NOT NULL,
+  user_group_id INTEGER NOT NULL,
+  status VARCHAR(100) NOT NULL,
+  created_by VARCHAR(100) NOT NULL,
+  approved_by VARCHAR(100) DEFAULT NULL,
+  deleted_by VARCHAR(100) DEFAULT NULL,
+  FOREIGN KEY (user_group_id) 
+  	REFERENCES user_group (id)
+  	ON DELETE CASCADE,
+  FOREIGN KEY (virtual_corpus_id) 
+  	REFERENCES virtual_corpus (id)
+  	ON DELETE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS virtual_corpus_status_index 
+	ON virtual_corpus_access(status);
+CREATE UNIQUE INDEX IF NOT EXISTS virtual_corpus_access_unique_index 
+	ON virtual_corpus_access(virtual_corpus_id,user_group_id);
+
diff --git a/src/main/resources/db/sqlite/V1.2__triggers.sql b/src/main/resources/db/sqlite/V1.2__triggers.sql
new file mode 100644
index 0000000..d2e9b6d
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.2__triggers.sql
@@ -0,0 +1,22 @@
+CREATE TRIGGER IF NOT EXISTS insert_member_status AFTER INSERT ON user_group_member
+     BEGIN
+      UPDATE user_group_member 
+      SET status_date = DATETIME('now', 'localtime')  
+      WHERE rowid = new.rowid;
+     END;
+
+CREATE TRIGGER IF NOT EXISTS update_member_status AFTER UPDATE ON user_group_member	
+     BEGIN
+      UPDATE user_group_member 
+      SET status_date = (datetime('now','localtime'))  
+      WHERE rowid = old.rowid;
+     END;   
+
+CREATE TRIGGER IF NOT EXISTS delete_member  AFTER UPDATE ON user_group
+	WHEN new.status = "DELETED" AND  old.status <> "DELETED"
+	BEGIN
+		UPDATE user_group_member 
+		SET status = "DELETED"
+		WHERE group_id = new.id;
+	END;

+	
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.3__create_admin_table.sql b/src/main/resources/db/sqlite/V1.3__create_admin_table.sql
new file mode 100644
index 0000000..5066799
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.3__create_admin_table.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS admin(
+	id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+	user_id VARCHAR(100) NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS admin_index on admin(user_id);
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql b/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql
new file mode 100644
index 0000000..58b8ccf
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql
@@ -0,0 +1,98 @@
+-- EM: modified from Michael Hanl version
+
+-- oauth2 db tables
+CREATE TABLE IF NOT EXISTS oauth2_client (
+	id VARCHAR(100) PRIMARY KEY NOT NULL,
+	name VARCHAR(100) NOT NULL,
+	secret VARCHAR(255) DEFAULT NULL,
+	type VARCHAR(50) NOT NULL,
+	super BOOLEAN DEFAULT FALSE,
+	redirect_uri TEXT DEFAULT NULL,
+	description VARCHAR(255) NOT NULL,
+	registered_by VARCHAR(100) NOT NULL,
+	--url_hashcode INTEGER,	
+	url TEXT DEFAULT NULL
+);
+
+--CREATE UNIQUE INDEX client_url_index on oauth2_client(url_hashcode);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_scope (
+	id VARCHAR(100) PRIMARY KEY NOT NULL
+);
+
+-- authorization tables are not needed if using cache
+
+--CREATE TABLE IF NOT EXISTS oauth2_authorization (
+--	id INTEGER PRIMARY KEY AUTOINCREMENT,
+--	code VARCHAR(255) NOT NULL,
+--	client_id VARCHAR(100) NOT NULL,
+--	user_id VARCHAR(100) NOT NULL,
+--	redirect_uri TEXT DEFAULT NULL,
+--	created_date TIMESTAMP NOT NULL,
+--	expiry_date TIMESTAMP NOT NULL,
+--	is_revoked BOOLEAN DEFAULT 0,
+--	total_attempts INTEGER DEFAULT 0,
+--	user_auth_time TIMESTAMP NOT NULL,
+--	nonce TEXT DEFAULT NULL,
+--	FOREIGN KEY (client_id)
+--	   REFERENCES oauth2_client(id)
+--);
+--
+--CREATE UNIQUE INDEX authorization_index on oauth2_authorization(code, client_id);
+--
+--CREATE TABLE IF NOT EXISTS oauth2_authorization_scope (
+--	id INTEGER PRIMARY KEY AUTOINCREMENT,
+--	authorization_id INTEGER NOT NULL,
+--	scope_id VARCHAR(100) NOT NULL,
+--	FOREIGN KEY (authorization_id)
+--	   REFERENCES oauth2_authorization(id),
+--	FOREIGN KEY (scope_id)
+--	   REFERENCES oauth2_access_scope(id)
+--);
+--
+--CREATE UNIQUE INDEX authorization_scope_index on 
+--	oauth2_authorization_scope(authorization_id, scope_id);
+
+CREATE TABLE IF NOT EXISTS oauth2_refresh_token (
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	token VARCHAR(255) NOT NULL,
+	user_id VARCHAR(100) DEFAULT NULL,
+	user_auth_time TIMESTAMP NOT NULL,
+	created_date TIMESTAMP NOT NULL,
+	expiry_date TIMESTAMP NULL,
+	is_revoked BOOLEAN DEFAULT 0,
+	client VARCHAR(100) NOT NULL,
+	FOREIGN KEY (client)
+	   REFERENCES oauth2_client(id)
+	   ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_refresh_token_scope (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token (
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	token VARCHAR(255) NOT NULL,
+	user_id VARCHAR(100) DEFAULT NULL,
+	created_date TIMESTAMP NOT NULL,
+	expiry_date TIMESTAMP NOT NULL,
+	is_revoked BOOLEAN DEFAULT 0,
+	user_auth_time TIMESTAMP NOT NULL,
+	refresh_token INTEGER DEFAULT NULL,
+	client VARCHAR(100) DEFAULT NULL,
+	FOREIGN KEY (client)
+	   REFERENCES oauth2_client(id)
+	   ON DELETE CASCADE
+	FOREIGN KEY (refresh_token)
+	   REFERENCES oauth2_refresh_token(id)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token_scope (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
+);
+
diff --git a/src/main/resources/db/sqlite/V1.5__oauth2_triggers.sql b/src/main/resources/db/sqlite/V1.5__oauth2_triggers.sql
new file mode 100644
index 0000000..96329af
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.5__oauth2_triggers.sql
@@ -0,0 +1,15 @@
+--CREATE TRIGGER insert_authorization_date AFTER INSERT ON oauth2_authorization
+--     BEGIN
+--      UPDATE oauth2_authorization
+--      SET created_date = DATETIME('now', 'localtime')  
+--      WHERE rowid = new.rowid;
+--     END;
+--     
+--
+--CREATE TRIGGER insert_access_token_date AFTER INSERT ON oauth2_access_token
+--     BEGIN
+--      UPDATE oauth2_access_token
+--      SET created_date = DATETIME('now', 'localtime')  
+--      WHERE rowid = new.rowid;
+--     END;
+     
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.6__user_tables.sql b/src/main/resources/db/sqlite/V1.6__user_tables.sql
new file mode 100644
index 0000000..9df4a20
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.6__user_tables.sql
@@ -0,0 +1,4 @@
+CREATE TABLE IF NOT EXISTS default_setting (
+    username VARCHAR(100) PRIMARY KEY,
+    settings TEXT NOT NULL
+);
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.7__query_references.sql b/src/main/resources/db/sqlite/V1.7__query_references.sql
new file mode 100644
index 0000000..5d71fc6
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.7__query_references.sql
@@ -0,0 +1,16 @@
+--CREATE TABLE IF NOT EXISTS query_reference (
+--  id INTEGER PRIMARY KEY AUTOINCREMENT,
+--  name VARCHAR(255) NOT NULL,
+--  type VARCHAR(100) NOT NULL,
+--  required_access VARCHAR(100) NOT NULL,
+--  created_by VARCHAR(100) NOT NULL,
+--  description VARCHAR(255) DEFAULT NULL,
+--  status VARCHAR(100) DEFAULT NULL,
+--  query TEXT NOT NULL,
+--  definition VARCHAR(255) DEFAULT NULL
+--);
+--
+--CREATE INDEX query_reference_owner_index ON query_reference(created_by);
+--CREATE INDEX query_reference_type_index ON query_reference(type);
+--CREATE UNIQUE INDEX query_reference_unique_name 
+--	ON query_reference(name,created_by);
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.8__oauth2_alteration.sql b/src/main/resources/db/sqlite/V1.8__oauth2_alteration.sql
new file mode 100644
index 0000000..3c0eaaa
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.8__oauth2_alteration.sql
@@ -0,0 +1,3 @@
+-- ND:2020-04-15
+-- Needs to be dropped as null-URLs result in non-unique hash codes
+DROP INDEX IF EXISTS client_url_index;
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.9__query_alteration.sql b/src/main/resources/db/sqlite/V1.9__query_alteration.sql
new file mode 100644
index 0000000..02cf486
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.9__query_alteration.sql
@@ -0,0 +1,37 @@
+ALTER TABLE virtual_corpus 
+ADD COLUMN query_type VARCHAR(100) NOT NULL;
+
+ALTER TABLE virtual_corpus 
+ADD COLUMN query TEXT DEFAULT NULL;
+
+ALTER TABLE virtual_corpus 
+ADD COLUMN query_language VARCHAR(100) DEFAULT NULL;
+
+ALTER TABLE virtual_corpus 
+RENAME COLUMN corpus_query TO koral_query;
+
+ALTER TABLE virtual_corpus
+RENAME TO query;
+
+DROP INDEX IF EXISTS virtual_corpus_owner_index;
+DROP INDEX IF EXISTS virtual_corpus_type_index;
+DROP INDEX IF EXISTS  virtual_corpus_unique_name; 
+
+CREATE INDEX IF NOT EXISTS query_owner_index ON query(created_by);
+CREATE INDEX IF NOT EXISTS query_type_index ON query(type);
+CREATE UNIQUE INDEX IF NOT EXISTS  query_unique_name 
+	ON query(name,created_by);
+
+
+ALTER TABLE virtual_corpus_access 
+RENAME COLUMN virtual_corpus_id TO query_id;
+
+ALTER TABLE virtual_corpus_access
+RENAME TO query_access;
+
+
+DROP TABLE IF EXISTS query_reference;
+
+DROP INDEX IF EXISTS query_reference_owner_index;
+DROP INDEX IF EXISTS query_reference_type_index;
+DROP INDEX IF EXISTS  query_reference_unique_name;
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1__initial_version.sql b/src/main/resources/db/sqlite/V1__initial_version.sql
new file mode 100644
index 0000000..1930f39
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1__initial_version.sql
@@ -0,0 +1,75 @@
+ CREATE TABLE IF NOT EXISTS annotation(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	code VARCHAR(20) NOT NULL,
+	type VARCHAR(20) NOT NULL,
+	text VARCHAR(20) NULL,
+	description VARCHAR(100) NOT NULL,
+	de_description VARCHAR(100)
+);
+
+CREATE UNIQUE INDEX annotation_index ON annotation (code, type);
+
+CREATE TABLE IF NOT EXISTS annotation_layer(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	foundry_id INTEGER NOT NULL,
+	layer_id INTEGER NOT NULL,
+	description VARCHAR(255) NOT NULL,
+	FOREIGN KEY (foundry_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX annotation_layer_index ON annotation_layer (foundry_id, layer_id);
+
+CREATE TABLE IF NOT EXISTS annotation_key(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	layer_id INTEGER NOT NULL,
+	key_id INTEGER NOT NULL,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation_layer (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (key_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX annotation_key_index ON annotation_key (layer_id, key_id);
+
+CREATE TABLE IF NOT EXISTS annotation_value(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	key_id INTEGER NOT NULL,
+	value_id INTEGER NOT NULL,
+	FOREIGN KEY (key_id)
+		REFERENCES annotation_key (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (value_id)
+		REFERENCES annotation (id)
+		ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX annotation_value_index ON annotation_value (key_id, value_id);
+
+CREATE TABLE resource(
+	id VARCHAR(100) PRIMARY KEY UNIQUE NOT NULL,
+	de_title VARCHAR(100) NOT NULL,
+	en_title VARCHAR(100) NOT NULL,
+	en_description VARCHAR(100)	
+);
+
+CREATE TABLE resource_layer(
+	id INTEGER PRIMARY KEY AUTOINCREMENT,
+	resource_id VARCHAR(100) NOT NULL,
+	layer_id INTEGER NOT NULL,
+	FOREIGN KEY (resource_id)
+		REFERENCES resource (id)
+		ON DELETE CASCADE,
+	FOREIGN KEY (layer_id)
+		REFERENCES annotation_layer (id)
+		ON DELETE CASCADE	
+);
+
+CREATE UNIQUE INDEX resource_layer_index ON resource_layer (resource_id, layer_id);
+
diff --git a/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql b/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql
new file mode 100644
index 0000000..d9d2c13
--- /dev/null
+++ b/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql
@@ -0,0 +1,118 @@
+-- dummy data only for testing
+
+-- user groups
+INSERT INTO user_group(name,status,created_by) 
+	VALUES ("marlin-group","ACTIVE","marlin");
+	
+INSERT INTO user_group(name,status,created_by) 
+	VALUES ("dory-group","ACTIVE","dory");
+
+INSERT INTO user_group(name,status,created_by) 
+	VALUES ("auto-group","HIDDEN","system");
+
+--INSERT INTO user_group(name,status,created_by) 
+--	VALUES ("all users","HIDDEN","system");
+
+INSERT INTO user_group(name,status,created_by, deleted_by) 
+	VALUES ("deleted-group","DELETED","dory", "dory");
+
+
+
+-- user group members
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "marlin",
+		(SELECT id from user_group where name = "marlin-group"),
+		"ACTIVE","marlin";
+
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "dory",
+		(SELECT id from user_group where name = "marlin-group"),
+		"ACTIVE","marlin";
+		
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "dory",
+		(SELECT id from user_group where name = "dory-group"),
+		"ACTIVE","dory";
+
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "nemo",
+		(SELECT id from user_group where name = "dory-group"),
+		"ACTIVE","dory";
+
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "marlin",
+		(SELECT id from user_group where name = "dory-group"),
+		"PENDING","dory";
+	
+INSERT INTO user_group_member(user_id, group_id, status, created_by, deleted_by)
+	SELECT "pearl",
+		(SELECT id from user_group where name = "dory-group"),
+		"DELETED","dory", "pearl";
+
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "pearl",
+		(SELECT id from user_group where name = "auto-group"),
+		"ACTIVE","system";
+
+INSERT INTO user_group_member(user_id, group_id, status, created_by)
+	SELECT "dory",
+		(SELECT id from user_group where name = "deleted-group"),
+		"ACTIVE","dory";
+
+		
+-- virtual corpora
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, koral_query) 
+	VALUES ("dory-vc", "PRIVATE", "VIRTUAL_CORPUS", "FREE", "dory", "test vc", "experimental",
+	'{"collection": { "@type": "koral:docGroup", "operands": [ { "@type": "koral:doc", "key": "corpusSigle", "match": "match:eq", "value": "GOE" }, { "@type": "koral:doc", "key": "creationDate", "match": "match:geq", "type": "type:date", "value": "1820" } ], "operation": "operation:and" }}');
+	
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, koral_query) 
+	VALUES ("group-vc", "PROJECT", "VIRTUAL_CORPUS", "PUB", "dory", "test vc", "experimental",
+	'{"collection": { "@type": "koral:docGroup", "operands": [ { "@type": "koral:doc", "key": "corpusSigle", "match": "match:eq", "value": "GOE" }, { "@type": "koral:doc", "key": "creationDate", "match": "match:leq", "type": "type:date", "value": "1810" } ], "operation": "operation:and" }}');
+
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, koral_query) 
+	VALUES ("system-vc", "SYSTEM", "VIRTUAL_CORPUS", "ALL", "system", "test vc", "experimental",
+	'{"collection":{"@type":"koral:doc","value":"GOE","match":"match:eq","key":"corpusSigle"}}');
+
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, koral_query) 
+	VALUES ("published-vc", "PUBLISHED", "VIRTUAL_CORPUS", "ALL", "marlin", "test vc", "experimental",
+	'{"collection":{"@type":"koral:doc","value":"GOE","match":"match:eq","key":"corpusSigle"}}');
+
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, koral_query) 
+	VALUES ("marlin-vc", "PRIVATE", "VIRTUAL_CORPUS", "FREE", "marlin", "marlin test share vc", "experimental",
+	'{"collection": { "@type": "koral:docGroup", "operands": [ { "@type": "koral:doc", "key": "corpusSigle", "match": "match:eq", "value": "GOE" }, { "@type": "koral:doc", "key": "creationDate", "match": "match:geq", "type": "type:date", "value": "1820" } ], "operation": "operation:and" }}');
+
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, koral_query) 
+	VALUES ("nemo-vc", "PRIVATE", "VIRTUAL_CORPUS", "ALL", "nemo", "nemo test vc", "experimental",
+	'{"collection":{"@type":"koral:doc","value":"GOE","match":"match:eq","key":"corpusSigle"}}');	
+	
+-- virtual corpus access
+INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+	SELECT 
+		(SELECT id from query where name = "group-vc"), 
+		(SELECT id from user_group where name = "dory-group"), 
+		"ACTIVE", "dory";
+
+--INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+--	SELECT 
+--		(SELECT id from query where name = "system-vc"), 
+--		(SELECT id from user_group where name = "all users"),
+--		"ACTIVE", "system";
+
+INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+	SELECT 
+		(SELECT id from query where name = "published-vc"),
+		(SELECT id from user_group where name = "marlin-group"),
+		"ACTIVE", "marlin";
+
+INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+	SELECT 
+		(SELECT id from query where name = "published-vc"),
+		(SELECT id from user_group where name = "auto-group"),
+		"HIDDEN", "system";
+
+	
+-- Summary user VC Lists
+-- dory: dory-vc, group-vc, system-vc
+-- nemo: group-vc, system-vc
+-- marlin: published-vc, system-vc
+-- pearl: system-vc, published-vc
diff --git a/src/main/resources/db/test/V3.3__insert_member_roles.sql b/src/main/resources/db/test/V3.3__insert_member_roles.sql
new file mode 100644
index 0000000..effbbcb
--- /dev/null
+++ b/src/main/resources/db/test/V3.3__insert_member_roles.sql
@@ -0,0 +1,52 @@
+-- member roles
+
+-- marlin group
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="marlin" AND group_id=1),
+	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
+	
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="marlin" AND group_id=1),
+	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
+	
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=1),
+	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
+	
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=1),
+	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
+	
+	
+-- dory group
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=2),
+	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
+	
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=2),
+	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
+	
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="nemo" AND group_id=2),
+	(SELECT id FROM role WHERE name = "USER_GROUP_MEMBER");
+	
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="nemo" AND group_id=2),
+	(SELECT id FROM role WHERE name = "VC_ACCESS_MEMBER");
+
+
+-- auto group
+INSERT INTO group_member_role(group_member_id,role_id)
+SELECT
+	(SELECT id FROM user_group_member WHERE user_id="pearl" AND group_id=3),
+	(SELECT id FROM role WHERE name = "VC_ACCESS_MEMBER");
+
diff --git a/src/main/resources/db/test/V3.4__insert_admins.sql b/src/main/resources/db/test/V3.4__insert_admins.sql
new file mode 100644
index 0000000..d8dbd8c
--- /dev/null
+++ b/src/main/resources/db/test/V3.4__insert_admins.sql
@@ -0,0 +1,2 @@
+-- dummy admin
+INSERT INTO admin(user_id) VALUES ("admin");
\ No newline at end of file
diff --git a/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql b/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
new file mode 100644
index 0000000..6b318e3
--- /dev/null
+++ b/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
@@ -0,0 +1,66 @@
+-- test clients
+
+-- plain secret value is "secret"
+INSERT INTO oauth2_client(id,name,secret,type,super,
+  redirect_uri,registered_by, description, url, registration_date, 
+  is_permitted) 
+VALUES ("fCBbQkAyYzI4NzUxMg","super confidential client",
+  "$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
+  "CONFIDENTIAL", 1, 
+  "https://korap.ids-mannheim.de/confidential/redirect", "system",
+  "Super confidential client.", 
+  "http://korap.ids-mannheim.de/confidential", CURRENT_TIMESTAMP, 1);
+
+  
+-- plain secret value is "secret"
+INSERT INTO oauth2_client(id,name,secret,type,super,
+  redirect_uri,registered_by, description,url,registration_date, 
+  is_permitted) 
+VALUES ("9aHsGW6QflV13ixNpez","non super confidential client",
+  "$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
+  "CONFIDENTIAL", 0,
+  "https://third.party.com/confidential/redirect", "system",
+  "Nonsuper confidential client.",
+  "http://third.party.com/confidential", CURRENT_TIMESTAMP,1);
+
+INSERT INTO oauth2_client(id,name,secret,type,super,
+  registered_by, description,url, registration_date, 
+  is_permitted,source) 
+VALUES ("52atrL0ajex_3_5imd9Mgw","confidential client 2",
+  "$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
+  "CONFIDENTIAL", 0,"system",
+  "Nonsuper confidential client plugin without redirect URI",
+  "http://example.client.de", CURRENT_TIMESTAMP, 1,'{"key":"value"}');
+
+INSERT INTO oauth2_client(id,name,secret,type,super,
+  redirect_uri, registered_by, description, url, registration_date, 
+  is_permitted,source)
+VALUES ("8bIDtZnH6NvRkW2Fq","public client plugin with redirect uri",
+  null, "PUBLIC", 0,
+  "https://third.party.client.com/redirect","system",
+  "Public client plugin with a registered redirect URI",
+  "http://third.party.client.com", CURRENT_TIMESTAMP,1,'{"key":"value"}');
+
+  
+INSERT INTO oauth2_client(id,name,secret,type,super,
+  registered_by, description, url, registration_date, 
+  is_permitted) 
+VALUES ("nW5qM63Rb2a7KdT9L","test public client",null,
+  "PUBLIC", 0, "Public client without redirect uri",
+  "system", "http://korap.ids-mannheim.de/public", 
+  CURRENT_TIMESTAMP, 1);
+  
+
+INSERT INTO oauth2_access_token(token,user_id,created_date, 
+expiry_date, user_auth_time)
+VALUES("fia0123ikBWn931470H8s5gRqx7Moc4p","marlin",1527776750000, 
+1527776750000, 1527690190000);
+
+INSERT INTO oauth2_refresh_token(token,user_id,user_auth_time, 
+created_date, expiry_date, client)
+VALUES("js9iQ4lw1Ri7fz06l0dXl8fCVp3Yn7vmq8","pearl",1496154350000, 
+1496240795000, 1527784020000, "nW5qM63Rb2a7KdT9L");
+
+-- EM: expiry date must be in epoch milis format for testing with sqlite,
+-- on the contrary, for testing using mysql use this format: "2018-05-31 16:27:00"
+-- otherwise criteria query using greaterThan does not work. 
diff --git a/src/main/resources/db/test/V3.6__insert_default_settings.sql b/src/main/resources/db/test/V3.6__insert_default_settings.sql
new file mode 100644
index 0000000..5bd5264
--- /dev/null
+++ b/src/main/resources/db/test/V3.6__insert_default_settings.sql
@@ -0,0 +1,2 @@
+INSERT INTO default_setting(username,settings) 
+	VALUES ("bubbles",'{"pos-foundry":"corenlp", "lemma-foundry":"opennlp"}');
diff --git a/src/main/resources/db/test/V3.7__insert_query_references.sql b/src/main/resources/db/test/V3.7__insert_query_references.sql
new file mode 100644
index 0000000..0507380
--- /dev/null
+++ b/src/main/resources/db/test/V3.7__insert_query_references.sql
@@ -0,0 +1,10 @@
+-- query references
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, 
+    koral_query, query, query_language) 
+	VALUES ("dory-q", "PRIVATE", "QUERY", "FREE", "dory", "test query", "experimental",
+	'{ "@type": "koral:token" }', "[]", "poliqarp");
+
+INSERT INTO query(name, type, query_type, required_access, created_by, description, status, 
+    koral_query, query, query_language) 
+	VALUES ("system-q", "SYSTEM", "QUERY", "FREE", "system", '"system" query', "experimental",
+	'{ "@type": "koral:token" }', "[]", "poliqarp");
diff --git a/src/main/resources/default-config.xml b/src/main/resources/default-config.xml
new file mode 100644
index 0000000..073bf26
--- /dev/null
+++ b/src/main/resources/default-config.xml
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:p="http://www.springframework.org/schema/p"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xmlns:aop="http://www.springframework.org/schema/aop"
+	xmlns:tx="http://www.springframework.org/schema/tx"
+	xmlns="http://www.springframework.org/schema/beans"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:cache="http://www.springframework.org/schema/cache"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans
+           http://www.springframework.org/schema/beans/spring-beans.xsd
+           http://www.springframework.org/schema/tx
+           http://www.springframework.org/schema/tx/spring-tx.xsd
+           http://www.springframework.org/schema/aop
+           http://www.springframework.org/schema/aop/spring-aop.xsd
+           http://www.springframework.org/schema/context
+           http://www.springframework.org/schema/context/spring-context.xsd
+           http://www.springframework.org/schema/util
+           http://www.springframework.org/schema/util/spring-util.xsd">
+
+	<context:component-scan
+		base-package="de.ids_mannheim.korap" />
+	<context:annotation-config />
+
+	<bean id="props"
+		class="org.springframework.beans.factory.config.PropertiesFactoryBean">
+		<property name="ignoreResourceNotFound" value="true" />
+		<property name="locations">
+			<array>
+				<value>classpath:kustvakt.conf</value>
+				<value>file:./kustvakt.conf</value>
+			</array>
+		</property>
+	</bean>
+
+	<bean id="placeholders"
+		class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
+		<property name="ignoreResourceNotFound" value="true" />
+		<property name="locations">
+			<array>
+				<value>classpath:properties/jdbc.properties</value>
+				<value>file:./jdbc.properties</value>
+				<value>classpath:properties/mail.properties</value>
+				<value>file:./mail.properties</value>
+				<value>classpath:properties/hibernate.properties</value>
+
+				<value>classpath:kustvakt.conf</value>
+				<value>file:./kustvakt.conf</value>
+			</array>
+		</property>
+	</bean>
+
+	<!-- <bean id='cacheManager' class='org.springframework.cache.ehcache.EhCacheCacheManager' 
+		p:cacheManager-ref='ehcache' /> <bean id='ehcache' class='org.springframework.cache.ehcache.EhCacheManagerFactoryBean' 
+		p:configLocation='classpath:ehcache.xml' p:shared='true' /> -->
+	<!--class="org.apache.commons.dbcp2.BasicDataSource" -->
+	<!-- org.springframework.jdbc.datasource.SingleConnectionDataSource -->
+	<bean id="dataSource"
+		class="org.apache.commons.dbcp2.BasicDataSource" lazy-init="true">
+		<property name="driverClassName"
+			value="${jdbc.driverClassName}" />
+		<property name="url" value="${jdbc.url}" />
+		<property name="username" value="${jdbc.username}" />
+		<property name="password" value="${jdbc.password}" />
+		<!-- relevant for single connection datasource and sqlite -->
+		<!-- <property name="suppressClose"> <value>true</value> </property> -->
+		<!--<property name="initialSize" value="2"/> -->
+		<property name="maxTotal" value="4" />
+		<property name="maxIdle" value="2" />
+		<property name="minIdle" value="1" />
+		<property name="maxWaitMillis" value="-1" />
+		<!--<property name="poolPreparedStatements" value="true"/> -->
+	</bean>
+
+	<bean id="c3p0DataSource"
+		class="com.mchange.v2.c3p0.ComboPooledDataSource"
+		destroy-method="close" lazy-init="true">
+		<property name="driverClass" value="${jdbc.driverClassName}" />
+		<property name="jdbcUrl" value="${jdbc.url}" />
+		<property name="user" value="${jdbc.username}" />
+		<property name="password" value="${jdbc.password}" />
+		<property name="maxPoolSize" value="4" />
+		<property name="minPoolSize" value="1" />
+		<property name="maxStatements" value="100" />
+		<!-- <property name="testConnectionOnCheckOut" value="true" /> -->
+		<property name="idleConnectionTestPeriod" value="60" />
+		<property name="testConnectionOnCheckin" value="true" />
+	</bean>
+
+	<bean id="sqliteDataSource"
+		class="org.springframework.jdbc.datasource.SingleConnectionDataSource"
+		lazy-init="true">
+		<property name="driverClassName"
+			value="${jdbc.driverClassName}" />
+		<property name="url" value="${jdbc.url}" />
+		<property name="username" value="${jdbc.username}" />
+		<property name="password" value="${jdbc.password}" />
+		<property name="connectionProperties">
+			<props>
+				<prop key="date_string_format">yyyy-MM-dd HH:mm:ss</prop>
+			</props>
+		</property>
+
+		<!-- relevant for single connection datasource and sqlite -->
+		<property name="suppressClose">
+			<value>true</value>
+		</property>
+		<!--<property name="initialSize" value="2"/> -->
+		<!--<property name="poolPreparedStatements" value="true"/> -->
+	</bean>
+
+	<!-- to configure database for sqlite, mysql, etc. migrations -->
+	<bean id="flywayConfig"
+		class="org.flywaydb.core.api.configuration.ClassicConfiguration">
+		<property name="baselineOnMigrate" value="true" />
+		<!-- <property name="validateOnMigrate" value="false" /> -->
+		<!-- <property name="cleanOnValidationError" value="true" /> -->
+		<property name="locations"
+			value="#{'${jdbc.schemaPath}'.split(',')}" />
+		<property name="dataSource" ref="dataSource" />
+		<!-- <property name="dataSource" ref="sqliteDataSource" /> -->
+		<!-- <property name="dataSource" ref="c3p0DataSource" /> -->
+		<property name="outOfOrder" value="true" />
+	</bean>
+
+	<bean id="flyway" class="org.flywaydb.core.Flyway"
+		init-method="migrate">
+		<constructor-arg ref="flywayConfig" />
+	</bean>
+
+	<bean id="entityManagerFactory"
+		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
+		depends-on="flyway">
+		<property name="dataSource" ref="dataSource" />
+		<!-- <property name="dataSource" ref="sqliteDataSource" /> -->
+		<!-- <property name="dataSource" ref="c3p0DataSource" /> -->
+
+		<property name="packagesToScan">
+			<array>
+				<value>de.ids_mannheim.korap.core.entity</value>
+				<value>de.ids_mannheim.korap.entity</value>
+				<value>de.ids_mannheim.korap.oauth2.entity</value>
+			</array>
+		</property>
+		<property name="jpaVendorAdapter">
+			<bean id="jpaVendorAdapter"
+				class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
+				<property name="databasePlatform"
+					value="${hibernate.dialect}" />
+			</bean>
+		</property>
+		<property name="jpaProperties">
+			<props>
+				<prop key="hibernate.dialect">${hibernate.dialect}</prop>
+				<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
+				<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
+				<prop key="hibernate.cache.use_query_cache">${hibernate.cache.use_query_cache}</prop>
+				<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache.use_second_level_cache}
+				</prop>
+				<prop key="hibernate.cache.provider_class">${hibernate.cache.provider}</prop>
+				<prop key="hibernate.cache.region.factory_class">${hibernate.cache.region.factory}</prop>
+				<prop key="hibernate.jdbc.time_zone">${hibernate.jdbc.time_zone}</prop>
+			</props>
+		</property>
+	</bean>
+
+	<tx:annotation-driven proxy-target-class="true"
+		transaction-manager="transactionManager" />
+
+	<bean id="transactionManager"
+		class="org.springframework.orm.jpa.JpaTransactionManager">
+		<property name="entityManagerFactory"
+			ref="entityManagerFactory" />
+	</bean>
+
+	<bean id="transactionTemplate"
+		class="org.springframework.transaction.support.TransactionTemplate">
+		<constructor-arg ref="transactionManager" />
+	</bean>
+
+	<!-- Data access objects -->
+	<bean id="adminDao" class="de.ids_mannheim.korap.dao.AdminDaoImpl" />
+	<bean id="authorizationDao"
+		class="de.ids_mannheim.korap.oauth2.dao.CachedAuthorizationDaoImpl" />
+
+	<!-- Filters -->
+	<!-- <bean id="authenticationFilter" class="de.ids_mannheim.korap.web.filter.AuthenticationFilter" 
+		> <property name="authenticationManager" ref="kustvakt_authenticationmanager"/> 
+		</bean> <bean id="piwikFilter" class="de.ids_mannheim.korap.web.filter.PiwikFilter" 
+		> <property name="authenticationManager" ref="kustvakt_authenticationmanager"/> 
+		</bean> -->
+
+	<!-- Services -->
+	<bean id="scopeService"
+		class="de.ids_mannheim.korap.oauth2.service.OAuth2ScopeServiceImpl" />
+
+	<!-- props are injected from default-config.xml -->
+	<bean id="kustvakt_config"
+		class="de.ids_mannheim.korap.config.FullConfiguration">
+		<constructor-arg name="properties" ref="props" />
+	</bean>
+
+	<bean id="initializator"
+		class="de.ids_mannheim.korap.init.Initializator" init-method="init">
+	</bean>
+
+	<!-- Krill -->
+	<bean id="search_krill"
+		class="de.ids_mannheim.korap.web.SearchKrill">
+		<constructor-arg value="${krill.indexDir}" />
+	</bean>
+
+	<!-- Validator -->
+	<bean id="validator"
+		class="de.ids_mannheim.korap.validator.ApacheValidator" />
+
+	<!-- URLValidator -->
+	<bean id="redirectURIValidator"
+		class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https" index="0" />
+		<constructor-arg index="1" type="long"
+			value="#{T(org.apache.commons.validator.routines.UrlValidator).ALLOW_LOCAL_URLS + 
+		T(org.apache.commons.validator.routines.UrlValidator).NO_FRAGMENTS}" />
+
+		<!-- <constructor-arg index="1" type="long"> <util:constant static-field="org.apache.commons.validator.routines.UrlValidator.NO_FRAGMENTS" 
+			/> </constructor-arg> -->
+	</bean>
+	<bean id="urlValidator"
+		class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https" />
+	</bean>
+
+	<!-- Rewrite -->
+	<bean id="foundryRewrite"
+		class="de.ids_mannheim.korap.rewrite.FoundryRewrite" />
+	<bean id="collectionRewrite"
+		class="de.ids_mannheim.korap.rewrite.CollectionRewrite" />
+	<bean id="virtualCorpusRewrite"
+		class="de.ids_mannheim.korap.rewrite.VirtualCorpusRewrite" />
+	<bean id="queryReferenceRewrite"
+		class="de.ids_mannheim.korap.rewrite.QueryReferenceRewrite" />
+
+	<util:list id="rewriteTasks"
+		value-type="de.ids_mannheim.korap.rewrite.RewriteTask">
+		<ref bean="foundryRewrite" />
+		<ref bean="collectionRewrite" />
+		<ref bean="virtualCorpusRewrite" />
+		<ref bean="queryReferenceRewrite" />
+	</util:list>
+
+	<bean id="rewriteHandler"
+		class="de.ids_mannheim.korap.rewrite.RewriteHandler">
+		<constructor-arg ref="rewriteTasks" />
+	</bean>
+
+	<bean id="kustvaktResponseHandler"
+		class="de.ids_mannheim.korap.web.KustvaktResponseHandler">
+	</bean>
+
+	<!-- OAuth -->
+	<bean id="oauth2ResponseHandler"
+		class="de.ids_mannheim.korap.web.OAuth2ResponseHandler">
+	</bean>
+
+	<bean name="kustvakt_encryption"
+		class="de.ids_mannheim.korap.encryption.KustvaktEncryption">
+		<constructor-arg ref="kustvakt_config" />
+	</bean>
+
+	<!-- authentication providers to use -->
+
+	<!-- <bean id="session_auth" class="de.ids_mannheim.korap.authentication.SessionAuthentication"> 
+		<constructor-arg type="de.ids_mannheim.korap.config.KustvaktConfiguration" 
+		ref="kustvakt_config" /> <constructor-arg type="de.ids_mannheim.korap.interfaces.EncryptionIface" 
+		ref="kustvakt_encryption" /> </bean> -->
+
+	<bean id="oauth2_auth"
+		class="de.ids_mannheim.korap.authentication.OAuth2Authentication" />
+
+	<util:list id="kustvakt_authproviders"
+		value-type="de.ids_mannheim.korap.interfaces.AuthenticationIface">
+		<!-- <ref bean="session_auth" /> -->
+		<ref bean="oauth2_auth" />
+	</util:list>
+
+
+	<!-- specify type for constructor argument -->
+	<bean id="authenticationManager"
+		class="de.ids_mannheim.korap.authentication.KustvaktAuthenticationManager">
+		<constructor-arg
+			type="de.ids_mannheim.korap.interfaces.EncryptionIface"
+			ref="kustvakt_encryption" />
+		<constructor-arg ref="kustvakt_config" />
+		<!-- inject authentication providers to use -->
+		<property name="providers" ref="kustvakt_authproviders" />
+	</bean>
+
+	<!-- todo: if db interfaces not loaded via spring, does transaction even 
+		work then? -->
+	<!-- the transactional advice (i.e. what 'happens'; see the <aop:advisor/> 
+		bean below) -->
+	<tx:advice id="txAdvice" transaction-manager="txManager">
+		<!-- the transactional semantics... -->
+		<tx:attributes>
+			<!-- all methods starting with 'get' are read-only -->
+			<tx:method name="get*" read-only="true"
+				rollback-for="KorAPException" />
+			<!-- other methods use the default transaction settings (see below) -->
+			<tx:method name="*" rollback-for="KorAPException" />
+		</tx:attributes>
+	</tx:advice>
+
+	<!-- ensure that the above transactional advice runs for any execution of 
+		an operation defined by the service interface -->
+	<aop:config>
+		<aop:pointcut id="service"
+			expression="execution(* de.ids_mannheim.korap.interfaces.db.*.*(..))" />
+		<aop:advisor advice-ref="txAdvice" pointcut-ref="service" />
+	</aop:config>
+
+	<!-- similarly, don't forget the PlatformTransactionManager -->
+	<bean id="txManager"
+		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
+		<!-- <property name="dataSource" ref="dataSource" /> -->
+		<property name="dataSource" ref="sqliteDataSource" />
+		<!-- <property name="dataSource" ref="c3p0DataSource" /> -->
+	</bean>
+
+	<!-- mail -->
+	<bean id="authenticator"
+		class="de.ids_mannheim.korap.service.MailAuthenticator">
+		<constructor-arg index="0" value="${mail.username}" />
+		<constructor-arg index="1" value="${mail.password}" />
+	</bean>
+	<bean id="smtpSession" class="jakarta.mail.Session"
+		factory-method="getInstance">
+		<constructor-arg index="0">
+			<props>
+				<prop key="mail.smtp.submitter">${mail.username}</prop>
+				<prop key="mail.smtp.auth">${mail.auth}</prop>
+				<prop key="mail.smtp.host">${mail.host}</prop>
+				<prop key="mail.smtp.port">${mail.port}</prop>
+				<prop key="mail.smtp.starttls.enable">${mail.starttls.enable}</prop>
+				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
+			</props>
+		</constructor-arg>
+		<constructor-arg index="1" ref="authenticator" />
+	</bean>
+	<bean id="mailSender"
+		class="org.springframework.mail.javamail.JavaMailSenderImpl">
+		<property name="username" value="${mail.username}" />
+		<property name="password" value="${mail.password}" />
+		<property name="session" ref="smtpSession" />
+	</bean>
+	<bean id="velocityEngine"
+		class="org.apache.velocity.app.VelocityEngine">
+		<constructor-arg index="0">
+			<props>
+				<prop key="resource.loader">class</prop>
+				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
+				</prop>
+			</props>
+		</constructor-arg>
+	</bean>
+</beans>
diff --git a/src/main/resources/default-lite-config.xml b/src/main/resources/default-lite-config.xml
new file mode 100644
index 0000000..da27995
--- /dev/null
+++ b/src/main/resources/default-lite-config.xml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:tx="http://www.springframework.org/schema/tx"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans
+           http://www.springframework.org/schema/beans/spring-beans.xsd
+           http://www.springframework.org/schema/tx
+           http://www.springframework.org/schema/tx/spring-tx.xsd
+           http://www.springframework.org/schema/context
+           http://www.springframework.org/schema/context/spring-context.xsd
+           http://www.springframework.org/schema/util
+           http://www.springframework.org/schema/util/spring-util.xsd">
+
+	<context:component-scan
+		base-package="
+		de.ids_mannheim.korap.core.service,
+		de.ids_mannheim.korap.core.web,
+		de.ids_mannheim.korap.web.filter, 
+		de.ids_mannheim.korap.web.utils,
+		de.ids_mannheim.korap.authentication.http" />
+	<context:annotation-config />
+
+	<bean id="placeholders"
+		class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
+		<property name="ignoreResourceNotFound" value="true" />
+		<property name="locations">
+			<array>
+				<value>classpath:properties/lite-jdbc.properties</value>
+				<value>file:./lite-jdbc.properties</value>
+				<value>classpath:properties/hibernate.properties</value>
+				<value>classpath:kustvakt-lite.conf</value>
+				<value>file:./kustvakt-lite.conf</value>
+			</array>
+		</property>
+	</bean>
+
+	<bean id="properties"
+		class="org.springframework.beans.factory.config.PropertiesFactoryBean">
+		<property name="ignoreResourceNotFound" value="true" />
+		<property name="locations">
+			<array>
+				<value>classpath:kustvakt-lite.conf</value>
+				<value>file:./kustvakt-lite.conf</value>
+			</array>
+		</property>
+	</bean>
+
+	<bean id="config"
+		class="de.ids_mannheim.korap.config.KustvaktConfiguration">
+		<constructor-arg index="0" name="properties"
+			ref="properties" />
+	</bean>
+
+	<!-- Database -->
+
+	<bean id="sqliteDataSource"
+		class="org.springframework.jdbc.datasource.SingleConnectionDataSource"
+		lazy-init="true">
+		<property name="driverClassName"
+			value="${jdbc.driverClassName}" />
+		<property name="url" value="${jdbc.url}" />
+		<property name="username" value="${jdbc.username}" />
+		<property name="password" value="${jdbc.password}" />
+		<property name="connectionProperties">
+			<props>
+				<prop key="date_string_format">yyyy-MM-dd HH:mm:ss</prop>
+			</props>
+		</property>
+
+		<!-- relevant for single connection datasource and sqlite -->
+		<property name="suppressClose">
+			<value>true</value>
+		</property>
+		<!--<property name="initialSize" value="2"/> -->
+		<!--<property name="poolPreparedStatements" value="true"/> -->
+	</bean>
+	<bean id="flywayConfig"
+		class="org.flywaydb.core.api.configuration.ClassicConfiguration">
+		<property name="baselineOnMigrate" value="true" />
+		<!-- <property name="validateOnMigrate" value="false" /> -->
+		<!-- <property name="cleanOnValidationError" value="true" /> -->
+		<property name="locations"
+			value="#{'${jdbc.schemaPath}'.split(',')}" />
+		<property name="dataSource" ref="sqliteDataSource" />
+		<property name="outOfOrder" value="true" />
+	</bean>
+
+	<bean id="flyway" class="org.flywaydb.core.Flyway"
+		init-method="migrate">
+		<constructor-arg ref="flywayConfig" />
+	</bean>
+
+
+	<bean id="entityManagerFactory"
+		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
+		<property name="dataSource" ref="sqliteDataSource" />
+
+		<property name="packagesToScan">
+			<array>
+				<value>de.ids_mannheim.korap.core.entity</value>
+			</array>
+		</property>
+		<property name="jpaVendorAdapter">
+			<bean id="jpaVendorAdapter"
+				class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
+				<property name="databasePlatform"
+					value="${hibernate.dialect}" />
+			</bean>
+		</property>
+		<property name="jpaProperties">
+			<props>
+				<prop key="hibernate.dialect">${hibernate.dialect}</prop>
+				<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
+				<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
+				<prop key="hibernate.cache.use_query_cache">${hibernate.cache.use_query_cache}</prop>
+				<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache.use_second_level_cache}
+				</prop>
+				<prop key="hibernate.cache.provider_class">${hibernate.cache.provider}</prop>
+				<prop key="hibernate.cache.region.factory_class">${hibernate.cache.region.factory}</prop>
+				<prop key="hibernate.jdbc.time_zone">${hibernate.jdbc.time_zone}</prop>
+			</props>
+		</property>
+	</bean>
+	<tx:annotation-driven proxy-target-class="true"
+		transaction-manager="transactionManager" />
+
+	<bean id="transactionManager"
+		class="org.springframework.orm.jpa.JpaTransactionManager">
+		<property name="entityManagerFactory"
+			ref="entityManagerFactory" />
+	</bean>
+
+	<bean id="transactionTemplate"
+		class="org.springframework.transaction.support.TransactionTemplate">
+		<constructor-arg ref="transactionManager" />
+	</bean>
+	<bean id="txManager"
+		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
+		<property name="dataSource" ref="sqliteDataSource" />
+	</bean>
+
+	<!-- Initialization -->
+	<!-- <bean id="annotationParser" class="de.ids_mannheim.korap.annotation.AnnotationParser" 
+		scope="singleton" /> -->
+	<!-- Search Engine -->
+	<bean id="search_krill"
+		class="de.ids_mannheim.korap.web.SearchKrill">
+		<constructor-arg value="${krill.indexDir}" />
+	</bean>
+
+
+	<!-- Filters -->
+	<!-- <bean id="APIVersionFilter" class="de.ids_mannheim.korap.web.APIVersionFilter" 
+		scope="singleton" /> -->
+
+	<!-- Authentication -->
+	<bean id="authenticationManager"
+		class="de.ids_mannheim.korap.authentication.DummyAuthenticationManager" />
+
+	<!-- Response handler -->
+	<bean id="kustvaktResponseHandler"
+		class="de.ids_mannheim.korap.web.KustvaktResponseHandler">
+	</bean>
+
+	<!-- Controllers -->
+	<!-- added via component-scan <bean id="annotationController" class="de.ids_mannheim.korap.web.controller.AnnotationController" 
+		/> <bean id="searchController" class="de.ids_mannheim.korap.web.controller.SearchController" 
+		/> <bean id="statisticController" class="de.ids_mannheim.korap.web.controller.StatisticController" 
+		/> -->
+	<!-- Services -->
+	<bean id="scopeService"
+		class="de.ids_mannheim.korap.oauth2.service.DummyOAuth2ScopeServiceImpl" />
+
+	<!-- DAO -->
+	<bean id="adminDao"
+		class="de.ids_mannheim.korap.dao.DummyAdminDaoImpl" />
+	<bean id="annotationDao"
+		class="de.ids_mannheim.korap.dao.AnnotationDao" />
+
+	<!-- DTO Converter -->
+	<bean id="annotationConverter"
+		class="de.ids_mannheim.korap.dto.converter.AnnotationConverter" />
+
+	<!-- Rewrite -->
+	<bean id="layerMapper"
+		class="de.ids_mannheim.korap.rewrite.LayerMapper" />
+	<bean id="foundryInject"
+		class="de.ids_mannheim.korap.rewrite.FoundryInject" />
+
+	<util:list id="rewriteTasks"
+		value-type="de.ids_mannheim.korap.rewrite.RewriteTask">
+		<ref bean="foundryInject" />
+	</util:list>
+
+	<bean id="rewriteHandler"
+		class="de.ids_mannheim.korap.rewrite.RewriteHandler">
+		<constructor-arg ref="rewriteTasks" />
+	</bean>
+</beans>
\ No newline at end of file
diff --git a/src/main/resources/ehcache.xml b/src/main/resources/ehcache.xml
new file mode 100644
index 0000000..077e260
--- /dev/null
+++ b/src/main/resources/ehcache.xml
@@ -0,0 +1,29 @@
+<ehcache
+	xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
+	updateCheck="true" monitoring="autodetect" dynamicConfig="true">
+
+	<defaultCache eternal='true' overflowToDisk='false' />
+	<!--maxBytesLocalHeap="200M" -->
+	<diskStore path="./cache_store" />
+
+
+	<!-- EM -->
+	<cache name='authorization' timeToLiveSeconds="1000"
+		eternal='false' memoryStoreEvictionPolicy="LRU"
+		maxEntriesLocalHeap="100" overflowToDisk='false' />
+
+	<cache name='access_token' timeToIdleSeconds="3600"
+		timeToLiveSeconds="15000" eternal='false'
+		memoryStoreEvictionPolicy="LRU" maxEntriesLocalHeap="500"
+		overflowToDisk='false' />
+
+	<!-- <cache name="named_vc" eternal="true" memoryStoreEvictionPolicy="LRU" 
+		maxBytesLocalHeap="256M" maxBytesLocalDisk="2G" diskExpiryThreadIntervalSeconds 
+		= "120" > <persistence strategy="localTempSwap"/> <sizeOfPolicy maxDepth="3000" 
+		maxDepthExceededBehavior="abort" /> -->
+
+	<cache name="total_results" timeToIdleSeconds="3600"
+		timeToLiveSeconds="15000" eternal='false'
+		memoryStoreEvictionPolicy="LRU" overflowToDisk='false'
+		maxEntriesLocalHeap="500" />
+</ehcache>
diff --git a/src/main/resources/embedded-ldap-default.conf b/src/main/resources/embedded-ldap-default.conf
new file mode 100644
index 0000000..00cd2a2
--- /dev/null
+++ b/src/main/resources/embedded-ldap-default.conf
@@ -0,0 +1,10 @@
+# default and sample configuration for an automatically starting
+# embedded LDAP server
+host=localhost
+port=3267
+searchBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=admin
+searchFilter=(uid=${login})
+useEmbeddedServer=true
+ldifFile=src/main/resources/korap-users.ldif
diff --git a/src/main/resources/embedded-ldap-example.conf b/src/main/resources/embedded-ldap-example.conf
new file mode 100644
index 0000000..0fa5f9f
--- /dev/null
+++ b/src/main/resources/embedded-ldap-example.conf
@@ -0,0 +1,10 @@
+# Sample configuration for an automatically starting
+# embedded LDAP server
+host=localhost
+port=3267
+searchBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=admin
+searchFilter=(|(uid=${login})(mail=${login}))
+useEmbeddedServer=true
+ldifFile=src/main/resources/example-users.ldif
diff --git a/src/main/resources/example-users.ldif b/src/main/resources/example-users.ldif
new file mode 100644
index 0000000..103582f
--- /dev/null
+++ b/src/main/resources/example-users.ldif
@@ -0,0 +1,39 @@
+dn: dc=example,dc=com
+dc: example
+ou: people
+objectClass: dcObject
+objectClass: organizationalUnit
+
+dn: ou=people,dc=example,dc=com
+ou: people
+objectClass: organizationalUnit
+
+dn: uid=user,ou=people,dc=example,dc=com
+cn: Rainer User
+uid: user
+mail: user@example.com
+userPassword: {BASE64}cGFzc3dvcmQ=
+
+dn: uid=user1,ou=people,dc=example,dc=com
+cn: User 1
+uid: user1
+mail: user1@example.com
+userPassword: {CLEAR}password1
+
+dn: uid=user2,ou=people,dc=example,dc=com
+cn: User 2
+uid: user2
+mail: user2@example.com
+userPassword: password2
+
+dn: uid=user3,ou=people,dc=example,dc=com
+cn: User 3
+uid: user3
+mail: user3@example.com
+userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
+
+dn: uid=user4,ou=people,dc=example,dc=com
+cn: user4
+uid: user4
+mail: user4@example.com
+userPassword: {SHA256}uXhzpA9zq+3Y1oWnzV5fheSpz7g+rCaIZkCggThQEis=
diff --git a/src/main/resources/free-resources.json b/src/main/resources/free-resources.json
new file mode 100644
index 0000000..b41581e
--- /dev/null
+++ b/src/main/resources/free-resources.json
@@ -0,0 +1,43 @@
+[{
+    "id": "WPD17",
+    "de_title" : "Deutsche Wikipedia Artikel 2017",
+    "en_title" : "German Wikipedia Articles 2017",
+    "en_description" : "A collection of articles of German Wikipedia from July 1st, 2017.",
+    "layers": [
+        "corenlp/p",
+        "marmot/m",
+        "marmot/p",
+        "opennlp/p",
+        "tt/l",
+        "tt/p"
+    ]
+},
+{
+    "id": "WDD17",
+    "de_title" : "Deutsche Wikipedia-Diskussionskorpus 2017",
+    "en_title" : "German Wikipedia talk corpus 2017",
+    "en_description" : "A collection of talk pages of German Wikipedia from July 1st, 2017.",
+    "layers": [
+        "corenlp/p",
+        "marmot/m",
+        "marmot/p",
+        "opennlp/p",
+        "tt/l",
+        "tt/p"
+    ]
+},
+{
+    "id": "WUD17",
+    "de_title" : "Deutsche Wikipedia-Benutzerdiskussionskorpus 2017",
+    "en_title" : "German Wikipedia user talk corpus 2017",
+    "en_description" : "A collection of user talk pages of German Wikipedia from July 1st, 2017.",
+    "layers": [
+        "corenlp/p",
+        "marmot/m",
+        "marmot/p",
+        "opennlp/p",
+        "tt/l",
+        "tt/p"
+    ]
+}
+]
\ No newline at end of file
diff --git a/src/main/resources/json/initial_super_client.json b/src/main/resources/json/initial_super_client.json
new file mode 100644
index 0000000..a8c997b
--- /dev/null
+++ b/src/main/resources/json/initial_super_client.json
@@ -0,0 +1,5 @@
+{
+  "name":"initial super client",
+  "type": "CONFIDENTIAL",
+  "description":"initial super client for user authentication"
+}
\ No newline at end of file
diff --git a/src/main/resources/kustvakt-lite.conf b/src/main/resources/kustvakt-lite.conf
new file mode 100644
index 0000000..b9315db
--- /dev/null
+++ b/src/main/resources/kustvakt-lite.conf
@@ -0,0 +1,35 @@
+# Krill settings
+
+# index dir
+krill.indexDir= sample-index
+
+krill.index.commit.count = 134217000
+krill.index.commit.log = log/krill.commit.log
+krill.index.commit.auto = 500
+krill.index.relations.max = 100
+
+# krill.namedVC=vc
+
+
+# Kustvakt settings
+
+api.welcome.message = Welcome to KorAP API!
+current.api.version = v1.0
+# multiple versions separated by space
+supported.api.version = v1.0
+
+# default
+kustvakt.base.url=/api/*
+
+# default foundries for layers
+default.foundry.partOfSpeech = tt
+default.foundry.lemma = tt
+default.foundry.orthography = opennlp
+default.foundry.dependency = malt
+default.foundry.constituent = corenlp
+default.foundry.morphology = marmot
+default.foundry.surface = base
+
+# server
+server.port=8089
+server.host=localhost
diff --git a/src/main/resources/kustvakt.conf b/src/main/resources/kustvakt.conf
new file mode 100644
index 0000000..a0a8554
--- /dev/null
+++ b/src/main/resources/kustvakt.conf
@@ -0,0 +1,112 @@
+#Krill
+krill.indexDir= sample-index
+
+krill.index.commit.count = 134217000
+krill.index.commit.log = log/krill.commit.log
+krill.index.commit.auto = 500
+krill.index.relations.max = 100
+# Directory path of virtual corpora to cache
+#krill.namedVC = vc
+
+# LDAP
+ldap.config = src/main/resources/embedded-ldap-example.conf
+
+# Kustvakt
+api.welcome.message = Welcome to KorAP API!
+current.api.version = v1.0
+# multiple versions separated by space
+# supported.api.version = v1.0
+
+# server
+server.port=8089
+server.host=localhost
+
+# mail settings
+mail.enabled = false
+mail.receiver = test@localhost
+mail.sender = noreply@ids-mannheim.de
+mail.address.retrieval = test
+
+# mail.templates
+template.group.invitation = notification.vm
+
+# default foundries for specific layers
+default.foundry.partOfSpeech = tt
+default.foundry.lemma = tt
+default.foundry.orthography = opennlp
+default.foundry.dependency = malt
+default.foundry.constituent = corenlp
+default.foundry.morphology = marmot
+default.foundry.surface = base
+
+# delete configuration (default hard)
+# delete.auto.group = hard
+delete.group = soft
+delete.group.member = soft
+
+
+# Availability regex only support |
+# It should be removed/commented when the data doesn't contain availability field.
+# 
+availability.regex.free = CC-BY.*
+availability.regex.public = ACA.*|QAO-NC
+availability.regex.all = QAO.*
+
+
+# Define resource filters for search and match info API
+# AuthenticationFilter activates authentication using OAuth2 tokens
+# DemoUserFilter allows access to API without login
+# 
+# Default values: AuthenticationFilter,DemoUserFilter
+#
+search.resource.filters=AuthenticationFilter,DemoUserFilter
+
+
+
+# options referring to the security module!
+
+# OAuth 
+# (see de.ids_mannheim.korap.constant.AuthenticationMethod for possible 
+# oauth.password.authentication values)
+oauth2.password.authentication = LDAP
+# used to determine native client, currently not used
+# oauth2.native.client.host = korap.ids-mannheim.de
+oauth2.max.attempts = 1
+# expiry in seconds (S), minutes (M), hours (H), days (D)
+oauth2.access.token.expiry = 1D
+oauth2.refresh.token.expiry = 90D
+# default 365D
+# oauth2.access.token.long.expiry = 365D
+# oauth2.refresh.token.long.expiry = 365D
+oauth2.authorization.code.expiry = 10M
+# scopes separated by space
+oauth2.default.scopes = search match_info 
+oauth2.client.credentials.scopes = client_info
+
+## see SecureRandom Number Generation Algorithms
+## optional
+# security.secure.random.algorithm=SHA1PRNG
+
+## see MessageDigest Algorithms
+## default MD5
+security.md.algoritm = SHA-256  
+
+### secure hash support: BCRYPT
+security.secure.hash.algorithm=BCRYPT
+security.encryption.loadFactor = 10
+
+# DEPRECATED
+# JWT
+security.jwt.issuer=korap.ids-mannheim.de
+security.sharedSecret=this-is-shared-secret-code-for-JWT-Signing.It-must-contains-minimum-256-bits
+
+## token expiration (used in other authentication provider than OAuth2)
+security.longTokenTTL=150D
+security.tokenTTL=72H
+security.shortTokenTTL=45M
+
+# Session authentication
+security.idleTimeoutDuration = 25M
+security.multipleLogIn = true
+security.loginAttemptNum = 3
+security.authAttemptTTL = 45M
\ No newline at end of file
diff --git a/src/main/resources/kustvakt.info b/src/main/resources/kustvakt.info
new file mode 100644
index 0000000..4a0344a
--- /dev/null
+++ b/src/main/resources/kustvakt.info
@@ -0,0 +1,8 @@
+kustvakt.version=${project.version}
+kustvakt.name=${project.name}
+
+# use this file to define the properties and logging file names
+kustvakt.properties=./kustvakt.conf
+kustvakt.logging=./log4j.properties
+kustvakt.cache=true
+kustvakt.cache_store=./store
\ No newline at end of file
diff --git a/src/main/resources/log4j2-debug.properties b/src/main/resources/log4j2-debug.properties
new file mode 100644
index 0000000..817e355
--- /dev/null
+++ b/src/main/resources/log4j2-debug.properties
@@ -0,0 +1,31 @@
+appenders = console, debugFile
+appender.console.type = Console
+appender.console.name = STDOUT
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = %d{yyyy-MM-dd, HH:mm:ss} %C{6} - %M%n %-5p: %m%n
+
+appender.debugFile.type = File
+appender.debugFile.name = DEBUGLOG
+appender.debugFile.fileName=./logs/debug.log
+appender.debugFile.layout.type=PatternLayout
+appender.debugFile.layout.pattern= %d{yyyy-MM-dd, HH:mm:ss} %C{6} - %M%n %-5p: %m%n
+
+rootLogger.level = warn
+rootLogger.appenderRefs = debugFile
+rootLogger.appenderRef.file.ref = DEBUGLOG
+rootLogger.additivity=false
+
+loggers=file
+logger.file.name=de.ids_mannheim.korap
+logger.file.level = debug
+logger.file.appenderRefs = debugFile
+logger.file.appenderRef.file.ref = DEBUGLOG
+logger.file.additivity=false
+
+
+#loggers=file
+#logger.file.name=com.sun.jersey.test.framework.spi.container
+#logger.file.level = info
+#logger.file.appenderRefs = file
+#logger.file.appenderRef.file.ref = ERRORLOG
+#logger.file.additivity=false
\ No newline at end of file
diff --git a/src/main/resources/log4j2.properties b/src/main/resources/log4j2.properties
new file mode 100644
index 0000000..618c58f
--- /dev/null
+++ b/src/main/resources/log4j2.properties
@@ -0,0 +1,45 @@
+#appenders = console, file, ldapFile
+appenders = console, file
+appender.console.type = Console
+appender.console.name = STDOUT
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = %d{yyyy-MM-dd, HH:mm:ss} %C{6} - %M%n %-5p: %m%n
+
+appender.file.type = File
+appender.file.name = MAINLOG
+appender.file.fileName=./logs/kustvakt.log
+appender.file.layout.type=PatternLayout
+appender.file.layout.pattern= %d{yyyy-MM-dd, HH:mm:ss} %C{6} - %M%n %-5p: %m%n
+
+#appender.ldapFile.type = File
+#appender.ldapFile.name = LDAP_LOG
+#appender.ldapFile.fileName=./logs/ldap.log
+#appender.ldapFile.layout.type=PatternLayout
+#appender.ldapFile.layout.pattern= %d{yyyy-MM-dd, HH:mm:ss} %C{6} - %M%n %-5p: %m%n
+
+
+rootLogger.level = warn
+rootLogger.appenderRefs = console,file
+rootLogger.appenderRef.file.ref = MAINLOG
+rootLogger.additivity=true
+
+loggers=file
+logger.file.name=de.ids_mannheim.korap
+logger.file.level = info
+logger.file.appenderRefs = file
+logger.file.appenderRef.file.ref = MAINLOG
+logger.file.additivity=false
+
+loggers=file
+logger.console.name=de.ids_mannheim.korap.web.controller.AuthenticationController
+logger.console.level = warn
+logger.console.appenderRefs = file
+logger.console.appenderRef.file.ref = MAINLOG
+logger.console.additivity=false
+
+#loggers=file
+#logger.file.name=de.ids_mannheim.korap.authentication.LdapAuth3
+#logger.file.level = info
+#logger.file.appenderRefs = file
+#logger.file.appenderRef.file.ref = LDAP_LOG
+#logger.file.additivity=false
\ No newline at end of file
diff --git a/src/main/resources/properties/basic-jdbc.properties b/src/main/resources/properties/basic-jdbc.properties
new file mode 100644
index 0000000..df8c513
--- /dev/null
+++ b/src/main/resources/properties/basic-jdbc.properties
@@ -0,0 +1,9 @@
+
+# Sqlite Settings
+
+jdbc.database=sqlite
+jdbc.driverClassName=org.sqlite.JDBC
+jdbc.url=jdbc:sqlite:basic.sqlite
+jdbc.username=pc
+jdbc.password=pc
+jdbc.schemaPath=db/sqlite, db/predefined,db/test
diff --git a/src/main/resources/properties/hibernate.properties b/src/main/resources/properties/hibernate.properties
new file mode 100644
index 0000000..e394a88
--- /dev/null
+++ b/src/main/resources/properties/hibernate.properties
@@ -0,0 +1,8 @@
+hibernate.dialect=org.hibernate.community.dialect.SQLiteDialect
+hibernate.hbm2ddl.auto=none
+hibernate.show_sql=false
+hibernate.cache.use_query_cache=false
+hibernate.cache.use_second_level_cache=false
+hibernate.cache.provider=org.hibernate.cache.EhCacheProvider
+hibernate.cache.region.factory=org.hibernate.cache.ehcache.EhCacheRegionFactory
+hibernate.jdbc.time_zone=UTC
\ No newline at end of file
diff --git a/src/main/resources/properties/jdbc.properties b/src/main/resources/properties/jdbc.properties
new file mode 100644
index 0000000..4b4a572
--- /dev/null
+++ b/src/main/resources/properties/jdbc.properties
@@ -0,0 +1,25 @@
+#-------------------------------------------------------------------------------
+# MySQL Settings
+# todo: test user and password in classpath file
+
+#jdbc.database=mysql
+#jdbc.driverClassName=com.mysql.jdbc.Driver
+#jdbc.url=jdbc:mysql://localhost:3306/kustvakt?useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC
+#jdbc.username=korap
+#jdbc.password=password
+
+# db.insert contains test data, omit it in production setting
+#jdbc.schemaPath=db.mysql, db.predefined
+
+
+#-------------------------------------------------------------------------------
+# Sqlite Settings
+
+jdbc.database=sqlite
+jdbc.driverClassName=org.sqlite.JDBC
+jdbc.url=jdbc:sqlite:data/db.sqlite
+# jdbc.url=jdbc:sqlite:kustvakt_init_test.sqlite
+jdbc.username=pc
+jdbc.password=pc
+# db.insert contains test data, omit it in production setting
+jdbc.schemaPath=db/sqlite, db/predefined
diff --git a/src/main/resources/properties/lite-jdbc.properties b/src/main/resources/properties/lite-jdbc.properties
new file mode 100644
index 0000000..c96fc5d
--- /dev/null
+++ b/src/main/resources/properties/lite-jdbc.properties
@@ -0,0 +1,9 @@
+#-------------------------------------------------------------------------------
+# Sqlite Settings
+
+jdbc.database=sqlite
+jdbc.driverClassName=org.sqlite.JDBC
+jdbc.url=jdbc:sqlite:data/liteDB.sqlite
+jdbc.username=pc
+jdbc.password=pc
+jdbc.schemaPath=db/lite
\ No newline at end of file
diff --git a/src/main/resources/properties/mail.properties b/src/main/resources/properties/mail.properties
new file mode 100644
index 0000000..29d1ca1
--- /dev/null
+++ b/src/main/resources/properties/mail.properties
@@ -0,0 +1,7 @@
+mail.host = localhost
+mail.port = 25
+mail.connectiontimeout = 3000
+mail.auth = false
+mail.starttls.enable = false
+mail.username = username
+mail.password = password
\ No newline at end of file
diff --git a/src/main/resources/service.properties b/src/main/resources/service.properties
new file mode 100644
index 0000000..2f428f5
--- /dev/null
+++ b/src/main/resources/service.properties
@@ -0,0 +1,4 @@
+kustvakt.version=${project.version}
+krill.version=${krill.version}
+koral.version=${koral.version}
+kustvakt.cache=true
\ No newline at end of file
diff --git a/src/main/resources/templates/notification.vm b/src/main/resources/templates/notification.vm
new file mode 100644
index 0000000..254ad83
--- /dev/null
+++ b/src/main/resources/templates/notification.vm
@@ -0,0 +1,12 @@
+<html>
+    <body>
+        <h3>Hi $username,</h3>
+        <p> you have been invited to group $group by $inviter! </p>
+        <p> Please login to <a href="https://korap.ids-mannheim.de/kalamar">KorAP</a> with your
+            account to accept or reject this invitation within 30 minutes. </p>
+        <p>After joining a group, you will be able to access virtual corpora shared with members of
+            the group. If you would like share your virtual corpus to a group, please contact the
+            group admin.</p>
+        <p> This is an automated generated email. Please do not reply directly to this e-mail. </p>
+    </body>
+</html>
\ No newline at end of file
diff --git a/src/main/resources/validation.properties b/src/main/resources/validation.properties
new file mode 100644
index 0000000..15e09cd
--- /dev/null
+++ b/src/main/resources/validation.properties
@@ -0,0 +1,42 @@
+# The ESAPI validator does many security checks on input, such as canonicalization
+# and whitelist validation. Note that all of these validation rules are applied *after*
+# canonicalization. Double-encoded characters (even with different encodings involved,
+# are never allowed.
+#
+# To use:
+#
+# First set up a pattern below. You can choose any name you want, prefixed by the word
+# "Validation." For example:
+#   Validation.Email=^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\\.[a-zA-Z]{2,4}$
+# 
+# Then you can validate in your code against the pattern like this:
+#     ESAPI.validator().isValidInput("User Email", input, "Email", maxLength, allowNull);
+# Where maxLength and allowNull are set for you needs, respectively.
+#
+# But note, when you use boolean variants of validation functions, you lose critical 
+# canonicalization. It is preferable to use the "get" methods (which throw exceptions) and
+# and use the returned user input which is in canonical form. Consider the following:
+#  
+# try {
+#    someObject.setEmail(ESAPI.validator().getValidInput("User Email", input, "Email", maxLength, allowNull));
+#
+# Validator.SafeString=^[.;:\\-\\p{Alnum}\\p{Space}]{0,1024}$
+# Validator.password_cap=((?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,20})
+Validator.SafeString=^[.;:,=\\*\/\/_()\\-0-9\\p{L}\\p{Space}]{0,1024}$
+Validator.email=^[A-Za-z0-9._%'-]+@[A-Za-z0-9.-]+\\.[a-zA-Z]{2,4}$
+Validator.ipddress=^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
+Validator.url=^(ht|f)tp(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\:\\'\\/\\\\\\+=&amp;%\\$#_]*)?$
+# Validator.CreditCard=^(\\d{4}[- ]?){3}\\d{4}$
+# Validator.SSN=^(?!000)([0-6]\\d{2}|7([0-6]\\d|7[012]))([ -]?)(?!00)\\d\\d\\3(?!0000)\\d{4}$
+
+# as used by apache commons validator for strings
+# Validator.string=^[\\.;:,\\=&\\*\\/\\/_()\\[\\]@\\|\\-0-9\\p{L}\\p{Space}]{0,1024}$
+
+#Validator.username=^[A-Za-z_.\\d]{6,15}$ by Hanl
+# 21.04.17/FB
+Validator.username=^[A-Za-z_.\\d]{3,20}$
+Validator.password=^((?=.*\\d)(?=.*[A-Za-z])(?!.*[\\(\\)-]).{8,20})$
+
+
+# EM
+Validator.setting=[a-zA-Z0-9_-]+
\ No newline at end of file