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"" );
+#
+# 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\\-\\.\\?\\,\\:\\'\\/\\\\\\+=&%\\$#_]*)?$
+# 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