From ecafbe6e252e13d40c5971c5d60217003fd7b84b Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Tue, 30 Jun 2020 11:56:23 +0200
Subject: [PATCH] MAINT: remove SQLite and AWI related stuff

---
 .../server/datatype/SQLiteDatatype.java       |  26 -
 .../server/jobs/extension/AWIBoxLoan.java     | 562 ------------------
 .../extension/AWIBoxLoanCuratorEmail.java     |  92 ---
 .../jobs/extension/AWIBoxLoanModel.java       | 154 -----
 .../AWIBoxLoanRequestLoanCuratorEmail.java    |  24 -
 .../AWIBoxLoanRequestReturnCuratorEmail.java  |  22 -
 .../server/jobs/extension/JobException.java   |  42 --
 .../jobs/extension/SQLiteTransaction.java     | 172 ------
 .../server/jobs/extension/TestAWIBoxLoan.java | 277 ---------
 9 files changed, 1371 deletions(-)
 delete mode 100644 src/main/java/caosdb/server/datatype/SQLiteDatatype.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/JobException.java
 delete mode 100644 src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java
 delete mode 100644 src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java

diff --git a/src/main/java/caosdb/server/datatype/SQLiteDatatype.java b/src/main/java/caosdb/server/datatype/SQLiteDatatype.java
deleted file mode 100644
index 9a7c2f1c..00000000
--- a/src/main/java/caosdb/server/datatype/SQLiteDatatype.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
- *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * ** end header
- */
-package caosdb.server.datatype;
-
-@DatatypeDefinition(name = "SQLite")
-public class SQLiteDatatype extends ReferenceDatatype {}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
deleted file mode 100644
index 30dd77e0..00000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
+++ /dev/null
@@ -1,562 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import static caosdb.server.permissions.Role.ANONYMOUS_ROLE;
-
-import caosdb.server.accessControl.UserSources;
-import caosdb.server.database.exceptions.EntityDoesNotExistException;
-import caosdb.server.datatype.SingleValue;
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.entity.Message;
-import caosdb.server.entity.Message.MessageType;
-import caosdb.server.entity.wrapper.Property;
-import caosdb.server.jobs.JobAnnotation;
-import caosdb.server.jobs.core.CheckPropValid;
-import caosdb.server.permissions.EntityACL;
-import caosdb.server.permissions.EntityACLFactory;
-import caosdb.server.permissions.EntityPermission;
-import caosdb.server.query.Query;
-import caosdb.server.transaction.Delete;
-import caosdb.server.transaction.Insert;
-import caosdb.server.transaction.Update;
-import caosdb.server.utils.EntityStatus;
-import caosdb.server.utils.ServerMessages;
-import caosdb.server.utils.Utils;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@JobAnnotation(transaction = caosdb.server.transaction.WriteTransaction.class, loadAlways = true)
-public class AWIBoxLoan extends AWIBoxLoanModel {
-
-  public Logger logger = LoggerFactory.getLogger(getClass());
-  private static final Message UNIQUE_USER =
-      new Message("The user must have a unique combination of first name and last name!");
-  private static final Message BOX_HAS_LOAN =
-      new Message(
-          "This box cannot be be requested right now because it appears to have a Loan property attached to it. This usually means, that the box is already requested or borrowed by someone.");
-
-  @Override
-  protected void run() {
-    try {
-      if (isAnonymous()
-          && (getTransaction() instanceof Delete
-              || isAcceptBorrowUpdateLoan()
-              || isConfirmLoanUpdateLoan()
-              || isRejectReturnUpdateLoan()
-              || isAcceptReturnUpdateLoan()
-              || isManualReturnUpdateBox()
-              || isManualReturnUpdateLoan()
-              || !(isRequestLoanSetUser()
-                  || isRequestLoanInsertLoan()
-                  || isRequestLoanUpdateBox()
-                  || isRequestReturnSetUser()
-                  || isRequestReturnUpdateLoan()))) {
-        addError(ServerMessages.AUTHORIZATION_ERROR);
-        getContainer()
-            .addMessage(
-                new Message(MessageType.Info, 0, "Anonymous users have restricted permissions."));
-        return;
-      }
-    } catch (EntityDoesNotExistException exc) {
-      addError(ServerMessages.AUTHORIZATION_ERROR);
-      getContainer()
-          .addMessage(
-              new Message(MessageType.Info, 0, "Anonymous users have restricted permissions."));
-      return;
-    }
-
-    try {
-      if (!(getTransaction() instanceof Delete
-          || isRequestLoanSetUser()
-          || isRequestLoanInsertLoan()
-          || isManualReturnUpdateLoan()
-          || isManualReturnUpdateBox()
-          || isRequestLoanUpdateBox()
-          || isAcceptBorrowUpdateLoan()
-          || isConfirmLoanUpdateLoan()
-          || isRequestReturnSetUser()
-          || isRequestReturnUpdateLoan()
-          || isRejectReturnUpdateLoan())) {
-        isAcceptReturnUpdateLoan();
-      }
-    } catch (EntityDoesNotExistException exc) {
-      // ignore
-    }
-
-    // special ACL for boxes, loans and persons
-    try {
-      if (getTransaction() instanceof Insert) {
-        for (EntityInterface e : getContainer()) {
-          if (isBoxRecord(e)) {
-            e.setEntityACL(EntityACL.combine(e.getEntityACL(), getBoxACL()));
-          } else if (isLoanRecord(e)) {
-            e.setEntityACL(EntityACL.combine(e.getEntityACL(), getLoanACL()));
-          } else if (isPersonRecord(e)) {
-            e.setEntityACL(EntityACL.combine(e.getEntityACL(), getPersonACL()));
-          }
-        }
-      }
-    } catch (EntityDoesNotExistException e) {
-    }
-  }
-
-  boolean isManualReturnUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasManualReturnLoanProperties(e)) {
-        logger.trace("isManualReturnUpdateLoan: false");
-        return false;
-      }
-    }
-    for (EntityInterface e : getContainer()) {
-      removeAnonymousPermissions(e);
-    }
-    logger.trace("isManualReturnUpdateLoan: true");
-    return true;
-  }
-
-  boolean hasManualReturnLoanProperties(EntityInterface e) {
-    for (Property p : e.getProperties()) {
-      if (isReturnedProperty(p)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  boolean isReturnedProperty(Property p) {
-    return Objects.equals(p.getId(), getReturnedId());
-  }
-
-  void removeAnonymousPermissions(EntityInterface e) {
-    EntityACLFactory f = new EntityACLFactory();
-    f.deny(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY);
-    f.deny(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY);
-    e.setEntityACL(EntityACL.combine(e.getEntityACL(), f.create()));
-  }
-
-  boolean isManualReturnUpdateBox() {
-    for (EntityInterface e : getContainer()) {
-      if (!isBoxRecord(e) || !hasManualReturnBoxProperties(e)) {
-        logger.trace("isManualReturnUpdateBox: false");
-        return false;
-      }
-    }
-    logger.trace("isManualReturnUpdateBox: true");
-    return true;
-  }
-
-  boolean hasManualReturnBoxProperties(EntityInterface e) {
-    return validBoxHasLoanProperty(e) && boxLoanHasReturnedProperty(e);
-  }
-
-  boolean boxLoanHasReturnedProperty(EntityInterface e) {
-    try {
-      EntityInterface validBox = retrieveValidEntity(e.getId());
-
-      for (Property p : validBox.getProperties()) {
-        if (isLoanProperty(p)) {
-          return validLoanHasReturnedProperty(p.getId());
-        }
-      }
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-    return false;
-  }
-
-  boolean validLoanHasReturnedProperty(Integer id) {
-    try {
-      EntityInterface validLoan = retrieveValidEntity(id);
-      for (Property p : validLoan.getProperties()) {
-        if (isReturnedProperty(p)) {
-          return true;
-        }
-      }
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-    return false;
-  }
-
-  boolean isLoanProperty(Property p) {
-    return Objects.equals(p.getId(), getLoanId());
-  }
-
-  private boolean isRejectReturnUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasRejectReturnProperties(e)) {
-        logger.trace("isRejectReturnUpdateLoan: false");
-        return false;
-      }
-    }
-    for (EntityInterface e : getContainer()) {
-      removeDestinationProperty(e);
-    }
-    logger.trace("isRejectReturnUpdateLoan: true");
-    return true;
-  }
-
-  void removeDestinationProperty(EntityInterface e) {
-    Iterator<Property> iterator = e.getProperties().iterator();
-    while (iterator.hasNext()) {
-      Property p = iterator.next();
-      if (isDestinationProperty(p)) {
-        iterator.remove();
-      }
-    }
-  }
-
-  boolean hasRejectReturnProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isDestinationProperty(p)) {
-        found = true;
-      } else if (isReturnAcceptedProperty(p) || isReturnRequestProperty(p)) {
-        logger.trace("hasRejectReturnProperties: false");
-        return false;
-      }
-    }
-    logger.trace("hasRejectReturnProperties found: {}", found);
-    return found;
-  }
-
-  boolean isDestinationProperty(Property p) {
-    return Objects.equals(p.getId(), getDestinationId());
-  }
-
-  boolean isAcceptReturnUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasAcceptReturnProperties(e)) {
-        logger.trace("isAcceptReturnUpdateLoan: false");
-        return false;
-      }
-    }
-    logger.trace("isAcceptReturnUpdateLoan: true");
-    return true;
-  }
-
-  boolean hasAcceptReturnProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isReturnAcceptedProperty(p)) {
-        found = true;
-      }
-    }
-    return found;
-  }
-
-  boolean isConfirmLoanUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasConfirmLoanProperties(e)) {
-        logger.trace("isConfirmLoanUpdateLoan: false");
-        return false;
-      }
-    }
-    // switch from destination to location
-    for (EntityInterface e : getContainer()) {
-      switchDestinationToLocation(e);
-    }
-    logger.trace("isConfirmLoanUpdateLoan: true");
-    return true;
-  }
-
-  void switchDestinationToLocation(EntityInterface e) {
-    Iterator<Property> iterator = e.getProperties().iterator();
-    EntityInterface p = retrieveValidEntity(getLocationId());
-    while (iterator.hasNext()) {
-      Property next = iterator.next();
-      if (isDestinationProperty(next)) {
-        iterator.remove();
-        p.setValue(next.getValue());
-        e.addProperty(new Property(p));
-        break;
-      }
-    }
-  }
-
-  boolean hasConfirmLoanProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isLentProperty(p)) {
-        found = true;
-      } else if (isReturnAcceptedProperty(p) || isDestinationProperty(p)) {
-        logger.trace("hasConfirmLoanProperties: false");
-        return false;
-      }
-    }
-    logger.trace("hasConfirmLoanProperties found: {}", found);
-    return found;
-  }
-
-  boolean isAcceptBorrowUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasLoanAcceptProperties(e)) {
-        logger.trace("isAcceptBorrowUpdateLoan: false");
-        return false;
-      }
-    }
-    logger.trace("isAcceptBorrowUpdateLoan: true");
-    return true;
-  }
-
-  boolean hasLoanAcceptProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isLoanAcceptProperty(p)) {
-        found = true;
-      } else if (isLentProperty(p) || isReturnAcceptedProperty(p)) {
-        logger.trace("hasLoanAcceptProperties: false");
-        return false;
-      }
-    }
-    logger.trace("hasLoanAcceptProperties found: {}", found);
-    return found;
-  }
-
-  boolean isReturnAcceptedProperty(Property p) {
-    return Objects.equals(p.getId(), getReturnAcceptedId());
-  }
-
-  boolean isLentProperty(Property p) {
-    return Objects.equals(p.getId(), getLentId());
-  }
-
-  boolean isLoanAcceptProperty(Property p) {
-    return Objects.equals(p.getId(), getLoanAcceptedId());
-  }
-
-  EntityACL getPersonACL() {
-    // same as loan acl - property updates are allowed for anonymous.
-    return getLoanACL();
-  }
-
-  EntityACL getLoanACL() {
-    EntityACLFactory f = new EntityACLFactory();
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY);
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY);
-    return f.create();
-  }
-
-  EntityACL getBoxACL() {
-    EntityACLFactory f = new EntityACLFactory();
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY);
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY);
-    return f.create();
-  }
-
-  boolean isAnonymous() {
-    boolean ret = getUser().hasRole(UserSources.ANONYMOUS_ROLE);
-    logger.trace("is Anonymous: {}", ret);
-    return ret;
-  }
-
-  boolean isRequestReturnUpdateLoan() {
-    // is UPDATE transaction
-    if (getTransaction() instanceof Update) {
-      // Container has only loan elements with special properties
-      for (EntityInterface e : getContainer()) {
-        if (!isLoanRecord(e) || !hasOnlyAllowedLoanProperties4RequestReturn(e)) {
-          logger.trace("isRequestReturnUpdateLoan: false");
-          return false;
-        }
-        setReturnRequestedDate(e);
-      }
-      appendJob(AWIBoxLoanRequestReturnCuratorEmail.class);
-      logger.trace("isRequestReturnUpdateLoan: true");
-      return true;
-    }
-    logger.trace("isRequestReturnUpdateLoan: false");
-    return false;
-  }
-
-  boolean isRequestReturnSetUser() {
-    // same as request_loan.set_user
-    logger.trace("isRequestReturnSetUser: ->");
-    return isRequestLoanSetUser();
-  }
-
-  boolean isRequestLoanUpdateBox() {
-    // is UPDATE transaction
-    if (getTransaction() instanceof Update) {
-      // Container has only box elements
-      for (EntityInterface e : getContainer()) {
-        if (validBoxHasLoanProperty(e)) {
-          e.addError(BOX_HAS_LOAN);
-          return false;
-        }
-        if (!isBoxRecord(e) || !hasOnlyAllowedBoxProperties4RequestLoan(e)) {
-          return false;
-        }
-        // TODO this breaks the box loan functionality if any other prior changes have been made to
-        // the box
-        //        appendJob(e, CheckNoAdditionalPropertiesPresent.class);
-      }
-      return true;
-    }
-    return false;
-  }
-
-  boolean validBoxHasLoanProperty(EntityInterface e) {
-    try {
-      EntityInterface validBox = retrieveValidEntity(e.getId());
-      for (Property p : validBox.getProperties()) {
-        if (isLoanProperty(p)) {
-          return true;
-        }
-      }
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-    return false;
-  }
-
-  /** Has only one new property -> Loan. */
-  boolean hasOnlyAllowedBoxProperties4RequestLoan(EntityInterface e) {
-    int count = 0;
-    for (Property p : e.getProperties()) {
-      if (p.getEntityStatus() == EntityStatus.QUALIFIED && Objects.equals(p.getId(), getLoanId())) {
-        count++;
-      }
-    }
-
-    // Box has only one update, a loan property
-    return count == 1;
-  }
-
-  boolean isRequestLoanInsertLoan() {
-    // is INSERT transaction
-    if (getTransaction() instanceof Insert) {
-      // Container has only loan elements
-      for (EntityInterface e : getContainer()) {
-        if (!isLoanRecord(e)) {
-          return false;
-        }
-      }
-      for (EntityInterface e : getContainer()) {
-        if (isAnonymous()) {
-          setCuratorAsOwner(e);
-        }
-        setLoanRequestDate(e);
-        // TODO this check breaks the box loan functionality if any other changes have been made to
-        // the box entity
-        //        appendJob(e, CheckNoAdditionalPropertiesPresent.class);
-        //        appendJob(e, CheckNoOverridesPresent.class);
-      }
-      appendJob(AWIBoxLoanRequestLoanCuratorEmail.class);
-      return true;
-    }
-    return false;
-  }
-
-  void setCuratorAsOwner(EntityInterface e) {
-    e.setEntityACL(EntityACL.getOwnerACLFor(caosdb.server.permissions.Role.create("curator")));
-  }
-
-  void setReturnRequestedDate(EntityInterface e) {
-    // TODO setDateProperty(e, getReturnRequestedId());
-  }
-
-  private void setDateProperty(EntityInterface e, Integer propertyId) {
-    // TODO
-    EntityInterface p = retrieveValidEntity(propertyId);
-    p.setValue(getTransaction().getTimestamp());
-    e.addProperty(new Property(p));
-  }
-
-  void setLoanRequestDate(EntityInterface e) {
-    // TODO setDateProperty(e, getLoanRequestedId());
-  }
-
-  boolean isRequestLoanSetUser() {
-    // is INSERT/UPDATE transaction
-    // Container has only one element, user
-    if ((getTransaction() instanceof Update || getTransaction() instanceof Insert)
-        && getContainer().size() == 1
-        && isPersonRecord(getContainer().get(0))
-        && checkUniqueName(getContainer().get(0))
-        && checkEmail(getContainer().get(0))) {
-      // TODO this check breaks the box loan functionality if any other changes have been made to
-      // the box entity
-      //      appendJob(getContainer().get(0), CheckNoAdditionalPropertiesPresent.class);
-      //      appendJob(getContainer().get(0), CheckNoOverridesPresent.class);
-      logger.trace("isRequestReturnSetUser: true");
-      return true;
-    }
-    logger.trace("isRequestReturnSetUser: false");
-    return false;
-  }
-
-  boolean checkEmail(EntityInterface entity) {
-    runJobFromSchedule(entity, CheckPropValid.class);
-    for (Property p : entity.getProperties()) {
-      if (Objects.equals(p.getId(), getEmailID()) && p.getValue() instanceof SingleValue) {
-        if (!Utils.isRFC822Compliant(((SingleValue) p.getValue()).toDatabaseString())) {
-          p.addError(ServerMessages.EMAIL_NOT_WELL_FORMED);
-        } else {
-          p.setName("email"); // TODO fix webinterface to use lower-case email
-          p.setNameOverride(false);
-        }
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private boolean checkUniqueName(EntityInterface entity) {
-    String firstName = null;
-    String lastName = null;
-    Query q =
-        new Query(
-                "FIND "
-                    + getPersonID().toString()
-                    + " WITH "
-                    + getFirstNameId().toString()
-                    + "='"
-                    + firstName
-                    + "' AND "
-                    + getLastNameId().toString()
-                    + "='"
-                    + lastName
-                    + "'",
-                getUser())
-            .execute(getTransaction().getAccess());
-    List<Integer> resultSet = q.getResultSet();
-    if (resultSet.isEmpty()
-        || (resultSet.size() == 1 && Objects.equals(resultSet.get(0), entity.getId()))) {
-      return true;
-    }
-    entity.addError(UNIQUE_USER);
-    return false;
-  }
-
-  /**
-   * Has only 5/6 new/updated properties: content, returnRequested, destination, Borrower, comment
-   * (optional), location
-   *
-   * @throws Message
-   */
-  boolean hasOnlyAllowedLoanProperties4RequestReturn(EntityInterface e) {
-    runJobFromSchedule(e, CheckPropValid.class);
-    //    appendJob(e, CheckNoOverridesPresent.class);
-
-    boolean foundReturnRequested = false;
-    for (Property p : e.getProperties()) {
-      if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update
-        if (isReturnRequestProperty(p)) {
-          foundReturnRequested = true;
-        } else if (isReturnAcceptedProperty(p) || isReturnedProperty(p)) {
-          logger.trace("hasOnlyAllowedLoanProperties4RequestReturn: false");
-          return false; // this is not a returnRequest, return has already been accepted
-        }
-      }
-    }
-    logger.trace("hasOnlyAllowedLoanProperties4RequestReturn found: {}", foundReturnRequested);
-    return foundReturnRequested;
-  }
-
-  boolean isReturnRequestProperty(Property p) {
-    return Objects.equals(p.getId(), getReturnRequestedId());
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java
deleted file mode 100644
index 2bf51e76..00000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.datetime.DateTimeInterface;
-import caosdb.server.CaosDBServer;
-import caosdb.server.ServerProperties;
-import caosdb.server.datatype.ReferenceValue;
-import caosdb.server.datatype.SingleValue;
-import caosdb.server.datatype.Value;
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.entity.wrapper.Property;
-import caosdb.server.utils.mail.Mail;
-import java.util.TimeZone;
-
-public abstract class AWIBoxLoanCuratorEmail extends AWIBoxLoanModel {
-
-  public static final String KEY_EXT_AWI_CURATOR_EMAIL = "EXT_AWI_BOX_CURATOR_EMAIL";
-  protected static final String FROM_EMAIL =
-      CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_EMAIL);
-  protected static final String FROM_NAME =
-      CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_NAME);
-  protected static final String CURATOR_EMAIL =
-      CaosDBServer.getServerProperty(KEY_EXT_AWI_CURATOR_EMAIL);
-
-  protected String loanToString(EntityInterface e) {
-    StringBuilder s = new StringBuilder();
-    for (Property p : e.getProperties()) {
-      s.append("\n");
-      s.append(p.getName());
-      s.append(": ");
-      if (p.getValue() instanceof ReferenceValue) {
-        Integer id = ((ReferenceValue) p.getValue()).getId();
-        if (isBorrowerProperty(p)) {
-          s.append(borrowerToString(id));
-        } else if (isBoxProperty(p)) {
-          s.append(boxToString(id));
-        } else {
-          s.append(valueToString(p.getValue()));
-        }
-      } else {
-        s.append(valueToString(p.getValue()));
-      }
-    }
-    return s.toString();
-  }
-
-  private String valueToString(Value value) {
-    if (value == null) {
-      return "";
-    }
-    if (value instanceof DateTimeInterface) {
-      return ((DateTimeInterface) value).toDateTimeString(TimeZone.getDefault());
-    }
-    if (value instanceof SingleValue) {
-      return ((SingleValue) value).toDatabaseString();
-    } else {
-      return value.toString();
-    }
-  }
-
-  private String boxToString(Integer id) {
-    StringBuilder s = new StringBuilder();
-    EntityInterface box = retrieveValidEntity(id);
-    for (Property sp : box.getProperties()) {
-      if (isNumberProperty(sp) || isContentProperty(sp) || isLocationProperty(sp)) {
-        s.append("\n  ");
-        s.append(sp.getName());
-        s.append(": ");
-        s.append(valueToString(sp.getValue()));
-      }
-    }
-    return s.toString();
-  }
-
-  private String borrowerToString(Integer id) {
-    StringBuilder s = new StringBuilder();
-    EntityInterface borrower = retrieveValidEntity(id);
-    for (Property sp : borrower.getProperties()) {
-      s.append("\n  ");
-      s.append(sp.getName());
-      s.append(": ");
-      s.append(valueToString(sp.getValue()));
-    }
-    return s.toString();
-  }
-
-  protected void sendCuratorEmail(String body, String subject) {
-    for (String addr : CURATOR_EMAIL.split(" ")) {
-      Mail m = new Mail(FROM_NAME, FROM_EMAIL, null, addr, subject, body);
-      m.send();
-    }
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java
deleted file mode 100644
index 92a8a240..00000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.server.CaosDBServer;
-import caosdb.server.database.exceptions.EntityDoesNotExistException;
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.entity.Role;
-import caosdb.server.entity.wrapper.Property;
-import caosdb.server.jobs.ContainerJob;
-import caosdb.server.utils.Utils;
-import java.util.Objects;
-
-public abstract class AWIBoxLoanModel extends ContainerJob {
-
-  /** Is Record and has single user parent. */
-  boolean isPersonRecord(EntityInterface entity) {
-    try {
-      return entity.getParents().size() == 1
-          && Objects.equals(
-              retrieveValidIDByName(entity.getParents().get(0).getName()), getPersonID());
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-  }
-
-  /** Is Record an has single box parent. */
-  boolean isBoxRecord(EntityInterface e) {
-    return e.getRole() == Role.Record
-        && e.getParents().size() == 1
-        && Objects.equals(e.getParents().get(0).getId(), getBoxId());
-  }
-
-  /** Is Record and has single loan parent */
-  boolean isLoanRecord(EntityInterface e) {
-    try {
-      return e.getRole() == Role.Record
-          && e.getParents().size() == 1
-          && Objects.equals(retrieveValidIDByName(e.getParents().get(0).getName()), getLoanId());
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-  }
-
-  boolean isBoxProperty(Property p) {
-    return Objects.equals(p.getId(), getBoxId());
-  }
-
-  boolean isBorrowerProperty(Property p) {
-    return Objects.equals(p.getId(), getBorrowerId());
-  }
-
-  boolean isLocationProperty(Property p) {
-    return Objects.equals(p.getId(), getLocationId());
-  }
-
-  boolean isContentProperty(Property p) {
-    return Objects.equals(p.getId(), getContentId());
-  }
-
-  boolean isNumberProperty(Property p) {
-    return Objects.equals(p.getId(), getNumberId());
-  }
-
-  Integer getIdOf(String string) {
-    String id = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_ID");
-    if (id != null && Utils.isNonNullInteger(id)) {
-      return Integer.parseInt(id);
-    }
-    String name = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_NAME");
-    if (name == null || name.isEmpty()) {
-      name = string;
-    }
-    return retrieveValidIDByName(name);
-  }
-
-  Integer getBorrowerId() {
-    return getIdOf("Borrower");
-  }
-
-  Integer getCommentId() {
-    return getIdOf("comment");
-  }
-
-  Integer getLocationId() {
-    return getIdOf("Location");
-  }
-
-  Integer getDestinationId() {
-    return getIdOf("destination");
-  }
-
-  Integer getContentId() {
-    return getIdOf("Content");
-  }
-
-  Integer getBoxId() {
-    return getIdOf("Box");
-  }
-
-  Integer getLoanId() {
-    return getIdOf("Loan");
-  }
-
-  Integer getPersonID() {
-    return getIdOf("Person");
-  }
-
-  Integer getEmailID() {
-    return getIdOf("email");
-  }
-
-  Integer getLoanAcceptedId() {
-    return getIdOf("loanAccepted");
-  }
-
-  Integer getReturnAcceptedId() {
-    return getIdOf("returnAccepted");
-  }
-
-  Integer getLentId() {
-    return getIdOf("lent");
-  }
-
-  Integer getLoanRequestedId() {
-    return getIdOf("loanRequested");
-  }
-
-  Integer getExhaustContentsId() {
-    return getIdOf("exhaustContents");
-  }
-
-  Integer getExpectedReturnId() {
-    return getIdOf("expectedReturn");
-  }
-
-  Integer getReturnRequestedId() {
-    return getIdOf("returnRequested");
-  }
-
-  Integer getLastNameId() {
-    return getIdOf("lastName");
-  }
-
-  Integer getFirstNameId() {
-    return getIdOf("firstName");
-  }
-
-  Integer getNumberId() {
-    return getIdOf("Number");
-  }
-
-  Integer getReturnedId() {
-    return getIdOf("returned");
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java
deleted file mode 100644
index ff490e99..00000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.jobs.JobAnnotation;
-import caosdb.server.jobs.JobExecutionTime;
-
-@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION)
-public class AWIBoxLoanRequestLoanCuratorEmail extends AWIBoxLoanCuratorEmail {
-
-  private static final String SUBJECT = "Box Loan Requests";
-
-  @Override
-  protected void run() {
-    StringBuilder body = new StringBuilder("LOAN REQUEST(S):");
-
-    if (!getContainer().isEmpty()) {
-      for (EntityInterface e : getContainer()) {
-        body.append("\n");
-        body.append(loanToString(e));
-      }
-      this.sendCuratorEmail(body.toString(), SUBJECT);
-    }
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java
deleted file mode 100644
index 373ce0b8..00000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.jobs.JobAnnotation;
-import caosdb.server.jobs.JobExecutionTime;
-
-@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION)
-public class AWIBoxLoanRequestReturnCuratorEmail extends AWIBoxLoanCuratorEmail {
-  private static final String SUBJECT = "Box Return Requests";
-
-  @Override
-  protected void run() {
-    if (!getContainer().isEmpty()) {
-      StringBuilder body = new StringBuilder("RETURN REQUEST(S):");
-      for (EntityInterface e : getContainer()) {
-        body.append("\n");
-        body.append(loanToString(e));
-      }
-      this.sendCuratorEmail(body.toString(), SUBJECT);
-    }
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/JobException.java b/src/main/java/caosdb/server/jobs/extension/JobException.java
deleted file mode 100644
index e41475bb..00000000
--- a/src/main/java/caosdb/server/jobs/extension/JobException.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
- *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * ** end header
- */
-package caosdb.server.jobs.extension;
-
-import caosdb.server.jobs.Job;
-
-public class JobException extends RuntimeException {
-
-  /** */
-  private static final long serialVersionUID = -2802983933079999366L;
-
-  private final Job job;
-
-  public JobException(final Job job, final Exception e) {
-    super(e);
-    this.job = job;
-  }
-
-  public Job getJob() {
-    return this.job;
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java b/src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java
deleted file mode 100644
index 4882f0af..00000000
--- a/src/main/java/caosdb/server/jobs/extension/SQLiteTransaction.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
- *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * ** end header
- */
-package caosdb.server.jobs.extension;
-
-import caosdb.server.CaosDBException;
-import caosdb.server.entity.FileProperties;
-import caosdb.server.entity.Message;
-import caosdb.server.jobs.EntityJob;
-import caosdb.server.utils.FileUtils;
-import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.locks.ReentrantLock;
-
-public class SQLiteTransaction extends EntityJob {
-  private static HashMap<String, ReentrantLock> lockedTables = new HashMap<String, ReentrantLock>();
-
-  @SuppressWarnings("unlikely-arg-type")
-  @Override
-  public final void run() {
-    Connection con = null;
-    try {
-      if (getEntity().hasFileProperties()) {
-        if (getEntity().getFileProperties().retrieveFromFileSystem() != null) {
-
-          if (getEntity().hasMessage("execute")) {
-            try {
-              if (!lockedTables.containsKey(
-                  getEntity().getFileProperties().getFile().getCanonicalPath())) {
-                synchronized (lockedTables) {
-                  lockedTables.put(
-                      getEntity().getFileProperties().getFile().getCanonicalPath(),
-                      new ReentrantLock());
-                }
-              }
-
-              lockedTables.get(getEntity().getFileProperties().getFile().getCanonicalPath()).lock();
-              try {
-                con =
-                    getConnection(
-                        getEntity()
-                            .getFileProperties()
-                            .retrieveFromFileSystem()
-                            .getCanonicalPath());
-                final Statement stmt = con.createStatement();
-
-                //
-                final List<Message> msgs = getEntity().getMessages("execute");
-                Collections.sort(msgs);
-                for (final Message m : msgs) {
-                  getEntity().removeMessage(m);
-                  stmt.execute(m.getBody());
-                  final ResultSet rs = stmt.getResultSet();
-                  final int updateCount = stmt.getUpdateCount();
-                  if (rs != null) {
-                    final ResultSetMetaData rsmd = rs.getMetaData();
-                    final int columns = rsmd.getColumnCount();
-                    final StringBuilder s = new StringBuilder();
-                    if (columns > 0) {
-                      s.append("|");
-                      for (int i = 1; i <= columns; i++) {
-                        s.append(rsmd.getColumnName(i));
-                        s.append("|");
-                      }
-                      s.append("\n");
-                      while (rs.next()) {
-                        s.append("|");
-                        for (int i = 1; i <= columns; i++) {
-                          s.append(rs.getString(i));
-                          s.append("|");
-                        }
-                        s.append("\n");
-                      }
-                    } else {
-                      s.append("");
-                    }
-                    final Message mret = new Message("result", m.getCode(), null, s.toString());
-                    getEntity().addMessage(mret);
-
-                  } else if (updateCount != -1) {
-                    final Message mret =
-                        new Message(
-                            "result",
-                            m.getCode(),
-                            null,
-                            Integer.toString(updateCount) + " row(s) affected by the query.");
-                    getEntity().addMessage(mret);
-                  }
-                }
-              } finally {
-                lockedTables
-                    .get(getEntity().getFileProperties().getFile().getCanonicalPath())
-                    .unlock();
-              }
-              synchronized (
-                  lockedTables.get(getEntity().getFileProperties().getFile().getCanonicalPath())) {
-                if (!lockedTables
-                    .get(getEntity().getFileProperties().getFile().getCanonicalPath())
-                    .hasQueuedThreads()) {
-                  lockedTables.remove(
-                      lockedTables.get(
-                          getEntity().getFileProperties().getFile().getCanonicalPath()));
-                }
-              }
-            } finally {
-              if (con != null && !con.isClosed()) {
-                con.close();
-              }
-              resetSizeAndChecksum(getEntity().getFileProperties());
-            }
-          }
-        }
-      }
-    } catch (final NoSuchAlgorithmException e) {
-      throw new JobException(this, e);
-    } catch (final IOException e) {
-      throw new JobException(this, e);
-    } catch (final CaosDBException e) {
-      throw new JobException(this, e);
-    } catch (final ClassNotFoundException e) {
-      throw new JobException(this, e);
-    } catch (final SQLException e) {
-      throw new JobException(this, e);
-    }
-  }
-
-  private static void resetSizeAndChecksum(final FileProperties f)
-      throws NoSuchAlgorithmException, IOException {
-    f.setSize(f.getFile().length());
-    f.setChecksum(FileUtils.getChecksum(f.getFile()));
-  }
-
-  private static Connection getConnection(final String database)
-      throws ClassNotFoundException, SQLException {
-    if (!driverLoaded) {
-      Class.forName("org.sqlite.JDBC");
-      driverLoaded = true;
-    }
-    final Connection connection = DriverManager.getConnection("jdbc:sqlite:" + database);
-    return connection;
-  }
-
-  private static boolean driverLoaded = false;
-}
diff --git a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java
deleted file mode 100644
index 08a0e412..00000000
--- a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java
+++ /dev/null
@@ -1,277 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import static org.junit.Assert.assertEquals;
-
-import caosdb.server.entity.container.TransactionContainer;
-import caosdb.server.jobs.core.Mode;
-import caosdb.server.transaction.Transaction;
-import caosdb.server.utils.EntityStatus;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Callable;
-import org.apache.shiro.authc.AuthenticationException;
-import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.authz.AuthorizationException;
-import org.apache.shiro.authz.Permission;
-import org.apache.shiro.session.Session;
-import org.apache.shiro.subject.ExecutionException;
-import org.apache.shiro.subject.PrincipalCollection;
-import org.apache.shiro.subject.Subject;
-import org.junit.Test;
-
-public class TestAWIBoxLoan {
-
-  @Test
-  public void testNonAnonymousUser() {
-    TransactionContainer container = new TransactionContainer();
-    AWIBoxLoan j =
-        new AWIBoxLoan() {
-
-          @Override
-          protected Subject getUser() {
-            return new Subject() {
-
-              @Override
-              public void runAs(PrincipalCollection principals)
-                  throws NullPointerException, IllegalStateException {}
-
-              @Override
-              public PrincipalCollection releaseRunAs() {
-                return null;
-              }
-
-              @Override
-              public void logout() {}
-
-              @Override
-              public void login(AuthenticationToken token) throws AuthenticationException {}
-
-              @Override
-              public boolean isRunAs() {
-                return false;
-              }
-
-              @Override
-              public boolean isRemembered() {
-                return false;
-              }
-
-              @Override
-              public boolean isPermittedAll(Collection<Permission> permissions) {
-                return false;
-              }
-
-              @Override
-              public boolean isPermittedAll(String... permissions) {
-                return false;
-              }
-
-              @Override
-              public boolean[] isPermitted(List<Permission> permissions) {
-                return null;
-              }
-
-              @Override
-              public boolean[] isPermitted(String... permissions) {
-                return null;
-              }
-
-              @Override
-              public boolean isPermitted(Permission permission) {
-
-                return false;
-              }
-
-              @Override
-              public boolean isPermitted(String permission) {
-
-                return false;
-              }
-
-              @Override
-              public boolean isAuthenticated() {
-
-                return false;
-              }
-
-              @Override
-              public boolean[] hasRoles(List<String> roleIdentifiers) {
-
-                return null;
-              }
-
-              @Override
-              public boolean hasRole(String roleIdentifier) {
-
-                return false;
-              }
-
-              @Override
-              public boolean hasAllRoles(Collection<String> roleIdentifiers) {
-
-                return false;
-              }
-
-              @Override
-              public Session getSession(boolean create) {
-                return null;
-              }
-
-              @Override
-              public Session getSession() {
-                return null;
-              }
-
-              @Override
-              public PrincipalCollection getPrincipals() {
-                return null;
-              }
-
-              @Override
-              public Object getPrincipal() {
-                return null;
-              }
-
-              @Override
-              public PrincipalCollection getPreviousPrincipals() {
-                return null;
-              }
-
-              @Override
-              public void execute(Runnable runnable) {}
-
-              @Override
-              public <V> V execute(Callable<V> callable) throws ExecutionException {
-                return null;
-              }
-
-              @Override
-              public void checkRoles(String... roleIdentifiers) throws AuthorizationException {}
-
-              @Override
-              public void checkRoles(Collection<String> roleIdentifiers)
-                  throws AuthorizationException {}
-
-              @Override
-              public void checkRole(String roleIdentifier) throws AuthorizationException {}
-
-              @Override
-              public void checkPermissions(Collection<Permission> permissions)
-                  throws AuthorizationException {}
-
-              @Override
-              public void checkPermissions(String... permissions) throws AuthorizationException {}
-
-              @Override
-              public void checkPermission(Permission permission) throws AuthorizationException {}
-
-              @Override
-              public void checkPermission(String permission) throws AuthorizationException {}
-
-              @Override
-              public Runnable associateWith(Runnable runnable) {
-                return null;
-              }
-
-              @Override
-              public <V> Callable<V> associateWith(Callable<V> callable) {
-                return null;
-              }
-            };
-          }
-        };
-    Transaction<TransactionContainer> t =
-        new Transaction<TransactionContainer>(container, null) {
-
-          @Override
-          protected void transaction() throws Exception {}
-
-          @Override
-          protected void preTransaction() throws InterruptedException {}
-
-          @Override
-          protected void preCheck() throws InterruptedException, Exception {}
-
-          @Override
-          protected void postTransaction() throws Exception {}
-
-          @Override
-          protected void postCheck() {}
-
-          @Override
-          public boolean logHistory() {
-            return false;
-          }
-
-          @Override
-          protected void init() throws Exception {}
-
-          @Override
-          protected void cleanUp() {}
-        };
-
-    j.init(Mode.MUST, null, t);
-    assertEquals(0, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
-
-    // non-anonymous user
-    j.run();
-    assertEquals(0, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
-  }
-
-  @Test
-  public void testAnonymousUserUnqualified() {
-    TransactionContainer container = new TransactionContainer();
-    AWIBoxLoan j =
-        new AWIBoxLoan() {
-
-          @Override
-          protected Subject getUser() {
-            return null;
-          }
-
-          @Override
-          boolean isAnonymous() {
-            return true;
-          }
-        };
-    Transaction<TransactionContainer> t =
-        new Transaction<TransactionContainer>(container, null) {
-
-          @Override
-          protected void transaction() throws Exception {}
-
-          @Override
-          protected void preTransaction() throws InterruptedException {}
-
-          @Override
-          protected void preCheck() throws InterruptedException, Exception {}
-
-          @Override
-          protected void postTransaction() throws Exception {}
-
-          @Override
-          protected void postCheck() {}
-
-          @Override
-          public boolean logHistory() {
-            return false;
-          }
-
-          @Override
-          protected void init() throws Exception {}
-
-          @Override
-          protected void cleanUp() {}
-        };
-
-    j.init(Mode.MUST, null, t);
-    assertEquals(0, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
-
-    j.run();
-    assertEquals(2, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.UNQUALIFIED, j.getContainer().getStatus());
-  }
-}
-- 
GitLab