From 82f48d15a390562b835f33b176945743b1e61816 Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Thu, 4 Jun 2020 00:55:57 +0200
Subject: [PATCH] WIP: versioning phase 8

---
 .../backend/transaction/RetrieveParents.java  | 13 ++++
 .../transaction/RetrieveSparseEntity.java     |  3 +-
 .../server/datatype/ReferenceDatatype2.java   |  2 +-
 .../server/datatype/ReferenceValue.java       | 64 ++++++++++++++++---
 .../java/caosdb/server/entity/Entity.java     |  6 +-
 src/main/java/caosdb/server/jobs/Job.java     | 32 +++++++++-
 .../jobs/core/CheckDatatypePresent.java       |  9 +--
 .../server/jobs/core/CheckParValid.java       |  2 +-
 .../server/jobs/core/CheckPropValid.java      |  2 +-
 .../jobs/core/CheckRefidIsaParRefid.java      |  3 +-
 .../server/jobs/core/CheckRefidValid.java     | 19 ++++--
 src/main/java/caosdb/server/query/Query.java  |  6 +-
 .../server/transaction/ChecksumUpdater.java   |  2 +-
 13 files changed, 131 insertions(+), 32 deletions(-)

diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java
index 9fffcae8..eac5aa7f 100644
--- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java
+++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java
@@ -34,6 +34,18 @@ import caosdb.server.entity.EntityInterface;
 import java.util.ArrayList;
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
 
+// TODO Problem with the caching.
+// When an old entity version has a parent which is deleted, the name is
+// still in the cached VerySparseEntity. This can be resolved by using a
+// similar strategy as in RetrieveProperties.java where the name etc. are
+// retrieved in a second step. Thus the deletion doesn't slips through
+// unnoticed.
+//
+// Changes are to be made in the backend-api, i.e. mysqlbackend and the
+// interfaces as well.
+//
+// See also a failing test in caosdb-pyinttest:
+// tests/test_version.py::test_bug_cached_parent_name_in_old_version
 public class RetrieveParents
     extends CacheableBackendTransaction<String, ArrayList<VerySparseEntity>> {
 
@@ -67,6 +79,7 @@ public class RetrieveParents
   @Override
   protected void process(final ArrayList<VerySparseEntity> t) throws TransactionException {
     this.entity.getParents().clear();
+
     DatabaseUtils.parseParentsFromVerySparseEntity(this.entity, t);
   }
 
diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java
index 6ad58df8..1eb0c4c5 100644
--- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java
+++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java
@@ -58,8 +58,9 @@ public class RetrieveSparseEntity extends CacheableBackendTransaction<String, Sp
     this.entity = entity;
   }
 
-  public RetrieveSparseEntity(final int id) {
+  public RetrieveSparseEntity(final int id, final String version) {
     this(new Entity(id));
+    this.entity.getVersion().setId(version);
   }
 
   @Override
diff --git a/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java b/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java
index ce92aa02..87c17079 100644
--- a/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java
+++ b/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java
@@ -47,7 +47,7 @@ public class ReferenceDatatype2 extends ReferenceDatatype {
   }
 
   public void setEntity(final EntityInterface datatypeEntity) {
-    this.refid.setEntity(datatypeEntity);
+    this.refid.setEntity(datatypeEntity, false);
   }
 
   @Override
diff --git a/src/main/java/caosdb/server/datatype/ReferenceValue.java b/src/main/java/caosdb/server/datatype/ReferenceValue.java
index 25fa0b38..3fa22c94 100644
--- a/src/main/java/caosdb/server/datatype/ReferenceValue.java
+++ b/src/main/java/caosdb/server/datatype/ReferenceValue.java
@@ -26,27 +26,28 @@ import caosdb.server.datatype.AbstractDatatype.Table;
 import caosdb.server.entity.EntityInterface;
 import caosdb.server.entity.Message;
 import caosdb.server.utils.ServerMessages;
+import java.util.Objects;
 import org.jdom2.Element;
 
 public class ReferenceValue implements SingleValue {
   private EntityInterface entity = null;
   private String name = null;
   private Integer id = null;
+  private String version = null;
+  private boolean versioned = false;
 
   public static ReferenceValue parseReference(final Object reference) throws Message {
     if (reference == null) {
       return null;
     }
     if (reference instanceof EntityInterface) {
-      return new ReferenceValue((EntityInterface) reference);
+      return new ReferenceValue(
+          (EntityInterface) reference, ((EntityInterface) reference).hasVersion());
     } else if (reference instanceof ReferenceValue) {
       return (ReferenceValue) reference;
     } else if (reference instanceof GenericValue) {
-      try {
-        return new ReferenceValue(Integer.parseInt(((GenericValue) reference).toDatabaseString()));
-      } catch (final NumberFormatException e) {
-        return new ReferenceValue(((GenericValue) reference).toDatabaseString());
-      }
+      String str = ((GenericValue) reference).toDatabaseString();
+      return parseFromString(str);
     } else if (reference instanceof CollectionValue) {
       throw ServerMessages.DATA_TYPE_DOES_NOT_ACCEPT_COLLECTION_VALUES;
     } else {
@@ -58,17 +59,46 @@ public class ReferenceValue implements SingleValue {
     }
   }
 
+  public static ReferenceValue parseIdVersion(String str) {
+    String[] split = str.split("@", 2);
+    if (split.length == 2) {
+      return new ReferenceValue(Integer.parseInt(split[0]), split[1]);
+    } else {
+      return new ReferenceValue(Integer.parseInt(str));
+    }
+  }
+
+  public static ReferenceValue parseFromString(String str) {
+    try {
+      return parseIdVersion(str);
+    } catch (final NumberFormatException e) {
+      return new ReferenceValue(str);
+    }
+  }
+
   @Override
   public String toString() {
-    if (this.entity != null) {
+    if (this.entity != null && versioned) {
+      return this.entity.getIdVersion().toString();
+    } else if (this.entity != null) {
       return this.entity.getId().toString();
     } else if (this.id == null && this.name != null) {
       return this.name;
+    } else if (this.version != null) {
+      return getIdVersion();
     }
     return this.id.toString();
   }
 
-  public ReferenceValue(final EntityInterface entity) {
+  public String getIdVersion() {
+    if (this.version != null) {
+      return new StringBuilder().append(this.id).append("@").append(this.version).toString();
+    }
+    return this.id.toString();
+  }
+
+  public ReferenceValue(final EntityInterface entity, boolean versioned) {
+    this.versioned = versioned;
     this.entity = entity;
   }
 
@@ -76,6 +106,11 @@ public class ReferenceValue implements SingleValue {
     this.id = id;
   }
 
+  public ReferenceValue(final Integer id, final String version) {
+    this.id = id;
+    this.version = version;
+  }
+
   public ReferenceValue(final String name) {
     this.name = name;
   }
@@ -84,7 +119,8 @@ public class ReferenceValue implements SingleValue {
     return this.entity;
   }
 
-  public final void setEntity(final EntityInterface entity) {
+  public final void setEntity(final EntityInterface entity, boolean versioned) {
+    this.versioned = versioned;
     this.entity = entity;
   }
 
@@ -102,6 +138,13 @@ public class ReferenceValue implements SingleValue {
     return this.id;
   }
 
+  public final String getVersion() {
+    if (this.entity != null && versioned && this.entity.hasVersion()) {
+      return this.entity.getVersion().getId();
+    }
+    return this.version;
+  }
+
   public final void setId(final Integer id) {
     this.id = id;
   }
@@ -126,7 +169,8 @@ public class ReferenceValue implements SingleValue {
     if (obj instanceof ReferenceValue) {
       final ReferenceValue that = (ReferenceValue) obj;
       if (that.getId() != null && getId() != null) {
-        return that.getId().equals(getId());
+        return that.getId().equals(getId())
+            && Objects.deepEquals(that.getVersion(), this.getVersion());
       } else if (that.getName() != null && getName() != null) {
         return that.getName().equals(getName());
       }
diff --git a/src/main/java/caosdb/server/entity/Entity.java b/src/main/java/caosdb/server/entity/Entity.java
index 9bd0b6dd..23fb170d 100644
--- a/src/main/java/caosdb/server/entity/Entity.java
+++ b/src/main/java/caosdb/server/entity/Entity.java
@@ -1142,7 +1142,11 @@ public class Entity extends AbstractObservable implements EntityInterface {
     if (!this.hasId()) {
       return null;
     } else if (this.hasVersion()) {
-      return new StringBuilder().append(getId()).append(getVersion().getId()).toString();
+      return new StringBuilder()
+          .append(getId())
+          .append("@")
+          .append(getVersion().getId())
+          .toString();
     }
     return getId().toString();
   }
diff --git a/src/main/java/caosdb/server/jobs/Job.java b/src/main/java/caosdb/server/jobs/Job.java
index 110809e7..8d98cbc3 100644
--- a/src/main/java/caosdb/server/jobs/Job.java
+++ b/src/main/java/caosdb/server/jobs/Job.java
@@ -171,11 +171,37 @@ public abstract class Job extends AbstractObservable implements Observer {
 
   protected final EntityInterface retrieveValidSparseEntityByName(final String name)
       throws Message {
-    return retrieveValidSparseEntityById(retrieveValidIDByName(name));
+    return retrieveValidSparseEntityById(retrieveValidIDByName(name), null);
   }
 
-  protected final EntityInterface retrieveValidSparseEntityById(final Integer id) throws Message {
-    final EntityInterface ret = execute(new RetrieveSparseEntity(id)).getEntity();
+  protected final EntityInterface retrieveValidSparseEntityById(
+      final Integer id, final String version) throws Message {
+
+    String resulting_version = version;
+    if (version == null || version.equals("HEAD")) {
+      // the targeted entity version is the entity after the transaction or the
+      // entity without a specific version. Thus we have to fetch the entity
+      // from the container if possible.
+      EntityInterface ret = getEntityById(id);
+      if (ret != null) {
+        return ret;
+      }
+    } else if (version.startsWith("HEAD~")) {
+      // if version is HEAD~{OFFSET} with {OFFSET} > 0 and the targeted entity is
+      // part of this request (i.e. is to be updated), the actual offset has to be
+      // reduced by 1. HEAD always denotes the entity@HEAD *after* the successful
+      // transaction.
+      int offset = Integer.parseInt(version.substring(5)) - 1;
+      if (offset == 0) {
+        // special case HEAD~1
+        resulting_version = "HEAD";
+      } else {
+        resulting_version = new StringBuilder().append("HEAD~").append(offset).toString();
+      }
+    }
+
+    final EntityInterface ret =
+        execute(new RetrieveSparseEntity(id, resulting_version)).getEntity();
     if (ret.getEntityStatus() == EntityStatus.NONEXISTENT) {
       throw ServerMessages.ENTITY_DOES_NOT_EXIST;
     }
diff --git a/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java b/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java
index 69d36f42..faf481db 100644
--- a/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java
+++ b/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java
@@ -138,7 +138,8 @@ public final class CheckDatatypePresent extends EntityJob {
       }
     } else {
 
-      final EntityInterface validDatatypeEntity = retrieveValidSparseEntityById(datatype.getId());
+      final EntityInterface validDatatypeEntity =
+          retrieveValidSparseEntityById(datatype.getId(), null);
       assertAllowedToUse(validDatatypeEntity);
       datatype.setEntity(validDatatypeEntity);
     }
@@ -151,7 +152,7 @@ public final class CheckDatatypePresent extends EntityJob {
   private void checkIfOverride() throws Message {
     if (getEntity().hasId() && getEntity().getId() > 0) {
       // get data type from database
-      final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId());
+      final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId(), null);
 
       if (foreign.hasDatatype() && !foreign.getDatatype().equals(getEntity().getDatatype())) {
         // is override!
@@ -179,7 +180,7 @@ public final class CheckDatatypePresent extends EntityJob {
     // the data type of the corresponding abstract property.
     if (getEntity().hasId() && getEntity().getId() > 0) {
       // get from data base
-      final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId());
+      final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId(), null);
       inheritDatatypeFromForeignEntity(foreign);
     } else if (getEntity().hasId() && getEntity().getId() < 0) {
       // get from container
@@ -218,7 +219,7 @@ public final class CheckDatatypePresent extends EntityJob {
     for (final EntityInterface parent : getEntity().getParents()) {
       EntityInterface parentEntity = null;
       if (parent.getId() > 0) {
-        parentEntity = retrieveValidSparseEntityById(parent.getId());
+        parentEntity = retrieveValidSparseEntityById(parent.getId(), null);
       } else {
         parentEntity = getEntityById(parent.getId());
         runJobFromSchedule(parentEntity, CheckDatatypePresent.class);
diff --git a/src/main/java/caosdb/server/jobs/core/CheckParValid.java b/src/main/java/caosdb/server/jobs/core/CheckParValid.java
index 005053a4..560558dc 100644
--- a/src/main/java/caosdb/server/jobs/core/CheckParValid.java
+++ b/src/main/java/caosdb/server/jobs/core/CheckParValid.java
@@ -65,7 +65,7 @@ public class CheckParValid extends EntityJob {
             if (parent.getId() >= 0) {
               // id >= 0 (parent is yet in the database)
               // retrieve parent by id
-              final EntityInterface foreign = retrieveValidSparseEntityById(parent.getId());
+              final EntityInterface foreign = retrieveValidSparseEntityById(parent.getId(), null);
               // check permissions for this
               // parentforeign.acceptObserver(o)
               assertAllowedToUse(foreign);
diff --git a/src/main/java/caosdb/server/jobs/core/CheckPropValid.java b/src/main/java/caosdb/server/jobs/core/CheckPropValid.java
index 6a7cbb51..5e199e84 100644
--- a/src/main/java/caosdb/server/jobs/core/CheckPropValid.java
+++ b/src/main/java/caosdb/server/jobs/core/CheckPropValid.java
@@ -55,7 +55,7 @@ public class CheckPropValid extends EntityJob {
             if (property.getId() >= 0) {
 
               final EntityInterface abstractProperty =
-                  retrieveValidSparseEntityById(property.getId());
+                  retrieveValidSparseEntityById(property.getId(), null);
 
               assertAllowedToUse(abstractProperty);
 
diff --git a/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java b/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
index fa2b21cc..04a1ac8c 100644
--- a/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
+++ b/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
@@ -94,7 +94,8 @@ public class CheckRefidIsaParRefid extends EntityJob {
                   && getEntityByName(rv.getName()).getRole() == Role.File) {
               } else if (rv.getId() != null
                   && rv.getId() > 0
-                  && retrieveValidSparseEntityById(rv.getId()).getRole() == Role.File) {
+                  && retrieveValidSparseEntityById(rv.getId(), rv.getVersion()).getRole()
+                      == Role.File) {
               } else if (rv.getName() != null
                   && retrieveValidSparseEntityByName(rv.getName()).getRole() == Role.File) {
               } else {
diff --git a/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java
index 14d98962..0139b98f 100644
--- a/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java
+++ b/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java
@@ -85,9 +85,12 @@ public class CheckRefidValid extends EntityJob {
   private void checkRefValue(final ReferenceValue ref) throws Message {
     if (ref.getId() != null) {
       if (ref.getId() >= 0) {
-        final EntityInterface referencedValidEntity = retrieveValidSparseEntityById(ref.getId());
+        final EntityInterface referencedValidEntity =
+            retrieveValidSparseEntityById(ref.getId(), ref.getVersion());
         assertAllowedToUse(referencedValidEntity);
-        ref.setEntity(referencedValidEntity);
+
+        // link the entity as versioned entity iff the reference specified a version
+        ref.setEntity(referencedValidEntity, ref.getVersion() != null);
 
       } else {
 
@@ -100,7 +103,9 @@ public class CheckRefidValid extends EntityJob {
           final EntityInterface referencedEntity = getEntityById(ref.getId());
           if (referencedEntity != null) {
             assertAllowedToUse(referencedEntity);
-            ref.setEntity(referencedEntity);
+
+            // link the entity as versioned entity iff the reference specified a version
+            ref.setEntity(referencedEntity, ref.getVersion() != null);
           } else {
             throw ServerMessages.REFERENCED_ENTITY_DOES_NOT_EXIST;
           }
@@ -117,7 +122,9 @@ public class CheckRefidValid extends EntityJob {
 
         if (referencedEntity != null) {
           assertAllowedToUse(referencedEntity);
-          ref.setEntity(referencedEntity);
+
+          // link the entity as versioned entity iff the reference specified a version
+          ref.setEntity(referencedEntity, ref.getVersion() != null);
           if (checkRefEntity(ref)) {
             ref.getEntity().acceptObserver(this);
           }
@@ -125,7 +132,9 @@ public class CheckRefidValid extends EntityJob {
           final EntityInterface referencedValidEntity =
               retrieveValidSparseEntityByName(ref.getName());
           assertAllowedToUse(referencedValidEntity);
-          ref.setEntity(referencedValidEntity);
+
+          // link the entity as versioned entity iff the reference specified a version
+          ref.setEntity(referencedValidEntity, ref.getVersion() != null);
         }
       }
     }
diff --git a/src/main/java/caosdb/server/query/Query.java b/src/main/java/caosdb/server/query/Query.java
index 1f858269..c235a34e 100644
--- a/src/main/java/caosdb/server/query/Query.java
+++ b/src/main/java/caosdb/server/query/Query.java
@@ -299,7 +299,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
 
         // ... check for RETRIEVE:ENTITY permission...
         final EntityInterface e =
-            execute(new RetrieveSparseEntity(q.getKey()), query.getAccess()).getEntity();
+            execute(new RetrieveSparseEntity(q.getKey(), null), query.getAccess()).getEntity();
         final EntityACL entityACL = e.getEntityACL();
         if (!entityACL.isPermitted(query.getUser(), EntityPermission.RETRIEVE_ENTITY)) {
           // ... and ignore if not.
@@ -553,7 +553,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
       while (rs.next()) {
         final long t1 = System.currentTimeMillis();
         final Integer id = rs.getInt("id");
-        if (!execute(new RetrieveSparseEntity(id), query.getAccess())
+        if (!execute(new RetrieveSparseEntity(id, null), query.getAccess())
             .getEntity()
             .getEntityACL()
             .isPermitted(query.getUser(), EntityPermission.RETRIEVE_ENTITY)) {
@@ -586,7 +586,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
     while (iterator.hasNext()) {
       final long t1 = System.currentTimeMillis();
       final Integer id = iterator.next();
-      if (!execute(new RetrieveSparseEntity(id), getAccess())
+      if (!execute(new RetrieveSparseEntity(id, null), getAccess())
           .getEntity()
           .getEntityACL()
           .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) {
diff --git a/src/main/java/caosdb/server/transaction/ChecksumUpdater.java b/src/main/java/caosdb/server/transaction/ChecksumUpdater.java
index 30313478..29603fc5 100644
--- a/src/main/java/caosdb/server/transaction/ChecksumUpdater.java
+++ b/src/main/java/caosdb/server/transaction/ChecksumUpdater.java
@@ -141,7 +141,7 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl
           instance.running = false;
           return null;
         }
-        return execute(new RetrieveSparseEntity(id), weakAccess).getEntity();
+        return execute(new RetrieveSparseEntity(id, null), weakAccess).getEntity();
       }
     } catch (final Exception e) {
       e.printStackTrace();
-- 
GitLab