From e8a64dd55acf73ad4ddb415011c825bbc8cb6ac9 Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Mon, 16 Nov 2020 11:51:47 +0100
Subject: [PATCH] WIP: fsm

---
 .../backend/transaction/GetIDByName.java      |   9 +-
 .../transaction/InsertEntityProperties.java   |   6 +-
 .../caosdb/server/entity/ClientMessage.java   |  44 ++
 .../java/org/caosdb/server/entity/Entity.java |  29 +-
 .../org/caosdb/server/entity/Message.java     |   6 +-
 .../caosdb/server/entity/UpdateEntity.java    |  10 +
 src/main/java/org/caosdb/server/jobs/Job.java |  37 +-
 .../java/org/caosdb/server/jobs/Schedule.java | 100 +---
 .../org/caosdb/server/jobs/ScheduledJob.java  |  57 +++
 .../org/caosdb/server/jobs/core/Atomic.java   |   5 +-
 .../jobs/core/CheckDatatypePresent.java       |   5 +-
 .../server/jobs/core/CheckParValid.java       |   3 +
 .../server/jobs/core/CheckPropValid.java      |   3 +
 .../jobs/core/CheckRefidIsaParRefid.java      |   3 +-
 .../server/jobs/core/CheckRefidValid.java     |   3 +-
 .../jobs/core/CheckStateTransition.java       |  81 +++
 .../server/jobs/core/CheckValueParsable.java  |   4 +-
 .../server/jobs/core/EntityStateJob.java      | 466 ++++++++++++++++++
 .../caosdb/server/jobs/core/Inheritance.java  |  31 +-
 .../server/jobs/core/InitEntityState.java     |  75 +++
 .../server/jobs/core/ParseStateMessage.java   |  22 +
 .../org/caosdb/server/jobs/core/PickUp.java   |   3 +-
 .../jobs/core/ProcessNameProperties.java      |   5 +-
 .../server/jobs/core/WriteStateMessage.java   |  23 +
 .../caosdb/server/transaction/Retrieve.java   |  23 +-
 .../server/transaction/Transaction.java       |   5 +-
 .../org/caosdb/server/transaction/Update.java |  26 +-
 27 files changed, 907 insertions(+), 177 deletions(-)
 create mode 100644 src/main/java/org/caosdb/server/entity/ClientMessage.java
 create mode 100644 src/main/java/org/caosdb/server/jobs/ScheduledJob.java
 create mode 100644 src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
 create mode 100644 src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
 create mode 100644 src/main/java/org/caosdb/server/jobs/core/InitEntityState.java
 create mode 100644 src/main/java/org/caosdb/server/jobs/core/ParseStateMessage.java
 create mode 100644 src/main/java/org/caosdb/server/jobs/core/WriteStateMessage.java

diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/GetIDByName.java b/src/main/java/org/caosdb/server/database/backend/transaction/GetIDByName.java
index b43db001..8fffb415 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/GetIDByName.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/GetIDByName.java
@@ -28,6 +28,7 @@ import org.caosdb.server.database.backend.interfaces.GetIDByNameImpl;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.database.exceptions.EntityWasNotUniqueException;
 import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.query.Query.Role;
 
 public class GetIDByName extends BackendTransaction {
 
@@ -37,11 +38,11 @@ public class GetIDByName extends BackendTransaction {
   private final String role;
 
   public GetIDByName(final String name) {
-    this(name, null, true);
+    this(name, (String) null, true);
   }
 
   public GetIDByName(final String name, final boolean unique) {
-    this(name, null, unique);
+    this(name, (String) null, unique);
   }
 
   public GetIDByName(final String name, final String role) {
@@ -54,6 +55,10 @@ public class GetIDByName extends BackendTransaction {
     this.unique = unique;
   }
 
+  public GetIDByName(String name, Role role, boolean unique) {
+    this(name, role.toString(), unique);
+  }
+
   @Override
   public void execute() throws TransactionException {
     final GetIDByNameImpl t = getImplementation(GetIDByNameImpl.class);
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityProperties.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityProperties.java
index bd5bf63a..7cb0cb40 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityProperties.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityProperties.java
@@ -167,7 +167,11 @@ public class InsertEntityProperties extends BackendTransaction {
             fp.collection =
                 ((AbstractCollectionDatatype) property.getDatatype()).getCollectionName();
           } else {
-            fp.type = property.getDatatype().getId().toString();
+            try {
+              fp.type = property.getDatatype().getId().toString();
+            } catch (NullPointerException e) {
+              throw e;
+            }
           }
         }
       }
diff --git a/src/main/java/org/caosdb/server/entity/ClientMessage.java b/src/main/java/org/caosdb/server/entity/ClientMessage.java
new file mode 100644
index 00000000..22f257da
--- /dev/null
+++ b/src/main/java/org/caosdb/server/entity/ClientMessage.java
@@ -0,0 +1,44 @@
+package org.caosdb.server.entity;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.jdom2.Attribute;
+import org.jdom2.Element;
+
+public class ClientMessage extends Message {
+
+  private static final long serialVersionUID = 1L;
+  private Map<String, String> properties = new HashMap<>();
+
+  public ClientMessage(String type) {
+    super(type, null, null, null);
+  }
+
+  @Override
+  public Element toElement() {
+    final Element e = new Element(this.type);
+    for (Entry<String, String> a : this.properties.entrySet()) {
+      e.setAttribute(a.getKey(), a.getValue());
+    }
+    return e;
+  }
+
+  @Override
+  public void addToElement(final Element parent) {
+    final Element e = toElement();
+    parent.addContent(e);
+  }
+
+  public static ClientMessage fromXML(Element pe) {
+    ClientMessage result = new ClientMessage(pe.getName());
+    for (Attribute a : pe.getAttributes()) {
+      result.properties.put(a.getName(), a.getValue());
+    }
+    return result;
+  }
+
+  public String getProperty(String key) {
+    return properties.get(key);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java
index 8578191c..27570ec8 100644
--- a/src/main/java/org/caosdb/server/entity/Entity.java
+++ b/src/main/java/org/caosdb/server/entity/Entity.java
@@ -848,34 +848,7 @@ public class Entity extends AbstractObservable implements EntityInterface {
       } else if (getRole() == Role.QueryTemplate && pe.getName().equalsIgnoreCase("Query")) {
         setQueryTemplateDefinition(pe.getTextNormalize());
       } else {
-        final String type = pe.getName();
-        Integer code = null;
-        String localDescription = null;
-        String body = null;
-
-        // Parse MESSAGE CODE.
-        if (pe.getAttribute("code") != null && !pe.getAttributeValue("code").equals("")) {
-          try {
-            code = Integer.parseInt(pe.getAttributeValue("code"));
-          } catch (final NumberFormatException e) {
-            addInfo("Message code was " + pe.getAttributeValue("code") + ".");
-            addError(ServerMessages.PARSING_FAILED);
-            setEntityStatus(EntityStatus.UNQUALIFIED);
-          }
-        }
-
-        // Parse MESSAGE DESCRIPTION.
-        if (pe.getAttribute("description") != null
-            && !pe.getAttributeValue("description").equals("")) {
-          localDescription = pe.getAttributeValue("description");
-        }
-
-        // Parse MESSAGE BODY.
-        if (pe.getTextTrim() != null && !pe.getTextTrim().equals("")) {
-          body = pe.getTextTrim();
-        }
-
-        addMessage(new Message(type, code, localDescription, body));
+        addMessage(ClientMessage.fromXML(pe));
       }
     }
 
diff --git a/src/main/java/org/caosdb/server/entity/Message.java b/src/main/java/org/caosdb/server/entity/Message.java
index 743a2b62..3e469b2d 100644
--- a/src/main/java/org/caosdb/server/entity/Message.java
+++ b/src/main/java/org/caosdb/server/entity/Message.java
@@ -27,7 +27,7 @@ import org.jdom2.Element;
 
 public class Message extends Exception implements Comparable<Message>, ToElementable {
 
-  private final String type;
+  protected final String type;
   private final Integer code;
   private final String description;
   private final String body;
@@ -127,7 +127,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement
     return this.code;
   }
 
-  public final Element toElement() {
+  public Element toElement() {
     final Element e = new Element(this.type);
     if (this.code != null) {
       e.setAttribute("code", Integer.toString(this.code));
@@ -142,7 +142,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement
   }
 
   @Override
-  public final void addToElement(final Element parent) {
+  public void addToElement(final Element parent) {
     final Element e = toElement();
     parent.addContent(e);
   }
diff --git a/src/main/java/org/caosdb/server/entity/UpdateEntity.java b/src/main/java/org/caosdb/server/entity/UpdateEntity.java
index aa6d5916..1c63e4fb 100644
--- a/src/main/java/org/caosdb/server/entity/UpdateEntity.java
+++ b/src/main/java/org/caosdb/server/entity/UpdateEntity.java
@@ -27,6 +27,8 @@ import org.jdom2.Element;
 
 public class UpdateEntity extends WritableEntity {
 
+  private EntityInterface original = null;
+
   public UpdateEntity(final Element element) {
     super(element);
   }
@@ -35,4 +37,12 @@ public class UpdateEntity extends WritableEntity {
   public boolean skipJob() {
     return getEntityStatus() != EntityStatus.QUALIFIED;
   }
+
+  public void setOriginal(EntityInterface original) {
+    this.original = original;
+  }
+
+  public EntityInterface getOriginal() {
+    return this.original;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/jobs/Job.java b/src/main/java/org/caosdb/server/jobs/Job.java
index d1e3fef3..76e6ba5d 100644
--- a/src/main/java/org/caosdb/server/jobs/Job.java
+++ b/src/main/java/org/caosdb/server/jobs/Job.java
@@ -50,14 +50,11 @@ import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.container.TransactionContainer;
 import org.caosdb.server.jobs.core.Mode;
 import org.caosdb.server.transaction.Transaction;
-import org.caosdb.server.utils.AbstractObservable;
 import org.caosdb.server.utils.EntityStatus;
-import org.caosdb.server.utils.Observable;
-import org.caosdb.server.utils.Observer;
 import org.caosdb.server.utils.ServerMessages;
 import org.reflections.Reflections;
 
-public abstract class Job extends AbstractObservable implements Observer {
+public abstract class Job {
   private Transaction<? extends TransactionContainer> transaction = null;
   private Mode mode = null;
 
@@ -77,7 +74,7 @@ public abstract class Job extends AbstractObservable implements Observer {
     return getContainer().getRequestId();
   }
 
-  protected Subject getUser() {
+  public Subject getUser() {
     return getTransaction().getTransactor();
   }
 
@@ -134,10 +131,6 @@ public abstract class Job extends AbstractObservable implements Observer {
     getTransaction().getSchedule().runJob(entity, jobclass);
   }
 
-  protected void runJobFromSchedule(ScheduledJob job) {
-    getTransaction().getSchedule().runJob(job);
-  }
-
   public EntityInterface getEntity() {
     return this.entity;
   }
@@ -176,8 +169,8 @@ public abstract class Job extends AbstractObservable implements Observer {
     return retrieveValidSparseEntityById(retrieveValidIDByName(name), null);
   }
 
-  protected final EntityInterface retrieveValidSparseEntityById(
-      final Integer id, final String version) throws Message {
+  public final EntityInterface retrieveValidSparseEntityById(final Integer id, final String version)
+      throws Message {
 
     String resulting_version = version;
     if (version == null || version.equals("HEAD")) {
@@ -212,11 +205,11 @@ public abstract class Job extends AbstractObservable implements Observer {
     return ret;
   }
 
-  protected final EntityInterface retrieveValidEntity(Integer id) {
+  public final EntityInterface retrieveValidEntity(Integer id) {
     return execute(new RetrieveFullEntity(id)).getContainer().get(0);
   }
 
-  protected final Integer retrieveValidIDByName(final String name) {
+  public final Integer retrieveValidIDByName(final String name) {
     return execute(new GetIDByName(name)).getId();
   }
 
@@ -232,13 +225,13 @@ public abstract class Job extends AbstractObservable implements Observer {
     }
   }
 
-  @Override
-  public boolean notifyObserver(final String e, final Observable o) {
-    if (getEntity().getEntityStatus() != EntityStatus.UNQUALIFIED) {
-      getTransaction().getSchedule().runJob(this);
-    }
-    return true;
-  }
+  //  @Override
+  //  public boolean notifyObserver(final String e, final Observable o) {
+  //    if (getEntity().getEntityStatus() != EntityStatus.UNQUALIFIED) {
+  //      getTransaction().getSchedule().runJob(this);
+  //    }
+  //    return true;
+  //  }
 
   static HashMap<String, Class<? extends Job>> allClasses = null;
   private static List<Class<? extends Job>> loadAlways;
@@ -458,10 +451,6 @@ public abstract class Job extends AbstractObservable implements Observer {
         + "]";
   }
 
-  public void finish() {
-    super.removeAllObservers();
-  }
-
   public void print() {
     System.out.println(toString());
   }
diff --git a/src/main/java/org/caosdb/server/jobs/Schedule.java b/src/main/java/org/caosdb/server/jobs/Schedule.java
index 7f35106b..77bc57c9 100644
--- a/src/main/java/org/caosdb/server/jobs/Schedule.java
+++ b/src/main/java/org/caosdb/server/jobs/Schedule.java
@@ -22,94 +22,48 @@
  */
 package org.caosdb.server.jobs;
 
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import org.caosdb.server.entity.EntityInterface;
 
-class ScheduledJob {
-
-  long runtime = 0;
-  final Job job;
-  private long startTime = -1;
-
-  public ScheduledJob(final Job j) {
-    this.job = j;
-  }
-
-  public void run() {
-    if (!hasStarted()) {
-      start();
-      this.job.run();
-      finish();
-
-      this.job.notifyObservers(null);
-    }
-  }
-
-  private void start() {
-    this.startTime = System.currentTimeMillis();
-  }
-
-  private void finish() {
-    this.runtime += System.currentTimeMillis() - this.startTime;
-    this.job
-        .getContainer()
-        .getTransactionBenchmark()
-        .addMeasurement(this.job.getClass().getSimpleName(), this.runtime);
-  }
-
-  void pause() {
-    this.runtime += System.currentTimeMillis() - this.startTime;
-  }
-
-  void unpause() {
-    start();
-  }
-
-  private boolean hasStarted() {
-    return this.startTime != -1;
-  }
-
-  public JobExecutionTime getExecutionTime() {
-    return this.job.getExecutionTime();
-  }
-
-  public boolean skip() {
-    return this.job.getTarget().skipJob();
-  }
-}
-
 public class Schedule {
 
-  private final CopyOnWriteArrayList<ScheduledJob> jobs = new CopyOnWriteArrayList<ScheduledJob>();
+  private final Map<Integer, List<ScheduledJob>> jobLists = new HashMap<>();
   private ScheduledJob running = null;
 
-  public void addAll(final Collection<Job> jobs) {
+  public List<ScheduledJob> addAll(final Collection<Job> jobs) {
+    List<ScheduledJob> result = new ArrayList<ScheduledJob>(jobs.size());
     for (final Job j : jobs) {
-      add(j);
+      result.add(add(j));
     }
+    return result;
   }
 
   public ScheduledJob add(final Job j) {
     ScheduledJob ret = new ScheduledJob(j);
-    this.jobs.add(ret);
+    List<ScheduledJob> jobs = jobLists.get(ret.getExecutionTime().ordinal());
+    if (jobs == null) {
+      jobs = new CopyOnWriteArrayList<ScheduledJob>();
+      jobLists.put(ret.getExecutionTime().ordinal(), jobs);
+    }
+    jobs.add(ret);
     return ret;
   }
 
   public void runJobs(final JobExecutionTime time) {
-    for (final ScheduledJob scheduledJob : this.jobs) {
-      if (scheduledJob.getExecutionTime().ordinal() == time.ordinal()
-          || (time.ordinal() <= JobExecutionTime.POST_CHECK.ordinal()
-              && scheduledJob.getExecutionTime().ordinal() < time.ordinal())) {
+    List<ScheduledJob> jobs = this.jobLists.get(time.ordinal());
+    if (jobs != null) {
+      for (final ScheduledJob scheduledJob : jobs) {
         runJob(scheduledJob);
       }
     }
   }
 
-  protected void runJob(final ScheduledJob scheduledJob) {
-    if (!this.jobs.contains(scheduledJob)) {
-      throw new RuntimeException("Job was not in schedule.");
-    }
+  public void runJob(final ScheduledJob scheduledJob) {
     if (scheduledJob.skip()) {
       return;
     }
@@ -127,18 +81,12 @@ public class Schedule {
     }
   }
 
-  public void runJob(final Job j) {
-    for (final ScheduledJob scheduledJob : this.jobs) {
-      if (scheduledJob.job == j) {
-        scheduledJob.run();
-        return;
-      }
-    }
-    throw new RuntimeException("Job was not in schedule.");
-  }
-
   public void runJob(final EntityInterface entity, final Class<? extends Job> jobclass) {
-    for (final ScheduledJob scheduledJob : this.jobs) {
+    List<ScheduledJob> jobs =
+        jobclass.isAnnotationPresent(JobAnnotation.class)
+            ? this.jobLists.get(jobclass.getAnnotation(JobAnnotation.class).time().ordinal())
+            : this.jobLists.get(JobExecutionTime.CHECK.ordinal());
+    for (final ScheduledJob scheduledJob : jobs) {
       if (jobclass.isInstance(scheduledJob.job)) {
         if (scheduledJob.job.getEntity() == entity) {
           runJob(scheduledJob);
diff --git a/src/main/java/org/caosdb/server/jobs/ScheduledJob.java b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java
new file mode 100644
index 00000000..94aebf15
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java
@@ -0,0 +1,57 @@
+package org.caosdb.server.jobs;
+
+public class ScheduledJob {
+
+  long runtime = 0;
+  final Job job;
+  private long startTime = -1;
+
+  ScheduledJob(final Job j) {
+    this.job = j;
+  }
+
+  void run() {
+    if (!hasStarted()) {
+      start();
+      this.job.run();
+      finish();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return this.job.toString();
+  }
+
+  private void start() {
+    this.startTime = System.currentTimeMillis();
+  }
+
+  private void finish() {
+    this.runtime += System.currentTimeMillis() - this.startTime;
+    this.job
+        .getContainer()
+        .getTransactionBenchmark()
+        .addMeasurement(this.job.getClass().getSimpleName(), this.runtime);
+  }
+
+  void pause() {
+    this.runtime += System.currentTimeMillis() - this.startTime;
+  }
+
+  void unpause() {
+    start();
+  }
+
+  private boolean hasStarted() {
+    return this.startTime != -1;
+  }
+
+  public JobExecutionTime getExecutionTime() {
+    return this.job.getExecutionTime();
+  }
+
+  public boolean skip() {
+    return this.job.getTarget().skipJob();
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/Atomic.java b/src/main/java/org/caosdb/server/jobs/core/Atomic.java
index 509f87fc..45b5de8e 100644
--- a/src/main/java/org/caosdb/server/jobs/core/Atomic.java
+++ b/src/main/java/org/caosdb/server/jobs/core/Atomic.java
@@ -30,13 +30,14 @@ import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.transaction.WriteTransaction;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
+import org.caosdb.server.utils.Observer;
 import org.caosdb.server.utils.ServerMessages;
 
 @JobAnnotation(
-    time = JobExecutionTime.POST_CHECK,
+    time = JobExecutionTime.PRE_TRANSACTION,
     transaction = WriteTransaction.class,
     loadAlways = true)
-public class Atomic extends ContainerJob {
+public class Atomic extends ContainerJob implements Observer {
 
   private boolean doCheck() {
     if (getContainer().getStatus() == EntityStatus.QUALIFIED) {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java
index ce9943ec..f46013d2 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java
@@ -33,6 +33,7 @@ import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.Role;
 import org.caosdb.server.jobs.EntityJob;
 import org.caosdb.server.jobs.Job;
+import org.caosdb.server.jobs.ScheduledJob;
 import org.caosdb.server.permissions.EntityPermission;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.ServerMessages;
@@ -81,8 +82,8 @@ public final class CheckDatatypePresent extends EntityJob {
 
         // run jobsreturn this.entities;
         final List<Job> datatypeJobs = loadDataTypeSpecificJobs();
-        getTransaction().getSchedule().addAll(datatypeJobs);
-        for (final Job job : datatypeJobs) {
+        List<ScheduledJob> scheduledJobs = getTransaction().getSchedule().addAll(datatypeJobs);
+        for (final ScheduledJob job : scheduledJobs) {
           getTransaction().getSchedule().runJob(job);
         }
       } else {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java
index c6d10a8f..02cb96fc 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java
@@ -32,6 +32,8 @@ import org.caosdb.server.entity.Role;
 import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.permissions.EntityPermission;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.ServerMessages;
@@ -41,6 +43,7 @@ import org.caosdb.server.utils.ServerMessages;
  *
  * @author tf
  */
+@JobAnnotation(time = JobExecutionTime.PRE_CHECK)
 public class CheckParValid extends EntityJob {
   @Override
   public final void run() {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java
index 10ce6295..f95104bd 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java
@@ -31,6 +31,8 @@ import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.permissions.EntityPermission;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.ServerMessages;
@@ -40,6 +42,7 @@ import org.caosdb.server.utils.ServerMessages;
  *
  * @author tf
  */
+@JobAnnotation(time = JobExecutionTime.PRE_CHECK)
 public class CheckPropValid extends EntityJob {
   @Override
   public final void run() {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
index 59fcfe1d..b9f804f5 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
@@ -37,6 +37,7 @@ import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.jobs.EntityJob;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
+import org.caosdb.server.utils.Observer;
 import org.caosdb.server.utils.ServerMessages;
 
 /**
@@ -45,7 +46,7 @@ import org.caosdb.server.utils.ServerMessages;
  *
  * @author tf
  */
-public class CheckRefidIsaParRefid extends EntityJob {
+public class CheckRefidIsaParRefid extends EntityJob implements Observer {
 
   private void doJob() {
     try {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
index 42476f87..38320291 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
@@ -38,6 +38,7 @@ import org.caosdb.server.jobs.EntityJob;
 import org.caosdb.server.permissions.EntityPermission;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
+import org.caosdb.server.utils.Observer;
 import org.caosdb.server.utils.ServerMessages;
 
 /**
@@ -45,7 +46,7 @@ import org.caosdb.server.utils.ServerMessages;
  *
  * @author tf
  */
-public class CheckRefidValid extends EntityJob {
+public class CheckRefidValid extends EntityJob implements Observer {
   @Override
   public final void run() {
     try {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
new file mode 100644
index 00000000..65df0fef
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
@@ -0,0 +1,81 @@
+package org.caosdb.server.jobs.core;
+
+import org.caosdb.server.entity.DeleteEntity;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.Message.MessageType;
+import org.caosdb.server.entity.UpdateEntity;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+import org.caosdb.server.transaction.WriteTransaction;
+
+@JobAnnotation(
+    time = JobExecutionTime.CHECK,
+    transaction = WriteTransaction.class,
+    loadAlways = true)
+public class CheckStateTransition extends EntityStateJob {
+
+  private static final Message TRANSITION_NOT_ALLOWED =
+      new Message(MessageType.Error, "Transition not allowed.");
+
+  @Override
+  protected void run() {
+    try {
+      if (getEntity() instanceof UpdateEntity) {
+        StateMessage toState = getStateMessage();
+        StateMessage fromState = getStateMessage(((UpdateEntity) getEntity()).getOriginal());
+        checkStateTransition(fromState, toState);
+      } else if (getEntity() instanceof DeleteEntity) {
+        StateMessage finalState = getStateMessage();
+        checkFinalState(finalState);
+      } else {
+        StateMessage initialState = getStateMessage();
+        checkInitialState(initialState);
+      }
+    } catch (Message m) {
+      getEntity().addError(m);
+    }
+  }
+
+  /** Check if state is valid and transition is allowed */
+  private void checkStateTransition(StateMessage fromState, StateMessage toState) throws Message {
+    if (fromState == null && toState == null) {
+      return;
+    } else if (fromState == null && toState != null) {
+      checkInitialState(toState);
+      return;
+    } else if (toState == null && fromState != null) {
+      checkFinalState(fromState);
+      return;
+    }
+
+    StateModel stateModel = findMatchingStateModel(fromState, toState);
+    if (stateModel == null) {
+      checkInitialState(toState);
+      checkFinalState(fromState);
+      return;
+    }
+
+    for (Transition t : stateModel.getTransitions()) {
+      if (t.fromStatesInclude(fromState.getState()) && t.toStatesInclude(toState.getState())) {
+        return;
+      }
+    }
+    throw TRANSITION_NOT_ALLOWED;
+  }
+
+  private StateModel findMatchingStateModel(StateMessage fromState, StateMessage toState)
+      throws Message {
+    if (fromState.getStateModel().equals(toState.getStateModel())) {
+      return fromState.getStateModel();
+    }
+    return null;
+  }
+
+  private void checkFinalState(StateMessage fromState) {
+    // TODO Auto-generated method stub
+  }
+
+  private void checkInitialState(StateMessage toState) {
+    // TODO Auto-generated method stub
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java b/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java
index 34e19609..1dd45714 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java
@@ -28,6 +28,7 @@ import org.caosdb.server.entity.Message;
 import org.caosdb.server.jobs.EntityJob;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
+import org.caosdb.server.utils.Observer;
 
 /**
  * Check whether the value of an entity is parsable according to the entity's data type. This job
@@ -37,7 +38,7 @@ import org.caosdb.server.utils.Observable;
  *
  * @author tf
  */
-public class CheckValueParsable extends EntityJob {
+public class CheckValueParsable extends EntityJob implements Observer {
   @Override
   public final void run() {
 
@@ -96,7 +97,6 @@ public class CheckValueParsable extends EntityJob {
       // Therefore, not the whole test has to be run again.
       if (getEntity().hasDatatype()) {
         parseValue();
-        super.notifyObservers(e);
         return false;
       }
     }
diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
new file mode 100644
index 00000000..32f1e019
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
@@ -0,0 +1,466 @@
+package org.caosdb.server.jobs.core;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.caosdb.server.database.backend.transaction.GetIDByName;
+import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
+import org.caosdb.server.datatype.AbstractCollectionDatatype;
+import org.caosdb.server.datatype.CollectionValue;
+import org.caosdb.server.datatype.IndexedSingleValue;
+import org.caosdb.server.datatype.ReferenceDatatype;
+import org.caosdb.server.datatype.ReferenceDatatype2;
+import org.caosdb.server.datatype.ReferenceValue;
+import org.caosdb.server.entity.ClientMessage;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.Message.MessageType;
+import org.caosdb.server.entity.StatementStatus;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.entity.xml.ToElementable;
+import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.query.Query;
+import org.jdom2.Element;
+
+public abstract class EntityStateJob extends EntityJob {
+
+  public class Transition {
+
+    private String transitionIdVersion;
+    private State fromState;
+    private State toState;
+
+    public Transition(EntityInterface transition) throws Message {
+      this.transitionIdVersion = transition.getIdVersion();
+      this.fromState = getFromState(transition);
+      this.toState = getToState(transition);
+    }
+
+    private State getToState(EntityInterface transition) throws Message {
+      for (Property p : transition.getProperties()) {
+        if (p.getName().equals("to")) {
+          return createStateMessage(p).getState();
+        }
+      }
+      return null;
+    }
+
+    private State getFromState(EntityInterface transition) throws Message {
+      for (Property p : transition.getProperties()) {
+        if (p.getName().equals("from")) {
+          return createStateMessage(p).getState();
+        }
+      }
+      return null;
+    }
+
+    public boolean fromStatesInclude(State previousState) {
+      return this.fromState.equals(previousState);
+    }
+
+    public boolean toStatesInclude(State nextState) {
+      return this.toState.equals(nextState);
+    }
+
+    public State getToState() {
+      return this.toState;
+    }
+
+    public State getFromState() {
+      return this.fromState;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof Transition) {
+        return ((Transition) obj).transitionIdVersion.equals(this.transitionIdVersion);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return 1239141238 + this.transitionIdVersion.hashCode();
+    }
+  }
+
+  public class State implements ToElementable {
+
+    private String stateIdVersion;
+
+    public State(EntityInterface stateEntity) {
+      this.stateIdVersion = stateEntity.getIdVersion();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof State) {
+        return ((State) obj).stateIdVersion.equals(this.stateIdVersion);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return 1239141238 + this.stateIdVersion.hashCode();
+    }
+
+    @Override
+    public void addToElement(Element ret) {
+      // TODO Auto-generated method stub
+
+    }
+  }
+
+  public class StateModel {
+
+    private String stateModelIdVersion;
+    private String name;
+    private Set<State> states;
+    private Set<Transition> transitions;
+    private State initialState;
+    private State finalState;
+
+    public StateModel(EntityInterface stateModelEntity) throws Message {
+      this.stateModelIdVersion = stateModelEntity.getIdVersion();
+      this.getName();
+      this.states = getStates(stateModelEntity);
+      this.transitions = getTransitions(stateModelEntity);
+      this.finalState = getFinalState(stateModelEntity);
+      this.initialState = getInitialState(stateModelEntity);
+    }
+
+    private State getInitialState(EntityInterface stateModelEntity) throws Message {
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals("initial")) {
+          return createStateMessage(p).getState();
+        }
+      }
+      return null;
+    }
+
+    private State getFinalState(EntityInterface stateModelEntity) throws Message {
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals("final")) {
+          return createStateMessage(p).getState();
+        }
+      }
+      return null;
+    }
+
+    private Set<Transition> getTransitions(EntityInterface stateModelEntity) throws Message {
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals("Transition")) {
+          return createTransitions(p);
+        }
+      }
+      return null;
+    }
+
+    private Set<Transition> createTransitions(Property p) throws Message {
+      Set<Transition> result = new HashSet<>();
+      try {
+        if (!(p.getDatatype() instanceof AbstractCollectionDatatype)) {
+          return result;
+        }
+        p.parseValue();
+        CollectionValue vals = (CollectionValue) p.getValue();
+        for (IndexedSingleValue val : vals) {
+          if (val.getWrapped() instanceof ReferenceValue) {
+            Integer refid = ((ReferenceValue) val.getWrapped()).getId();
+            EntityInterface transition = retrieveValidEntity(refid);
+            result.add(new Transition(transition));
+          }
+        }
+      } catch (Exception e) {
+        e.printStackTrace();
+        throw COULD_NOT_GENERATE_TRANSITIONS;
+      }
+      return result;
+    }
+
+    private Set<State> getStates(EntityInterface stateModelEntity) throws Message {
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals("State")) {
+          return createStates(p);
+        }
+      }
+      return null;
+    }
+
+    private Set<State> createStates(Property p) throws Message {
+      Set<State> result = new HashSet<>();
+      try {
+        if (!(p.getDatatype() instanceof AbstractCollectionDatatype)) {
+          return result;
+        }
+        p.parseValue();
+        CollectionValue vals = (CollectionValue) p.getValue();
+        for (IndexedSingleValue val : vals) {
+          if (val.getWrapped() instanceof ReferenceValue) {
+            Integer refid = ((ReferenceValue) val.getWrapped()).getId();
+            EntityInterface transition = retrieveValidEntity(refid);
+            result.add(new State(transition));
+          }
+        }
+      } catch (Exception e) {
+        e.printStackTrace();
+        throw COULD_NOT_GENERATE_STATES;
+      }
+      return result;
+    }
+
+    public String getName() {
+      return this.name;
+    }
+
+    public Set<State> getStates() {
+      return this.states;
+    }
+
+    public Set<Transition> getTransitions() {
+      return this.transitions;
+    }
+
+    public State getFinalState() {
+      return this.finalState;
+    }
+
+    public State getInitialState() {
+      return this.initialState;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof StateModel) {
+        return ((StateModel) obj).stateModelIdVersion.equals(this.stateModelIdVersion);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return 1239141238 + this.stateModelIdVersion.hashCode();
+    }
+  }
+
+  public class StateMessage implements ToElementable {
+
+    String name;
+    String model;
+    private EntityInterface stateModelEntity;
+    private EntityInterface stateEntity;
+
+    /**
+     * @param name
+     * @param model
+     */
+    public StateMessage(String name, String model) {
+      this.name = name;
+      this.model = model;
+    }
+
+    @Override
+    public void addToElement(Element ret) {
+      Element e = new Element("State");
+      e.setAttribute("model", this.model);
+      e.setAttribute("name", this.name);
+      ret.addContent(e);
+    }
+
+    public Property createStateProperty() {
+      EntityInterface stateRecordType = getStateRecordType();
+      Property stateProperty = new Property(stateRecordType);
+      stateProperty.setDatatype(new ReferenceDatatype2(stateRecordType));
+      stateProperty.setValue(new ReferenceValue(getStateEntity(), false));
+      stateProperty.setStatementStatus(StatementStatus.FIX);
+      return stateProperty;
+    }
+
+    private EntityInterface getStateRecordType() {
+      try {
+        return retrieveValidSparseEntityByName("State");
+      } catch (Message e) {
+        // TODO Auto-generated catch block
+        e.printStackTrace();
+        return null;
+      }
+    }
+
+    public EntityInterface getStateModelEntity() {
+      if (this.stateModelEntity == null) {
+        try {
+          this.stateModelEntity = retrieveStateModelEntity(model);
+        } catch (Message e) {
+          // TODO Auto-generated catch block
+          e.printStackTrace();
+        }
+      }
+      return this.stateModelEntity;
+    }
+
+    public EntityInterface getStateEntity() {
+      if (this.stateEntity == null) {
+        try {
+          this.stateEntity = retrieveStateEntity(getStateModelEntity(), this.name);
+        } catch (Message e) {
+          // TODO Auto-generated catch block
+          e.printStackTrace();
+        }
+      }
+      return this.stateEntity;
+    }
+
+    public StateModel getStateModel() throws Message {
+      return new StateModel(getStateModelEntity());
+    }
+
+    public State getState() {
+      return new State(getStateEntity());
+    }
+  }
+
+  private static final Message STATE_MODEL_NOT_FOUND =
+      new Message(MessageType.Error, "StateModel does not exist.");
+  private static final Message STATE_ENTITY_NOT_FOUND =
+      new Message(MessageType.Error, "State does not exist in this StateModel.");
+  private static final Message COULD_NOT_GENERATE_STATE_MESSAGE =
+      new Message(MessageType.Error, "Could not generate the state message");
+  private static final Message COULD_NOT_GENERATE_TRANSITIONS =
+      new Message(MessageType.Error, "Could not generate transitions");
+  private static final Message COULD_NOT_GENERATE_STATES =
+      new Message(MessageType.Error, "Could not generate states");
+
+  protected EntityInterface retrieveStateEntity(EntityInterface stateModelEntity, String stateName)
+      throws Message {
+
+    List<Integer> list;
+    try {
+      list = execute(new GetIDByName(stateName)).getList();
+    } catch (EntityDoesNotExistException e) {
+      throw STATE_ENTITY_NOT_FOUND;
+    }
+    for (Property p : stateModelEntity.getProperties()) {
+      if (Objects.equals(p.getName(), "State")) {
+        p.parseValue();
+        for (IndexedSingleValue val : ((CollectionValue) p.getValue())) {
+          ReferenceValue refid = (ReferenceValue) val.getWrapped();
+
+          // match id
+          for (Integer id : list) {
+            if (Objects.equals(refid.getId(), id)) {
+              return retrieveValidEntity(id);
+            }
+          }
+        }
+      }
+    }
+    throw STATE_ENTITY_NOT_FOUND;
+  }
+
+  protected EntityInterface retrieveStateModelEntity(String stateModel) throws Message {
+    try {
+      return retrieveValidEntity(retrieveValidIDByName(stateModel));
+    } catch (EntityDoesNotExistException e) {
+      throw STATE_MODEL_NOT_FOUND;
+    }
+  }
+
+  protected StateMessage getStateMessage() {
+    return getStateMessage(false);
+  }
+
+  protected StateMessage getStateMessage(EntityInterface entity) {
+    return getStateMessage(entity, false);
+  }
+
+  protected StateMessage getStateMessage(EntityInterface entity, boolean remove) {
+    Iterator<ToElementable> messages = entity.getMessages().iterator();
+    while (messages.hasNext()) {
+      ToElementable s = messages.next();
+      if (s instanceof StateMessage) {
+        if (remove) {
+          messages.remove();
+        }
+        return (StateMessage) s;
+      }
+    }
+    return null;
+  }
+
+  protected StateMessage getStateMessage(boolean remove) {
+    return getStateMessage(getEntity(), remove);
+  }
+
+  protected Property getStateProperty(EntityInterface entity, boolean remove) {
+    Iterator<Property> it = entity.getProperties().iterator();
+    while (it.hasNext()) {
+      Property p = it.next();
+      if (Objects.equals(p.getName(), "State")) {
+        if (!(p.getDatatype() instanceof ReferenceDatatype)) {
+          continue;
+        }
+        if (remove) {
+          it.remove();
+        }
+        return p;
+      }
+    }
+    return null;
+  }
+
+  protected Property getStateProperty(boolean remove) {
+    return getStateProperty(getEntity(), remove);
+  }
+
+  protected ClientMessage getStateClientMessage(EntityInterface entity, boolean remove) {
+    Iterator<Message> stateMessages = entity.getMessages("State").iterator();
+    while (stateMessages.hasNext()) {
+      Message s = stateMessages.next();
+      if (s instanceof ClientMessage) {
+        if (remove) {
+          stateMessages.remove();
+        }
+        return (ClientMessage) s;
+      }
+    }
+    return null;
+  }
+
+  protected ClientMessage getStateClientMessage(boolean remove) {
+    return getStateClientMessage(getEntity(), remove);
+  }
+
+  protected StateMessage createStateMessage(ClientMessage s) throws Message {
+    String stateModel = s.getProperty("model");
+    String stateName = s.getProperty("name");
+    return new StateMessage(stateName, stateModel);
+  }
+
+  protected StateMessage createStateMessage(Property p) throws Message {
+    try {
+      p.parseValue();
+      ReferenceValue refid = (ReferenceValue) p.getValue();
+      EntityInterface stateEntity = retrieveValidSparseEntityById(refid.getId(), null);
+
+      EntityInterface stateModelEntity = findStateModel(stateEntity);
+      return new StateMessage(stateEntity.getName(), stateModelEntity.getName());
+    } catch (Exception e) {
+      // TODO Auto-generated catch block
+      e.printStackTrace();
+      throw COULD_NOT_GENERATE_STATE_MESSAGE;
+    }
+  }
+
+  EntityInterface findStateModel(EntityInterface stateEntity) throws Exception {
+    Query query =
+        new Query(
+            "FIND Record StateModel WHICH REFERENCES " + Integer.toString(stateEntity.getId()),
+            getUser(),
+            null);
+    query.execute(getTransaction().getAccess());
+    return retrieveValidSparseEntityById(query.getResultSet().get(0), null);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
index 086787e8..b3f91338 100644
--- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
+++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
@@ -32,6 +32,7 @@ import org.caosdb.server.entity.Message.MessageType;
 import org.caosdb.server.entity.StatementStatus;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.jobs.ScheduledJob;
 import org.caosdb.server.transaction.Insert;
 import org.caosdb.server.transaction.Update;
 import org.caosdb.server.utils.EntityStatus;
@@ -65,7 +66,7 @@ public class Inheritance extends EntityJob {
   protected void run() {
     if (getTransaction() instanceof Insert || getTransaction() instanceof Update) {
       if (getEntity().hasParents()) {
-        final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>();
+        final ArrayList<Property> transfer = new ArrayList<>();
         parentLoop:
         for (final EntityInterface parent : getEntity().getParents()) {
           try {
@@ -100,23 +101,25 @@ public class Inheritance extends EntityJob {
 
         // transfer properties if they are not implemented yet
         outerLoop:
-        for (final EntityInterface prop : transfer) {
-          for (final EntityInterface eprop : getEntity().getProperties()) {
+        for (final Property prop : transfer) {
+          for (final Property eprop : getEntity().getProperties()) {
             if (prop.hasId() && eprop.hasId() && prop.getId().equals(eprop.getId())) {
               continue outerLoop;
             }
           }
           // prop's Datatype might need to be resolved.
-          this.appendJob(prop, CheckDatatypePresent.class);
-          getEntity().addProperty(new Property(prop));
+          ScheduledJob job = this.appendJob(prop, CheckDatatypePresent.class);
+          getTransaction().getSchedule().runJob(job);
+
+          getEntity().addProperty(new Property(prop.getWrapped()));
         }
       }
 
       // implement properties
       if (getEntity().hasProperties()) {
         propertyLoop:
-        for (final EntityInterface property : getEntity().getProperties()) {
-          final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>();
+        for (final Property property : getEntity().getProperties()) {
+          final ArrayList<Property> transfer = new ArrayList<>();
           try {
             if (property.getFlags().get("inheritance") == null) {
               break propertyLoop;
@@ -156,15 +159,17 @@ public class Inheritance extends EntityJob {
 
           // transfer properties if they are not implemented yet
           outerLoop:
-          for (final EntityInterface prop : transfer) {
-            for (final EntityInterface eprop : property.getProperties()) {
+          for (final Property prop : transfer) {
+            for (final Property eprop : property.getProperties()) {
               if (prop.hasId() && eprop.hasId() && prop.getId() == eprop.getId()) {
                 continue outerLoop;
               }
             }
             // prop's Datatype might need to be resolved.
-            this.appendJob(prop, CheckDatatypePresent.class);
-            property.addProperty(new Property(prop));
+            ScheduledJob job = this.appendJob(prop, CheckDatatypePresent.class);
+            getTransaction().getSchedule().runJob(job);
+
+            property.addProperty(new Property(prop.getWrapped()));
           }
         }
       }
@@ -182,9 +187,9 @@ public class Inheritance extends EntityJob {
    * @param inheritance
    */
   private void collectInheritedProperties(
-      List<EntityInterface> transfer, EntityInterface from, INHERITANCE_MODE inheritance) {
+      List<Property> transfer, EntityInterface from, INHERITANCE_MODE inheritance) {
     if (from.hasProperties()) {
-      for (final EntityInterface propProperty : from.getProperties()) {
+      for (final Property propProperty : from.getProperties()) {
         switch (inheritance) {
             // the following cases are ordered according to their importance level and use a
             // fall-through.
diff --git a/src/main/java/org/caosdb/server/jobs/core/InitEntityState.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityState.java
new file mode 100644
index 00000000..9a92dd42
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityState.java
@@ -0,0 +1,75 @@
+package org.caosdb.server.jobs.core;
+
+import java.util.Objects;
+import org.caosdb.server.entity.ClientMessage;
+import org.caosdb.server.entity.Entity;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.Message.MessageType;
+import org.caosdb.server.entity.UpdateEntity;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+import org.caosdb.server.transaction.WriteTransaction;
+import org.caosdb.server.utils.EntityStatus;
+import org.caosdb.server.utils.Observable;
+import org.caosdb.server.utils.Observer;
+
+@JobAnnotation(
+    loadAlways = true,
+    time = JobExecutionTime.INIT,
+    transaction = WriteTransaction.class)
+public class InitEntityState extends EntityStateJob implements Observer {
+
+  @Override
+  protected void run() {
+    try {
+      State newState = initStateMessage(getEntity());
+      try {
+        if (getEntity() instanceof UpdateEntity) {
+          State oldState = initStateMessage(((UpdateEntity) getEntity()).getOriginal());
+          if (!Objects.equals(newState, oldState)) {
+            getEntity().acceptObserver(this);
+          }
+        }
+      } catch (Message m) {
+        getEntity().addWarning(STATE_ERROR_IN_ORIGINAL_ENTITY(m));
+      }
+    } catch (Message m) {
+      getEntity().addError(m);
+      return;
+    }
+  }
+
+  private static final Message STATE_ERROR_IN_ORIGINAL_ENTITY(Message m) {
+    return new Message(
+        MessageType.Warning, "State error in previous entity version\n" + m.getDescription());
+  }
+
+  private State initStateMessage(EntityInterface entity) throws Message {
+    ClientMessage stateClientMessage = getStateClientMessage(entity, true);
+    if (stateClientMessage != null) {
+      StateMessage stateMessage = createStateMessage(stateClientMessage);
+      entity.addMessage(stateMessage);
+      return stateMessage.getState();
+    }
+    Property stateProperty = getStateProperty(entity, true);
+    if (stateProperty != null) {
+      StateMessage stateMessage = createStateMessage(stateProperty);
+      entity.addMessage(stateMessage);
+      return stateMessage.getState();
+    }
+    return null;
+  }
+
+  @Override
+  public boolean notifyObserver(String e, Observable o) {
+    if (e == Entity.ENTITY_STATUS_CHANGED_EVENT) {
+      if (o == getEntity() && getEntity().getEntityStatus() == EntityStatus.VALID) {
+        getEntity().setEntityStatus(EntityStatus.QUALIFIED);
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/ParseStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/ParseStateMessage.java
new file mode 100644
index 00000000..d7cd71dc
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/ParseStateMessage.java
@@ -0,0 +1,22 @@
+package org.caosdb.server.jobs.core;
+
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+import org.caosdb.server.transaction.WriteTransaction;
+
+@JobAnnotation(
+    loadAlways = true,
+    transaction = WriteTransaction.class,
+    time = JobExecutionTime.PRE_TRANSACTION)
+public class ParseStateMessage extends EntityStateJob {
+
+  @Override
+  protected void run() {
+    StateMessage s = getStateMessage(true);
+    if (s != null) addStateProperty(s);
+  }
+
+  private void addStateProperty(StateMessage stateEntity) {
+    getEntity().addProperty(stateEntity.createStateProperty());
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/PickUp.java b/src/main/java/org/caosdb/server/jobs/core/PickUp.java
index dff574a4..b91fccf7 100644
--- a/src/main/java/org/caosdb/server/jobs/core/PickUp.java
+++ b/src/main/java/org/caosdb/server/jobs/core/PickUp.java
@@ -33,9 +33,10 @@ import org.caosdb.server.jobs.JobAnnotation;
 import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
+import org.caosdb.server.utils.Observer;
 
 @JobAnnotation(time = JobExecutionTime.INIT)
-public class PickUp extends EntityJob {
+public class PickUp extends EntityJob implements Observer {
 
   @Override
   protected void run() {
diff --git a/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java b/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java
index 950c61d3..e7e8f0e4 100644
--- a/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java
+++ b/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java
@@ -34,14 +34,17 @@ import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.ServerMessages;
 
 /**
- * To be called after CheckPropValid.
+ * To be called after CheckPropValid and Inheritance.
  *
  * @author tf
  */
+@JobAnnotation(time = JobExecutionTime.PRE_TRANSACTION)
 public class ProcessNameProperties extends EntityJob {
 
   @Override
diff --git a/src/main/java/org/caosdb/server/jobs/core/WriteStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/WriteStateMessage.java
new file mode 100644
index 00000000..9fa21b7c
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/WriteStateMessage.java
@@ -0,0 +1,23 @@
+package org.caosdb.server.jobs.core;
+
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+
+@JobAnnotation(loadAlways = true, time = JobExecutionTime.POST_TRANSACTION)
+public class WriteStateMessage extends EntityStateJob {
+
+  @Override
+  protected void run() {
+    try {
+      Property stateProperty = getStateProperty(true);
+      if (stateProperty != null) {
+        StateMessage stateMessage = createStateMessage(stateProperty);
+        getEntity().addMessage(stateMessage);
+      }
+    } catch (Message e) {
+      getEntity().addError(e);
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java
index 5b01657a..087bc5a3 100644
--- a/src/main/java/org/caosdb/server/transaction/Retrieve.java
+++ b/src/main/java/org/caosdb/server/transaction/Retrieve.java
@@ -30,6 +30,7 @@ import org.caosdb.server.entity.container.RetrieveContainer;
 import org.caosdb.server.entity.xml.SetFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementStrategy;
 import org.caosdb.server.entity.xml.ToElementable;
+import org.caosdb.server.jobs.ScheduledJob;
 import org.caosdb.server.jobs.core.Mode;
 import org.caosdb.server.jobs.core.RemoveDuplicates;
 import org.caosdb.server.jobs.core.ResolveNames;
@@ -50,15 +51,19 @@ public class Retrieve extends Transaction<RetrieveContainer> {
     setAccess(getMonitor().acquiredWeakAccess(this));
 
     // resolve names
-    final ResolveNames r = new ResolveNames();
-    r.init(Mode.SHOULD, null, this);
-    getSchedule().add(r);
-    getSchedule().runJob(r);
-
-    final RemoveDuplicates job = new RemoveDuplicates();
-    job.init(Mode.MUST, null, this);
-    getSchedule().add(job);
-    getSchedule().runJob(job);
+    {
+      final ResolveNames r = new ResolveNames();
+      r.init(Mode.SHOULD, null, this);
+      ScheduledJob scheduledJob = getSchedule().add(r);
+      getSchedule().runJob(scheduledJob);
+    }
+
+    {
+      final RemoveDuplicates job = new RemoveDuplicates();
+      job.init(Mode.MUST, null, this);
+      ScheduledJob scheduledJob = getSchedule().add(job);
+      getSchedule().runJob(scheduledJob);
+    }
 
     // make schedule for all parsed entities
     makeSchedule();
diff --git a/src/main/java/org/caosdb/server/transaction/Transaction.java b/src/main/java/org/caosdb/server/transaction/Transaction.java
index 0235b8a2..6fb28c6f 100644
--- a/src/main/java/org/caosdb/server/transaction/Transaction.java
+++ b/src/main/java/org/caosdb/server/transaction/Transaction.java
@@ -41,6 +41,7 @@ import org.caosdb.server.entity.container.TransactionContainer;
 import org.caosdb.server.jobs.Job;
 import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.jobs.Schedule;
+import org.caosdb.server.jobs.ScheduledJob;
 import org.caosdb.server.jobs.core.AccessControl;
 import org.caosdb.server.jobs.core.CheckDatatypePresent;
 import org.caosdb.server.jobs.core.CheckEntityACLRoles;
@@ -88,8 +89,8 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra
   protected void makeSchedule() throws Exception {
     // load flag jobs
     final Job loadContainerFlags = Job.getJob("LoadContainerFlagJobs", Mode.MUST, null, this);
-    this.schedule.add(loadContainerFlags);
-    this.schedule.runJob(loadContainerFlags);
+    ScheduledJob scheduledJob = this.schedule.add(loadContainerFlags);
+    this.schedule.runJob(scheduledJob);
 
     // AccessControl
     this.schedule.add(Job.getJob(AccessControl.class.getSimpleName(), Mode.MUST, null, this));
diff --git a/src/main/java/org/caosdb/server/transaction/Update.java b/src/main/java/org/caosdb/server/transaction/Update.java
index f5a9a7ff..50d7f978 100644
--- a/src/main/java/org/caosdb/server/transaction/Update.java
+++ b/src/main/java/org/caosdb/server/transaction/Update.java
@@ -52,7 +52,20 @@ public class Update extends WriteTransaction<UpdateContainer> {
   }
 
   @Override
-  protected void preCheck() throws Exception {}
+  protected void preCheck() throws Exception {
+    for (final EntityInterface e : getContainer()) {
+      if (e.getEntityStatus() == EntityStatus.QUALIFIED) {
+        org.caosdb.server.entity.UpdateEntity newEntity = (org.caosdb.server.entity.UpdateEntity) e;
+        try {
+          checkPermissions(newEntity, deriveUpdate(newEntity, newEntity.getOriginal()));
+        } catch (final AuthorizationException exc) {
+          newEntity.setEntityStatus(EntityStatus.UNQUALIFIED);
+          newEntity.addError(ServerMessages.AUTHORIZATION_ERROR);
+          newEntity.addInfo(exc.getMessage());
+        }
+      }
+    }
+  }
 
   @Override
   protected void init() throws Exception {
@@ -84,7 +97,8 @@ public class Update extends WriteTransaction<UpdateContainer> {
     execute(new RetrieveFullEntity(oldContainer), getAccess());
 
     // Check if any updates are to be processed.
-    for (final EntityInterface newEntity : getContainer()) {
+    for (final EntityInterface e : getContainer()) {
+      org.caosdb.server.entity.UpdateEntity newEntity = (org.caosdb.server.entity.UpdateEntity) e;
       if (newEntity.getEntityStatus() == EntityStatus.QUALIFIED) {
         innerLoop:
         for (final EntityInterface oldEntity : oldContainer) {
@@ -138,13 +152,7 @@ public class Update extends WriteTransaction<UpdateContainer> {
                 }
               }
 
-              try {
-                checkPermissions(newEntity, deriveUpdate(newEntity, oldEntity));
-              } catch (final AuthorizationException exc) {
-                newEntity.setEntityStatus(EntityStatus.UNQUALIFIED);
-                newEntity.addError(ServerMessages.AUTHORIZATION_ERROR);
-                newEntity.addInfo(exc.getMessage());
-              }
+              newEntity.setOriginal(oldEntity);
             }
             break innerLoop;
           }
-- 
GitLab