diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd1415efae27e94cb5edea8469c5049d246300ec..6e560318bb4372e3882b21c0acb12f2bdd443643 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+* New EntityState plug-in. The plug-in disabled by default and can be enabled
+  by setting the server property `EXT_ENTITY_STATE=ENABLED`. See
+  [!62](https://gitlab.com/caosdb/caosdb-server/-/merge_requests/62) for more
+  information.
 * `ETag` property for the query. The `ETag` is assigned to the query cache
   each time the cache is cleared (currently whenever the server state is being
   updated, i.e. the stored entities change).
diff --git a/caosdb-webui b/caosdb-webui
index 8c59cc861d646cbdba0ec749ba052656f67fd58d..5dfe879722bd01acc5209c581b60bf0ac49635b6 160000
--- a/caosdb-webui
+++ b/caosdb-webui
@@ -1 +1 @@
-Subproject commit 8c59cc861d646cbdba0ec749ba052656f67fd58d
+Subproject commit 5dfe879722bd01acc5209c581b60bf0ac49635b6
diff --git a/conf/core/server.conf b/conf/core/server.conf
index c9151888e785b8114f104fdcd14c43714fc80b37..76ed6030523f24f977f41a510c703fe075f30042 100644
--- a/conf/core/server.conf
+++ b/conf/core/server.conf
@@ -188,3 +188,11 @@ GLOBAL_ENTITY_PERMISSIONS_FILE=./conf/core/global_entity_permissions.xml
 
 # If set to true, versioning of entities' history is enabled.
 ENTITY_VERSIONING_ENABLED=true
+
+
+# --------------------------------------------------
+# Extension settings
+# --------------------------------------------------
+
+# Enabling the state machine extension
+# EXT_STATE_ENTITY=ENABLE
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java
index ec0c0c1b0a067d529ecb72309d7fe83a8b37f819..7af4022595f64b909231c72c83ae1e151a9d06c1 100644
--- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java
@@ -27,10 +27,14 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.shiro.SecurityUtils;
+import org.caosdb.server.database.DatabaseUtils;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.backend.interfaces.RetrieveAllImpl;
 import org.caosdb.server.database.exceptions.TransactionException;
 import org.caosdb.server.entity.Role;
+import org.caosdb.server.permissions.EntityACL;
+import org.caosdb.server.permissions.EntityPermission;
 
 public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImpl {
 
@@ -38,17 +42,20 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp
     super(access);
   }
 
-  public static final String STMT_GET_ALL_HEAD = "Select id from entities where id > 99";
+  public static final String STMT_GET_ALL_HEAD =
+      "SELECT e.id AS ID, a.acl AS ACL FROM entities AS e JOIN entity_acl AS a ON (e.acl = a.id) WHERE e.id > 99";
   public static final String STMT_ENTITY_WHERE_CLAUSE =
-      " AND ( role=? OR role='"
+      " AND ( e.role='"
+          + Role.Record
+          + "' OR e.role='"
           + Role.RecordType
-          + "' OR role='"
+          + "' OR e.role='"
           + Role.Property
-          + "' OR role='"
+          + "' OR e.role='"
           + Role.File
           + "'"
           + " )";
-  public static final String STMT_OTHER_ROLES = " AND role=?";
+  public static final String STMT_OTHER_ROLES = " AND e.role=?";
 
   @Override
   public List<Integer> execute(final String role) throws TransactionException {
@@ -58,10 +65,7 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp
               + (role.equalsIgnoreCase("ENTITY") ? STMT_ENTITY_WHERE_CLAUSE : STMT_OTHER_ROLES);
       final PreparedStatement stmt = prepareStatement(STMT_GET_ALL);
 
-      if (role.equalsIgnoreCase("ENTITY")) {
-        stmt.setString(1, Role.Record.toString());
-
-      } else {
+      if (!role.equalsIgnoreCase("ENTITY")) {
         stmt.setString(1, role);
       }
 
@@ -69,7 +73,11 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp
       try {
         final ArrayList<Integer> ret = new ArrayList<Integer>();
         while (rs.next()) {
-          ret.add(rs.getInt(1));
+          String acl = DatabaseUtils.bytes2UTF8(rs.getBytes("ACL"));
+          if (EntityACL.deserialize(acl)
+              .isPermitted(SecurityUtils.getSubject(), EntityPermission.RETRIEVE_ENTITY)) {
+            ret.add(rs.getInt("ID"));
+          }
         }
         return ret;
       } finally {
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 0000000000000000000000000000000000000000..f6b996770b1d1d88992a56551acc0d5aaa5db1f0
--- /dev/null
+++ b/src/main/java/org/caosdb/server/entity/ClientMessage.java
@@ -0,0 +1,90 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 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;
+
+/**
+ * Class which represents client messages. Client messages is a way to extend the Entity API with
+ * special properties which may be used by plug-ins.
+ *
+ * <p>If no plug-in handles the client message, it is printed back to the response unaltered.
+ *
+ * <p>Client message can have arbitrary key-value (string-string typed) tuples {@link #properties}.
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+public class ClientMessage extends Message {
+
+  private static final long serialVersionUID = 1L;
+  private Map<String, String> properties = new HashMap<>();
+
+  public ClientMessage(String type, String body) {
+    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);
+  }
+
+  /** NB: This is the only place where properties are set in this class. */
+  public static ClientMessage fromXML(Element pe) {
+    ClientMessage result = new ClientMessage(pe.getName(), pe.getText());
+    for (Attribute a : pe.getAttributes()) {
+      result.properties.put(a.getName(), a.getValue());
+    }
+    return result;
+  }
+
+  public String getProperty(String key) {
+    return properties.get(key);
+  }
+
+  @Override
+  public String toString() {
+    return this.type + " - " + this.properties.toString();
+  }
+
+  @Override
+  public int hashCode() {
+    return type.hashCode()
+        + (this.getBody() == null ? 0 : this.getBody().hashCode())
+        + this.properties.hashCode();
+  }
+}
diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java
index aa4c96127cfc97b03f308a50cb587dc12949c105..0a60f9af59f731098a0e194e4848b578094d4e68 100644
--- a/src/main/java/org/caosdb/server/entity/Entity.java
+++ b/src/main/java/org/caosdb/server/entity/Entity.java
@@ -35,6 +35,7 @@ import org.apache.shiro.authz.AuthorizationException;
 import org.apache.shiro.authz.Permission;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBException;
+import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.database.proto.SparseEntity;
 import org.caosdb.server.database.proto.VerySparseEntity;
 import org.caosdb.server.datatype.AbstractCollectionDatatype;
@@ -104,10 +105,12 @@ public class Entity extends AbstractObservable implements EntityInterface {
   public void checkPermission(final Subject subject, final Permission permission) {
     try {
       if (!this.hasPermission(subject, permission)) {
+        String user = "The current user ";
+        if (subject.getPrincipal() instanceof Principal) {
+          user = ((Principal) subject.getPrincipal()).getUsername();
+        }
         throw new AuthorizationException(
-            subject.getPrincipal().toString()
-                + " doesn't have permission "
-                + permission.toString());
+            user + " doesn't have permission " + permission.toString());
       }
     } catch (final NullPointerException e) {
       throw new AuthorizationException("This entity doesn't have an ACL!");
@@ -848,34 +851,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 743a2b62fa87570e0dbca05a1f279e6764920f0b..3e469b2dc84b07e3ac3de378f8990cc520989cc0 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 aa6d591602df66cc9317351cf6a1ea980bc53ea3..884632b5ed3f92d740f2eea69ed49ef77511fd1b 100644
--- a/src/main/java/org/caosdb/server/entity/UpdateEntity.java
+++ b/src/main/java/org/caosdb/server/entity/UpdateEntity.java
@@ -22,11 +22,21 @@
  */
 package org.caosdb.server.entity;
 
+import org.caosdb.server.transaction.WriteTransaction;
 import org.caosdb.server.utils.EntityStatus;
 import org.jdom2.Element;
 
+/**
+ * UpdateEntity class represents entities which are to be updated. The previous version is appeded
+ * during the {@link WriteTransaction} transactions initialization.
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
 public class UpdateEntity extends WritableEntity {
 
+  /** The previous version of this entity. */
+  private EntityInterface original = null;
+
   public UpdateEntity(final Element element) {
     super(element);
   }
@@ -35,4 +45,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 28dc512b669eb6f1df0cfd24e32e2051f734b697..86ba49fabeedf04833f4e82214876be698aaa8cd 100644
--- a/src/main/java/org/caosdb/server/jobs/Job.java
+++ b/src/main/java/org/caosdb/server/jobs/Job.java
@@ -50,10 +50,7 @@ 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;
 
@@ -62,7 +59,7 @@ import org.reflections.Reflections;
  *
  * @todo Describe me.
  */
-public abstract class Job extends AbstractObservable implements Observer {
+public abstract class Job {
   private Transaction<? extends TransactionContainer> transaction = null;
   private Mode mode = null;
 
@@ -139,10 +136,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;
   }
@@ -237,14 +230,6 @@ 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;
-  }
-
   static HashMap<String, Class<? extends Job>> allClasses = null;
   private static List<Class<? extends Job>> loadAlways;
 
@@ -463,10 +448,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 7f35106bcdda8ddc00512197091fa096a6bc157e..77bc57c9e3f4183bf572b6a95dfe16fc79b6e003 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 0000000000000000000000000000000000000000..ee0c29805d7500987f5823f2c7d3eaec8f6ed50b
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java
@@ -0,0 +1,88 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 org.caosdb.server.jobs;
+
+/**
+ * ScheduledJob is a wrapper class for jobs held by the Scheduler.
+ *
+ * <p>It is mainly a means to have simplified interface for the Scheduler which also measures the
+ * execution time of the job "from outside".
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+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 509f87fc8515ca08daa1a5cb8ef6571dfe6fa441..45b5de8e4d5835bd835a797290e2d3c875687e44 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 ce9943ecbefb4d669003a097e74bf9baff078167..f46013d278f268b95b1b105130e1eabd74f3a9f4 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 c6d10a8f4f7cd0656f571a4cbf70385ab9cb7725..02cb96fc1214712c7a8ef1bb98327ba9fb39e9d0 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 10ce6295a5b4910084d769278ca6348287e6223a..f95104bd2f2552a642e7e4bcd2fc13f7fd2788d4 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 59fcfe1d877ca61c98674ee85919f03b7bd4278e..b9f804f519948b605a9fa8820c91495618463fba 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 c9d06cb5487ff62a276ff1028c22864afa850562..8563ea716072602684dc1a78870392d573dc3a22 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
@@ -36,6 +36,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;
 
 /**
@@ -43,7 +44,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 0000000000000000000000000000000000000000..cd187cf17ad72c5431fcd3f2eb0b95890941faf9
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
@@ -0,0 +1,200 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 org.caosdb.server.jobs.core;
+
+import java.util.Map;
+import org.apache.shiro.authz.AuthorizationException;
+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;
+import org.caosdb.server.utils.ServerMessages;
+
+/**
+ * Check if the attempted state transition is allowed.
+ *
+ * <p>This job checks if the attempted state transition is in compliance with the state model. This
+ * job runs during the CHECK phase and should do all necessary consistency and permission checks.
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+@JobAnnotation(time = JobExecutionTime.POST_CHECK, transaction = WriteTransaction.class)
+public class CheckStateTransition extends EntityStateJob {
+
+  private static final String PERMISSION_STATE_FORCE_FINAL = "STATE:FORCE:FINAL";
+  private static final String PERMISSION_STATE_UNASSIGN = "STATE:UNASSIGN:";
+  private static final String PERMISSION_STATE_ASSIGN = "STATE:ASSIGN:";
+  private static final Message TRANSITION_NOT_ALLOWED =
+      new Message(MessageType.Error, "Transition not allowed.");
+  private static final Message INITIAL_STATE_NOT_ALLOWED =
+      new Message(MessageType.Error, "Initial state not allowed.");
+  private static final Message FINAL_STATE_NOT_ALLOWED =
+      new Message(MessageType.Error, "Final state not allowed.");
+
+  /**
+   * The forceFinalState flag is especially useful if you want to delete entities in the middle of
+   * the state machine's usual state cycle.
+   */
+  private static final String FLAG_FORCE_FINAL_STATE = "forceFinalState";
+
+  @Override
+  protected void run() {
+    try {
+      State newState = getState();
+      if (newState != null) {
+        checkStateValid(newState);
+      }
+      if (getEntity() instanceof UpdateEntity) {
+        State oldState = getState(((UpdateEntity) getEntity()).getOriginal());
+        checkStateTransition(oldState, newState);
+      } else if (getEntity() instanceof DeleteEntity) {
+        if (newState != null) checkFinalState(newState);
+      } else { // fresh Entity
+        if (newState != null) checkInitialState(newState);
+      }
+    } catch (Message m) {
+      getEntity().addError(m);
+    } catch (AuthorizationException e) {
+      getEntity().addError(ServerMessages.AUTHORIZATION_ERROR);
+      getEntity().addInfo(e.getMessage());
+    }
+  }
+
+  /**
+   * Check if the state belongs to the state model.
+   *
+   * <p>In practical terms, throw a Message if the state is invalid.
+   *
+   * @param state
+   * @throws Message
+   */
+  private void checkStateValid(State state) throws Message {
+    if (state.isFinal() || state.isInitial() || state.getStateModel().getStates().contains(state)) {
+      return;
+    }
+    throw STATE_NOT_IN_STATE_MODEL;
+  }
+
+  /**
+   * Check if state is valid and transition is allowed.
+   *
+   * <p>Especially, transitions between {@code null} states are allowed, non-trivial transitions
+   * from or to {@code null} must be initial or final states, respectively ({@link
+   * FORCE_FINAL_STATE} exception applies).
+   *
+   * @param oldState
+   * @param newState
+   * @throws Message if not
+   */
+  private void checkStateTransition(State oldState, State newState) throws Message {
+    if (oldState == null && newState == null) {
+      return;
+    } else if (oldState == null && newState != null) {
+      checkInitialState(newState);
+      return;
+    } else if (newState == null && oldState != null) {
+      checkFinalState(oldState);
+      return;
+    }
+
+    StateModel stateModel = findMatchingStateModel(oldState, newState);
+    if (stateModel == null) {
+      // change from one stateModel to another
+      checkInitialState(newState);
+      checkFinalState(oldState);
+      return;
+    }
+
+    boolean transition_defined = false;
+    for (Transition t : stateModel.getTransitions()) {
+      if (t.isFromState(oldState) && t.isToState(newState)) {
+        transition_defined = true;
+        if (t.isPermitted(getUser())) {
+          return;
+        }
+      }
+    }
+    if (transition_defined) {
+      throw new AuthorizationException(
+          getUser().getPrincipal().toString()
+              + " doesn't have permission to perform this transition.");
+    }
+    throw TRANSITION_NOT_ALLOWED;
+  }
+
+  /**
+   * Return the two State's common StateModel, or {@code null} if they don't have one in common.
+   *
+   * @param oldState
+   * @param newState
+   * @return the state model which contains both of the states.
+   * @throws Message if the state model of one of the states cannot be constructed.
+   */
+  private StateModel findMatchingStateModel(State oldState, State newState) throws Message {
+    if (oldState.getStateModel().equals(newState.getStateModel())) {
+      return oldState.getStateModel();
+    }
+    return null;
+  }
+
+  /**
+   * Check if the old state is final or if the {@link FORCE_FINAL_STATE} flag is true.
+   *
+   * @param oldState
+   * @throws Message if the state is not final.
+   */
+  private void checkFinalState(State oldState) throws Message {
+    if (!oldState.isFinal()) {
+      if (isForceFinal()) {
+        getUser().checkPermission(PERMISSION_STATE_FORCE_FINAL);
+      } else {
+        throw FINAL_STATE_NOT_ALLOWED;
+      }
+    }
+    getUser().checkPermission(PERMISSION_STATE_UNASSIGN + oldState.getStateModelName());
+  }
+
+  /**
+   * Check if the new state is an initial state.
+   *
+   * @param newState
+   * @throws Message if not
+   */
+  private void checkInitialState(State newState) throws Message {
+    if (!newState.isInitial()) {
+      throw INITIAL_STATE_NOT_ALLOWED;
+    }
+    getUser().checkPermission(PERMISSION_STATE_ASSIGN + newState.getStateModelName());
+  }
+
+  private boolean isForceFinal() {
+    Map<String, String> containerFlags = getTransaction().getContainer().getFlags();
+    return (containerFlags != null
+            && "true".equalsIgnoreCase(containerFlags.get(FLAG_FORCE_FINAL_STATE)))
+        || "true".equalsIgnoreCase(getEntity().getFlag(FLAG_FORCE_FINAL_STATE));
+  }
+}
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 34e1960961f72118567653a77e4f48fe357dfedc..1dd457143f8cc23c0136f2c51cb40edf8552ba15 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 0000000000000000000000000000000000000000..c20b18b6e4950c07967d2cfcdd776c95c25f4a53
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
@@ -0,0 +1,857 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 org.caosdb.server.jobs.core;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.shiro.subject.Subject;
+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.datatype.TextDatatype;
+import org.caosdb.server.entity.ClientMessage;
+import org.caosdb.server.entity.DeleteEntity;
+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.container.TransactionContainer;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.entity.xml.ToElementable;
+import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.permissions.EntityACI;
+import org.caosdb.server.permissions.EntityACL;
+import org.caosdb.server.query.Query;
+import org.caosdb.server.utils.EntityStatus;
+import org.jdom2.Element;
+
+/**
+ * The EntityStateJob is the abstract base class for four EntityJobs:
+ *
+ * <p>1. The {@link InitEntityState} job reads ClientMessages or StateProperties with tag state and
+ * converts them into instances of State. This job runs during WriteTransactions. This job runs
+ * during the INIT Phase and does not perform any checks other than those necessary for the
+ * conversion.
+ *
+ * <p>2. The {@link CheckStateTransition} job checks if the attempted state transition is in
+ * compliance with the state model. This job runs during the CHECK phase and should do all necessary
+ * consistency and permission checks.
+ *
+ * <p>3. The {@link MakeStateProperty} job constructs an ordinary Property from the State right
+ * before the entity is being written to the back-end and after any checks run.
+ *
+ * <p>4. The {@link MakeStateMessage} job converts a state property (back) into State messages and
+ * appends them to the entity.
+ *
+ * <p>Only the 4th job ({@link MakeStateMessage}) runs during Retrieve transitions. During
+ * WriteTransactions all four jobs do run.
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+public abstract class EntityStateJob extends EntityJob {
+
+  protected static final String SERVER_PROPERTY_EXT_ENTITY_STATE = "EXT_ENTITY_STATE";
+
+  public static final String TO_STATE_PROPERTY_NAME = "to";
+  public static final String FROM_STATE_PROPERTY_NAME = "from";
+  public static final String FINAL_STATE_PROPERTY_NAME = "final";
+  public static final String INITIAL_STATE_PROPERTY_NAME = "initial";
+  public static final String STATE_RECORD_TYPE_NAME = "State";
+  public static final String STATE_MODEL_RECORD_TYPE_NAME = "StateModel";
+  public static final String TRANSITION_RECORD_TYPE_NAME = "Transition";
+  public static final String TRANSITION_XML_TAG = "Transition";
+  public static final String TRANSITION_ATTRIBUTE_NAME = "name";
+  public static final String TRANSITION_ATTRIBUTE_DESCRIPTION = "description";
+  public static final String TO_XML_TAG = "ToState";
+  public static final String FROM_XML_TAG = "FromState";
+  public static final String STATE_XML_TAG = "State";
+  public static final String STATE_ATTRIBUTE_MODEL = "model";
+  public static final String STATE_ATTRIBUTE_NAME = "name";
+  public static final String STATE_ATTRIBUTE_DESCRIPTION = "description";
+  public static final String STATE_ATTRIBUTE_ID = "id";
+  public static final String ENTITY_STATE_ROLE_MARKER = "?STATE?";
+  public static final String PERMISSION_STATE_TRANSION = "STATE:TRANSITION:";
+
+  public static final Message STATE_MODEL_NOT_FOUND =
+      new Message(MessageType.Error, "StateModel not found.");
+  public static final Message STATE_NOT_IN_STATE_MODEL =
+      new Message(MessageType.Error, "State does not exist in this StateModel.");
+  public static final Message COULD_NOT_CONSTRUCT_STATE_MESSAGE =
+      new Message(MessageType.Error, "Could not construct the state message.");
+  public static final Message COULD_NOT_CONSTRUCT_TRANSITIONS =
+      new Message(MessageType.Error, "Could not construct the transitions.");
+  public static final Message STATE_MODEL_NOT_SPECIFIED =
+      new Message(MessageType.Error, "State model not specified.");
+  public static final Message STATE_NOT_SPECIFIED =
+      new Message(MessageType.Error, "State not specified.");
+
+  /**
+   * Represents a Transition which is identified by a name and the two States from and to which an
+   * entity is being transitioned.
+   *
+   * <p>Currently, only exactly one toState and one fromState can be defined. However, it might be
+   * allowed in the future to have multiple states here.
+   *
+   * @author Timm Fitschen (t.fitschen@indiscale.com)
+   */
+  public class Transition {
+
+    private String name;
+    private String description;
+    private State fromState;
+    private State toState;
+    private Map<String, String> transitionProperties;
+
+    /**
+     * @param transition The transition Entity, from which the Transition is created. Relevant
+     *     Properties are "to" and "from"
+     */
+    public Transition(EntityInterface transition) throws Message {
+      this.name = transition.getName();
+      this.description = transition.getDescription();
+      this.fromState = getFromState(transition);
+      this.toState = getToState(transition);
+      this.transitionProperties = getTransitionProperties(transition);
+    }
+
+    private Map<String, String> getTransitionProperties(EntityInterface transition) {
+      Map<String, String> result = new LinkedHashMap<>();
+      for (Property p : transition.getProperties()) {
+        if (p.getDatatype() instanceof TextDatatype) {
+          result.put(p.getName(), p.getValue().toString());
+        }
+      }
+      return result;
+    }
+
+    private State getToState(EntityInterface transition) throws Message {
+      for (Property p : transition.getProperties()) {
+        if (p.getName().equals(TO_STATE_PROPERTY_NAME)) {
+          return createState(p);
+        }
+      }
+      return null;
+    }
+
+    private State getFromState(EntityInterface transition) throws Message {
+      for (Property p : transition.getProperties()) {
+        if (p.getName().equals(FROM_STATE_PROPERTY_NAME)) {
+          return createState(p);
+        }
+      }
+      return null;
+    }
+
+    /**
+     * @param previousState
+     * @return true iff the previous state is a fromState of this transition.
+     */
+    public boolean isFromState(State previousState) {
+      return this.fromState.equals(previousState);
+    }
+
+    /**
+     * @param nextState
+     * @return true iff the next state is a toState of this transition.
+     */
+    public boolean isToState(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) {
+        Transition that = (Transition) obj;
+        return Objects.equals(this.getName(), that.getName())
+            && Objects.equals(this.getFromState(), that.getFromState())
+            && Objects.equals(this.getToState(), that.getToState());
+      }
+      return false;
+    }
+
+    public String getName() {
+      return this.name;
+    }
+
+    public String getDescription() {
+      return this.description;
+    }
+
+    @Override
+    public String toString() {
+      return "Transition (name="
+          + getName()
+          + ", from="
+          + getFromState().getStateName()
+          + ", to="
+          + getToState().getStateName()
+          + ")";
+    }
+
+    public Element toElement() {
+      Element result = new Element(TRANSITION_XML_TAG);
+      if (this.transitionProperties != null) {
+        this.transitionProperties.forEach(
+            (String key, String value) -> {
+              result.setAttribute(key, value);
+            });
+      }
+      if (this.name != null) {
+        result.setAttribute(TRANSITION_ATTRIBUTE_NAME, this.name);
+      }
+      if (this.description != null) {
+        result.setAttribute(TRANSITION_ATTRIBUTE_DESCRIPTION, this.description);
+      }
+      Element to = new Element(TO_XML_TAG);
+      to.setAttribute(STATE_ATTRIBUTE_NAME, this.toState.stateName);
+      if (this.toState.stateDescription != null) {
+        to.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.toState.stateDescription);
+      }
+      Element from = new Element(FROM_XML_TAG);
+      from.setAttribute(STATE_ATTRIBUTE_NAME, this.fromState.stateName);
+      return result.addContent(from).addContent(to);
+    }
+
+    public boolean isPermitted(Subject user) {
+      return user.isPermitted(PERMISSION_STATE_TRANSION + this.name);
+    }
+  }
+
+  /**
+   * The State instance represents a single entity state. This class is used for concrete states
+   * (the state of a stateful entity, say a Record) and abstract states (states which are part of a
+   * {@link StateModel}).
+   *
+   * <p>States are identified via their name and the name of the model to which they belong.
+   *
+   * <p>States are represented by Records with the state's name as the Record name. They belong to a
+   * StateModel iff the StateModel RecordType references the State Record. Each State should only
+   * belong to one StateModel.
+   *
+   * <p>Furthermore, States are the start or end point of {@link Transition Transitions} which
+   * belong to the same StateModel. Each State can be part of several transitions at the same time.
+   *
+   * <p>Note: The purpose of this should not be confused with {@link EntityStatus} which is purely
+   * for internal use.
+   *
+   * @author Timm Fitschen (t.fitschen@indiscale.com)
+   */
+  public class State implements ToElementable {
+
+    private String stateModelName = null;
+    private String stateName = null;
+    private EntityInterface stateEntity = null;
+    private EntityInterface stateModelEntity = null;
+    private StateModel stateModel;
+    private String stateDescription = null;
+    private Integer stateId = null;
+    private EntityACL stateACL = null;
+    private Map<String, String> stateProperties;
+
+    public State(EntityInterface stateEntity, EntityInterface stateModelEntity) throws Message {
+      this.stateEntity = stateEntity;
+      this.stateDescription = stateEntity.getDescription();
+      this.stateId = stateEntity.getId();
+      this.stateName = stateEntity.getName();
+      this.stateModelEntity = stateModelEntity;
+      this.stateModelName = stateModelEntity.getName();
+      this.stateACL = createStateACL(stateEntity.getEntityACL());
+      this.stateProperties = createStateProperties(stateEntity);
+    }
+
+    private Map<String, String> createStateProperties(EntityInterface stateEntity) {
+      Map<String, String> result = new LinkedHashMap<>();
+      for (Property p : stateEntity.getProperties()) {
+        if (p.getDatatype() instanceof TextDatatype) {
+          result.put(p.getName(), p.getValue().toString());
+        }
+      }
+      return result;
+    }
+
+    private EntityACL createStateACL(EntityACL entityACL) {
+      LinkedList<EntityACI> rules = new LinkedList<>();
+      for (EntityACI aci : entityACL.getRules()) {
+        if (aci.getResponsibleAgent().toString().startsWith(ENTITY_STATE_ROLE_MARKER)) {
+          int end = aci.getResponsibleAgent().toString().length() - 1;
+          String role = aci.getResponsibleAgent().toString().substring(7, end);
+          rules.add(
+              new EntityACI(org.caosdb.server.permissions.Role.create(role), aci.getBitSet()));
+        }
+      }
+      return new EntityACL(rules);
+    }
+
+    public EntityACL getStateACL() {
+      return this.stateACL;
+    }
+
+    public String getStateDescription() throws Message {
+      return this.stateDescription;
+    }
+
+    public Integer getStateId() throws Message {
+      return this.stateId;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof State) {
+        State that = (State) obj;
+        return Objects.equals(that.getStateName(), this.getStateName())
+            && Objects.equals(that.getStateModelName(), this.getStateModelName());
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return 21364234 + this.getStateName().hashCode() + this.getStateModelName().hashCode();
+    }
+
+    /**
+     * Serialize this State into XML.
+     *
+     * <p>The result looks approximately like this: {@code <State name="My name" model="Model's
+     * name"/>}
+     */
+    @Override
+    public void addToElement(Element ret) {
+      Element e = new Element(STATE_XML_TAG);
+      if (this.stateProperties != null) {
+        this.stateProperties.forEach(
+            (String key, String value) -> {
+              e.setAttribute(key, value);
+            });
+      }
+      if (this.stateModelName != null) {
+        e.setAttribute(STATE_ATTRIBUTE_MODEL, this.stateModelName);
+      }
+      if (this.stateName != null) {
+        e.setAttribute(STATE_ATTRIBUTE_NAME, this.stateName);
+      }
+      if (this.stateDescription != null) {
+        e.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.stateDescription);
+      }
+      if (this.stateId != null) {
+        e.setAttribute(STATE_ATTRIBUTE_ID, Integer.toString(this.stateId));
+      }
+      if (this.stateModel != null) {
+        this.stateModel.transitions.forEach(
+            (Transition t) -> {
+              if (t.isFromState(this) && t.isPermitted(getUser())) {
+                e.addContent(t.toElement());
+              }
+            });
+      }
+      ret.addContent(e);
+    }
+
+    public String getStateModelName() {
+      return this.stateModelName;
+    }
+
+    private String getStateName() {
+      return this.stateName;
+    }
+
+    public StateModel getStateModel() throws Message {
+      if (this.stateModel == null) {
+        this.stateModel = new StateModel(this.stateModelEntity);
+      }
+      return this.stateModel;
+    }
+
+    /**
+     * @return true iff this state is an initial state of its StateModel.
+     * @throws Message
+     */
+    public boolean isInitial() throws Message {
+      return Objects.equals(this, getStateModel().initialState);
+    }
+
+    /**
+     * @return true iff this state is a final state of its StateModel.
+     * @throws Message
+     */
+    public boolean isFinal() throws Message {
+      return Objects.equals(this, getStateModel().finalState);
+    }
+
+    /**
+     * Create a Property which represents the current entity state of a stateful entity.
+     *
+     * @return stateProperty
+     * @throws Message
+     */
+    public Property createStateProperty() throws Message {
+      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;
+    }
+
+    public EntityInterface getStateEntity() {
+      return this.stateEntity;
+    }
+
+    public EntityInterface getStateModelEntity() {
+      return this.stateModelEntity;
+    }
+
+    @Override
+    public String toString() {
+      String isInitial = null;
+      String isFinal = null;
+      try {
+        isInitial = String.valueOf(isInitial());
+      } catch (Message e) {
+        isInitial = "null";
+      }
+      try {
+        isFinal = String.valueOf(isFinal());
+      } catch (Message e) {
+        isFinal = "null";
+      }
+      return "State (name="
+          + getStateName()
+          + ", model="
+          + getStateModelName()
+          + ", initial="
+          + isInitial
+          + ", final="
+          + isFinal
+          + ")";
+    }
+  }
+
+  /**
+   * A StateModel is an abstract definition of a Finite State Machine for entities.
+   *
+   * <p>It consists of a set of States, a set of transitions, a initial state and a final state.
+   *
+   * <p>If the StateModel has no initial state, it cannot be initialized (no entity will ever be in
+   * any of the StateModel's states) without using the forceInitialState flag.
+   *
+   * <p>If the StateModel has not final state, an entity with any of the states from this StateModel
+   * cannot leave this StateModel (and cannot be deleted either) without using the forceFinalState
+   * flag.
+   *
+   * @author Timm Fitschen (t.fitschen@indiscale.com)
+   */
+  public class StateModel {
+
+    private String name;
+    private Set<State> states;
+    private Set<Transition> transitions;
+    private State initialState;
+    private State finalState;
+
+    public StateModel(EntityInterface stateModelEntity) throws Message {
+      this.name = stateModelEntity.getName();
+      this.transitions = getTransitions(stateModelEntity);
+      this.states = getStates(transitions, this);
+      this.finalState = getFinalState(stateModelEntity);
+      this.initialState = getInitialState(stateModelEntity);
+    }
+
+    private State getInitialState(EntityInterface stateModelEntity) throws Message {
+      // TODO maybe check if there is more than one "initial" Property?
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals(INITIAL_STATE_PROPERTY_NAME)) {
+          return createState(p);
+        }
+      }
+      return null;
+    }
+
+    private State getFinalState(EntityInterface stateModelEntity) throws Message {
+      // TODO maybe check if there is more than one "final" Property?
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals(FINAL_STATE_PROPERTY_NAME)) {
+          return createState(p);
+        }
+      }
+      return null;
+    }
+
+    /** Transitions are taken from list Property with name="Transition". */
+    private Set<Transition> getTransitions(EntityInterface stateModelEntity) throws Message {
+      for (Property p : stateModelEntity.getProperties()) {
+        if (p.getName().equals(TRANSITION_RECORD_TYPE_NAME)) {
+          return createTransitions(p);
+        }
+      }
+      return null;
+    }
+
+    /**
+     * Read out the "Transition" property and create Transition instances.
+     *
+     * @param p the transition property
+     * @return a set of transitions
+     * @throws Message if the transitions could ne be created.
+     */
+    private Set<Transition> createTransitions(Property p) throws Message {
+      Set<Transition> result = new LinkedHashSet<>();
+      try {
+        if (!(p.getDatatype() instanceof AbstractCollectionDatatype)) {
+          // FIXME raise an exception instead?
+          return result;
+        }
+        p.parseValue();
+        CollectionValue vals = (CollectionValue) p.getValue();
+        for (IndexedSingleValue val : vals) {
+          if (val.getWrapped() instanceof ReferenceValue) {
+            Integer refid = ((ReferenceValue) val.getWrapped()).getId();
+
+            String key = "transition" + Integer.toString(refid);
+            EntityInterface transition = getCached(key);
+            if (transition == null) {
+              transition = retrieveValidEntity(refid);
+              putCache(key, transition);
+            }
+            result.add(new Transition(transition));
+          }
+        }
+      } catch (Exception e) {
+        throw COULD_NOT_CONSTRUCT_TRANSITIONS;
+      }
+      return result;
+    }
+
+    /**
+     * Collect all possible states from the set of transitions.
+     *
+     * <p>This function does not perform any consistency checks. It only add all toStates and
+     * fromStates of the transitions to the result.
+     *
+     * @param transitions
+     * @param stateModel
+     * @return set of states.
+     * @throws Message
+     */
+    private Set<State> getStates(Set<Transition> transitions, StateModel stateModel)
+        throws Message {
+      // TODO Move outside of this class
+      Iterator<Transition> it = transitions.iterator();
+      Set<State> result = new LinkedHashSet<>();
+      while (it.hasNext()) {
+        Transition t = it.next();
+        result.add(t.getFromState());
+        result.add(t.getToState());
+      }
+      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).getName().equals(this.getName());
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder("StateModel (name=");
+      sb.append(this.getName());
+      sb.append(", initial=");
+      sb.append(this.getInitialState().stateName);
+      sb.append(", final=");
+      sb.append(this.getFinalState().stateName);
+      sb.append(", transitions=[");
+      Iterator<Transition> iterator = this.transitions.iterator();
+      while (iterator.hasNext()) {
+        sb.append(iterator.next().name);
+        sb.append(" -> ");
+        sb.append(iterator.next().name);
+        sb.append(", ");
+      }
+      sb.append("])");
+      return sb.toString();
+    }
+  }
+
+  private EntityInterface retrieveStateEntity(String stateName) throws Message {
+    try {
+      return retrieveValidEntity(retrieveValidIDByName(stateName));
+    } catch (EntityDoesNotExistException e) {
+      throw STATE_NOT_IN_STATE_MODEL;
+    }
+  }
+
+  private EntityInterface retrieveStateModelEntity(String stateModel) throws Message {
+    try {
+      return retrieveValidEntity(retrieveValidIDByName(stateModel));
+    } catch (EntityDoesNotExistException e) {
+      throw STATE_MODEL_NOT_FOUND;
+    }
+  }
+
+  protected EntityInterface getStateRecordType() throws Message {
+    EntityInterface stateRecordType = getCached(STATE_RECORD_TYPE_NAME);
+    if (stateRecordType == null) {
+      stateRecordType = retrieveValidSparseEntityByName(STATE_RECORD_TYPE_NAME);
+      putCache(STATE_RECORD_TYPE_NAME, stateRecordType);
+    }
+    return stateRecordType;
+  }
+
+  protected State getState() {
+    return getState(false);
+  }
+
+  protected State getState(EntityInterface entity) {
+    return getState(entity, false);
+  }
+
+  protected State getState(EntityInterface entity, boolean remove) {
+    Iterator<ToElementable> messages = entity.getMessages().iterator();
+    while (messages.hasNext()) {
+      ToElementable s = messages.next();
+      if (s instanceof State) {
+        if (remove) {
+          messages.remove();
+        }
+        return (State) s;
+      }
+    }
+    return null;
+  }
+
+  protected State getState(boolean remove) {
+    return getState(getEntity(), remove);
+  }
+
+  /** Return (and possibly remove) the States Properties of `entity`. */
+  protected List<Property> getStateProperties(EntityInterface entity, boolean remove) {
+    Iterator<Property> it = entity.getProperties().iterator();
+    List<Property> result = new ArrayList<>();
+    while (it.hasNext()) {
+      Property p = it.next();
+      if (Objects.equals(p.getName(), STATE_RECORD_TYPE_NAME)) {
+        if (!(p.getDatatype() instanceof ReferenceDatatype)) {
+          continue;
+        }
+        if (remove) {
+          it.remove();
+        }
+        result.add(p);
+      }
+    }
+    return result;
+  }
+
+  protected List<Property> getStateProperties(boolean remove) {
+    return getStateProperties(getEntity(), remove);
+  }
+
+  /** Get the {@code ClientMessage}s which denote a state. */
+  protected List<ClientMessage> getStateClientMessages(EntityInterface entity, boolean remove) {
+    Iterator<ToElementable> stateMessages = entity.getMessages().iterator();
+    List<ClientMessage> result = new ArrayList<>();
+    while (stateMessages.hasNext()) {
+      ToElementable s = stateMessages.next();
+      if (s instanceof ClientMessage && STATE_XML_TAG.equals(((ClientMessage) s).getType())) {
+        if (remove) {
+          stateMessages.remove();
+        }
+        result.add((ClientMessage) s);
+      }
+    }
+    return result;
+  }
+
+  protected List<ClientMessage> getStateClientMessages(boolean remove) {
+    return getStateClientMessages(getEntity(), remove);
+  }
+
+  protected State createState(ClientMessage s) throws Message {
+    String stateModel = s.getProperty(STATE_ATTRIBUTE_MODEL);
+    if (stateModel == null) {
+      throw STATE_MODEL_NOT_SPECIFIED;
+    }
+    String stateName = s.getProperty(STATE_ATTRIBUTE_NAME);
+    if (stateName == null) {
+      throw STATE_NOT_SPECIFIED;
+    }
+    String stateModelKey = "statemodel:" + stateModel;
+
+    EntityInterface stateModelEntity = getCached(stateModelKey);
+    if (stateModelEntity == null) {
+      stateModelEntity = retrieveStateModelEntity(stateModel);
+      putCache(stateModelKey, stateModelEntity);
+    }
+
+    String stateKey = "namestate:" + stateName;
+
+    EntityInterface stateEntity = getCached(stateKey);
+    if (stateEntity == null) {
+      stateEntity = retrieveStateEntity(stateName);
+      putCache(stateKey, stateEntity);
+    }
+    return new State(stateEntity, stateModelEntity);
+  }
+
+  /**
+   * Create a State instance from the value of the state property.
+   *
+   * <p>This method also retrieves the state entity from the back-end. The StateModel is deduced
+   * from finding an appropriately referencing StateModel Record.
+   *
+   * @param p the entity's state property
+   * @return The state of the entity
+   * @throws Message
+   */
+  protected State createState(Property p) throws Message {
+    try {
+      p.parseValue();
+      ReferenceValue refid = (ReferenceValue) p.getValue();
+      String key = "idstate" + Integer.toString(refid.getId());
+
+      EntityInterface stateEntity = getCached(key);
+      if (stateEntity == null) {
+        stateEntity = retrieveValidEntity(refid.getId());
+        putCache(key, stateEntity);
+      }
+
+      EntityInterface stateModelEntity = findStateModel(stateEntity);
+      return new State(stateEntity, stateModelEntity);
+    } catch (Message e) {
+      throw e;
+    } catch (Exception e) {
+      throw COULD_NOT_CONSTRUCT_STATE_MESSAGE;
+    }
+  }
+
+  private static final Map<String, EntityInterface> cache = new HashMap<>();
+  private static final Set<Integer> id_in_cache = new HashSet<>();
+
+  EntityInterface findStateModel(EntityInterface stateEntity) throws Exception {
+    boolean cached = true;
+    String key = "modelof" + Integer.toString(stateEntity.getId());
+
+    EntityInterface result = getCached(key);
+    if (result != null && cached) {
+      return result;
+    }
+    // TODO This should throw a meaningful Exception if no matching StateModel can be found.
+    TransactionContainer c = new TransactionContainer();
+    Query query =
+        new Query(
+            "FIND RECORD "
+                + STATE_MODEL_RECORD_TYPE_NAME
+                + " WHICH REFERENCES "
+                + TRANSITION_RECORD_TYPE_NAME
+                + " WHICH REFERENCES "
+                + Integer.toString(stateEntity.getId()),
+            getUser(),
+            c);
+    query.execute(getTransaction().getAccess());
+    result = retrieveValidEntity(c.get(0).getId());
+    putCache(key, result);
+    return result;
+  }
+
+  private EntityInterface getCached(String key) {
+    EntityInterface result;
+    synchronized (cache) {
+      result = cache.get(key);
+    }
+    return result;
+  }
+
+  private void putCache(String key, EntityInterface value) {
+    synchronized (cache) {
+      if (value instanceof DeleteEntity) {
+        throw new RuntimeException("Delete entity in cache. This is an implementation error.");
+      }
+      id_in_cache.add(value.getId());
+      cache.put(key, value);
+    }
+  }
+
+  protected void removeCached(EntityInterface entity) {
+    synchronized (cache) {
+      if (id_in_cache.contains(entity.getId())) {
+        id_in_cache.remove(entity.getId());
+
+        List<String> remove = new LinkedList<>();
+        for (Entry<String, EntityInterface> entry : cache.entrySet()) {
+          if (entry.getValue().getId().equals(entity.getId())) {
+            remove.add(entry.getKey());
+          }
+        }
+        for (String key : remove) {
+          cache.remove(key);
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java
index b6583f024071e3c1151cf974b387ffe72d1579b7..7f5444b36104526101ebf9b3f2aa547b3d91cbc9 100644
--- a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java
+++ b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java
@@ -22,6 +22,7 @@
  */
 package org.caosdb.server.jobs.core;
 
+import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.jobs.FlagJob;
 import org.caosdb.server.jobs.JobAnnotation;
@@ -51,5 +52,8 @@ public class ExecuteQuery extends FlagJob {
       getContainer().addMessage(new Message(e.getMessage()));
     }
     getContainer().addMessage(queryInstance);
+    for (EntityInterface entity : getContainer()) {
+      getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction()));
+    }
   }
 }
diff --git a/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java b/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java
new file mode 100644
index 0000000000000000000000000000000000000000..bda4b8ebe885aa49df13ecf36ab14663c7ead270
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java
@@ -0,0 +1,36 @@
+package org.caosdb.server.jobs.core;
+
+import java.util.List;
+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.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+
+@JobAnnotation(time = JobExecutionTime.CHECK)
+public class InheritInitialState extends EntityStateJob {
+
+  @Override
+  protected void run() {
+    try {
+      State parentState = getFirstParentState();
+      if (parentState != null) {
+        getEntity().addMessage(parentState);
+        parentState.getStateEntity();
+        getEntity().setEntityACL(parentState.getStateACL());
+      }
+    } catch (Message e) {
+      getEntity().addError(e);
+    }
+  }
+
+  private State getFirstParentState() throws Message {
+    for (Parent par : getEntity().getParents()) {
+      List<Property> stateProperties = getStateProperties(par, false);
+      if (stateProperties.size() > 0) {
+        return createState(stateProperties.get(0));
+      }
+    }
+    return 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 d49bec09c909c5e884eae80619a5911a50b1f333..107c5768a5c15fbb05ea6d68b0cb956cbb235687 100644
--- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
+++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
@@ -34,6 +34,7 @@ import org.caosdb.server.entity.StatementStatus;
 import org.caosdb.server.entity.UpdateEntity;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.jobs.ScheduledJob;
 import org.caosdb.server.utils.EntityStatus;
 
 /**
@@ -65,7 +66,7 @@ public class Inheritance extends EntityJob {
   protected void run() {
     if (getEntity() instanceof InsertEntity || getEntity() instanceof UpdateEntity) {
       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/InitEntityStateJobs.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
new file mode 100644
index 0000000000000000000000000000000000000000..7dbff695a342e5dcd2a19a2d72116c14fba78a6c
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
@@ -0,0 +1,200 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 org.caosdb.server.jobs.core;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.entity.ClientMessage;
+import org.caosdb.server.entity.DeleteEntity;
+import org.caosdb.server.entity.Entity;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.InsertEntity;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.Message.MessageType;
+import org.caosdb.server.entity.Role;
+import org.caosdb.server.entity.UpdateEntity;
+import org.caosdb.server.entity.WritableEntity;
+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;
+
+/**
+ * Initialize the other entity jobs by converting the client message with type "State" or
+ * StateProperties into {@link State} instances.
+ *
+ * <p>This job also needs to initialize the other jobs even if the current entity version does not
+ * have a state anymore but the previous version had, because it has to be checked if the stateModel
+ * allows to leave in this state.
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+@JobAnnotation(
+    loadAlways = true,
+    time = JobExecutionTime.INIT,
+    transaction = WriteTransaction.class)
+public class InitEntityStateJobs extends EntityStateJob implements Observer {
+
+  @Override
+  protected void run() {
+    if ("ENABLED".equals(CaosDBServer.getServerProperty(SERVER_PROPERTY_EXT_ENTITY_STATE))) {
+      State newState = handleNewState();
+      State oldState = handleOldState(newState);
+      if (newState != null || oldState != null) {
+        if (!(getEntity() instanceof DeleteEntity)) {
+
+          appendJob(MakeStateProperty.class);
+        }
+        appendJob(CheckStateTransition.class);
+        appendJob(MakeStateMessage.class);
+      } else if (newState == null
+          && getEntity().getRole() == Role.Record
+          && getEntity() instanceof InsertEntity) {
+        appendJob(InheritInitialState.class);
+        appendJob(CheckStateTransition.class);
+        appendJob(MakeStateProperty.class);
+        appendJob(MakeStateMessage.class);
+      }
+      if (getEntity() instanceof WritableEntity || getEntity() instanceof DeleteEntity) {
+        removeCached(getEntity());
+      }
+    }
+  }
+
+  /**
+   * Converts the state property of the original entity into a state message (only needed for
+   * updates).
+   *
+   * <p>Also, this method adds an observer to the entity state which handles a corner case where the
+   * entity changes the state, but no other property changes. In this case Update.deriveUpdate
+   * cannot detect any changes and will mark this entity as "to-be-skipped". The observer waits for
+   * that to happen and changes the {@EntityStatus} back to normal.
+   *
+   * @param newState
+   * @return The old state or null.
+   */
+  private State handleOldState(State newState) {
+    State oldState = null;
+    try {
+      if (getEntity() instanceof UpdateEntity) {
+        List<State> states = initStateMessage(((UpdateEntity) getEntity()).getOriginal());
+        oldState = null;
+        if (states.size() == 1) {
+          oldState = states.get(0);
+          if (newState != null) {
+            ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL());
+          }
+        }
+        if (!Objects.equals(newState, oldState)) {
+          getEntity().acceptObserver(this);
+        }
+      }
+    } catch (Message m) {
+      getEntity().addWarning(STATE_ERROR_IN_ORIGINAL_ENTITY(m));
+    }
+    return oldState;
+  }
+
+  /**
+   * Converts the state property of this entity into a state message.
+   *
+   * @return The new state or null.
+   */
+  private State handleNewState() {
+    State newState = null;
+    try {
+      List<State> states = initStateMessage(getEntity());
+      if (states.size() > 1) {
+        throw new Message(
+            MessageType.Error, "Currently, each entity can only have one state at a time.");
+      } else if (states.size() == 1) {
+        newState = states.get(0);
+        if (getEntity().getRole() == Role.Record) {
+          transferEntityACL(getEntity(), newState);
+        }
+      }
+    } catch (Message m) {
+      getEntity().addError(m);
+    }
+
+    return newState;
+  }
+
+  private void transferEntityACL(EntityInterface entity, State newState) throws Message {
+    newState.getStateEntity();
+    entity.setEntityACL(newState.getStateACL());
+  }
+
+  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());
+  }
+
+  /**
+   * Return a list of states from their representations as properties or client messages in the
+   * entity.
+   *
+   * @param entity
+   * @return list of state instances for the entity.
+   * @throws Message
+   */
+  private List<State> initStateMessage(EntityInterface entity) throws Message {
+    List<ClientMessage> stateClientMessages = getStateClientMessages(entity, true);
+    List<State> result = new ArrayList<>();
+    if (stateClientMessages != null) {
+      for (ClientMessage s : stateClientMessages) {
+        State stateMessage = createState(s);
+        entity.addMessage(stateMessage);
+        result.add(stateMessage);
+      }
+    }
+    List<Property> stateProperties = getStateProperties(entity, true);
+    if (stateProperties != null) {
+      for (Property p : stateProperties) {
+        State stateMessage = createState(p);
+        entity.addMessage(stateMessage);
+        result.add(stateMessage);
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public boolean notifyObserver(String e, Observable o) {
+    if (e == Entity.ENTITY_STATUS_CHANGED_EVENT) {
+      if (o == getEntity() && getEntity().getEntityStatus() == EntityStatus.VALID) {
+        // The Update.deriveUpdate method didn't recognize that the state is changing and set the
+        // entity to "VALID"
+        getEntity().setEntityStatus(EntityStatus.QUALIFIED);
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e98704a5f6d73199993f2ed531e6d6277934e64
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java
@@ -0,0 +1,108 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 org.caosdb.server.jobs.core;
+
+import java.util.List;
+import java.util.Map;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.entity.xml.ToElementable;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+import org.caosdb.server.transaction.Retrieve;
+import org.jdom2.Element;
+
+/**
+ * Remove the state property from the entity and, iff necessary, convert it into a State instance
+ * which is then being appended to the entity's messages.
+ *
+ * <p>If this job belongs to a Write transaction there is already a State instance present and the
+ * conversion is not necessary.
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+@JobAnnotation(
+    loadAlways = true,
+    transaction = Retrieve.class,
+    time = JobExecutionTime.POST_TRANSACTION)
+public class MakeStateMessage extends EntityStateJob {
+
+  public static final String SPARSE_FLAG = "sparseState";
+
+  @Override
+  protected void run() {
+
+    if ("ENABLED".equals(CaosDBServer.getServerProperty(SERVER_PROPERTY_EXT_ENTITY_STATE))) {
+      try {
+        // fetch all state properties and remove them from the entity (indicated by "true")
+        List<Property> stateProperties = getStateProperties(true);
+        State stateMessage = getState(false);
+
+        if (stateMessage != null) {
+          // trigger retrieval of state model because when the XML Writer calls the addToElement
+          // method, it is to late.
+          stateMessage.getStateModel();
+        } else if (stateProperties != null && stateProperties.size() > 0) {
+          for (Property s : stateProperties) {
+            getEntity().addMessage(getMessage(s, isSparse()));
+          }
+        }
+      } catch (Message e) {
+        getEntity().addError(e);
+      }
+    }
+  }
+
+  private ToElementable getMessage(Property s, boolean sparse) throws Message {
+    if (sparse) {
+      return getSparseStateMessage(s);
+    }
+    State stateMessage = createState(s);
+
+    // trigger retrieval of state model because when the XML Writer calls the addToElement method,
+    // it is to late.
+    stateMessage.getStateModel();
+    return stateMessage;
+  }
+
+  private ToElementable getSparseStateMessage(Property s) {
+    return new ToElementable() {
+      @Override
+      public void addToElement(Element ret) {
+        Element state = new Element(STATE_XML_TAG);
+        state.setAttribute(STATE_ATTRIBUTE_ID, s.getValue().toString());
+        ret.addContent(state);
+      }
+    };
+  }
+
+  private boolean isSparse() {
+    Map<String, String> flags = getTransaction().getContainer().getFlags();
+    if (flags != null) {
+      return "true".equals(flags.getOrDefault(SPARSE_FLAG, "false"));
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java
new file mode 100644
index 0000000000000000000000000000000000000000..56682921563d77f462903567a2c128fcd5d92555
--- /dev/null
+++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java
@@ -0,0 +1,53 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * 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 org.caosdb.server.jobs.core;
+
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.jobs.JobAnnotation;
+import org.caosdb.server.jobs.JobExecutionTime;
+import org.caosdb.server.transaction.WriteTransaction;
+
+/**
+ * This job constructs an ordinary Property from the State right before the entity is being written
+ * to the back-end and after any checks run.
+ */
+@JobAnnotation(transaction = WriteTransaction.class, time = JobExecutionTime.PRE_TRANSACTION)
+public class MakeStateProperty extends EntityStateJob {
+
+  @Override
+  protected void run() {
+    State s = getState();
+    if (s != null) {
+      try {
+        addStateProperty(s);
+      } catch (Message e) {
+        getEntity().addError(e);
+      }
+    }
+  }
+
+  private void addStateProperty(State stateEntity) throws Message {
+    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 dff574a4ad0aa5199fe3ccdfdd9c72d2a7b2bb67..b91fccf72d54022c6f6d19ba26c2b0974a54829f 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 950c61d36255b68f52e3066616b1dee5d9f259c3..e7e8f0e4d6aab3f3d0b3e9392d408a88aaa0abcc 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/RetrieveAllJob.java b/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java
index 1ca43cec45413d80c307df2f4d09c6b8d34cfb49..603d861ad24b659fab262291d888420c02f6bcf5 100644
--- a/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java
+++ b/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java
@@ -23,6 +23,7 @@
 package org.caosdb.server.jobs.core;
 
 import org.caosdb.server.database.backend.transaction.RetrieveAll;
+import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.jobs.FlagJob;
 import org.caosdb.server.jobs.JobAnnotation;
 import org.caosdb.server.jobs.JobExecutionTime;
@@ -37,6 +38,9 @@ public class RetrieveAllJob extends FlagJob {
         value = "ENTITY";
       }
       execute(new RetrieveAll(getContainer(), value));
+      for (EntityInterface entity : getContainer()) {
+        getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction()));
+      }
     }
   }
 }
diff --git a/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java b/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java
index 3a3d03d18bc7b0913a522e2edd6c235f9a7d39a7..c8cd93e4a1d12125235819b4a2ba69fa12bd0256 100644
--- a/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java
+++ b/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
 
@@ -168,32 +169,75 @@ public abstract class AbstractEntityACLFactory<T extends EntityACL> {
     return create(acis);
   }
 
+  /**
+   * Normalize the permission rules.
+   *
+   * <p>This means that rules which are overriden by other rules are removed. E.g. a granting rule
+   * for the permission X and the agent P would be removed if there is a denial of X (for P) with
+   * the same or a higher priority. Likewise, A denial of Y for agent Q would be removed if there is
+   * a granting rule of Y (for Q) with a higher priority.
+   */
   private void normalize() {
-    for (final Entry<ResponsibleAgent, Long> set : this.priorityDenials.entrySet()) {
-      if (this.priorityGrants.containsKey(set.getKey())) {
-        this.priorityGrants.put(
-            set.getKey(), this.priorityGrants.get(set.getKey()) & ~set.getValue());
+    // 1. run through all prioritized denials and remove overriden rules
+    // (priority grants, normal grants and normal denials)
+    Iterator<Entry<ResponsibleAgent, Long>> iterator = this.priorityDenials.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<ResponsibleAgent, Long> next = iterator.next();
+      final ResponsibleAgent agent = next.getKey();
+      long bitset = next.getValue();
+      if (bitset == 0L) {
+        iterator.remove();
+        continue;
       }
-      if (this.normalDenials.containsKey(set.getKey())) {
-        this.normalDenials.put(
-            set.getKey(), this.normalDenials.get(set.getKey()) & ~set.getValue());
+      if (this.priorityGrants.containsKey(agent)) {
+        this.priorityGrants.put(agent, this.priorityGrants.get(agent) & ~bitset);
       }
-      if (this.normalGrants.containsKey(set.getKey())) {
-        this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue());
+      if (this.normalDenials.containsKey(agent)) {
+        this.normalDenials.put(agent, this.normalDenials.get(agent) & ~bitset);
+      }
+      if (this.normalGrants.containsKey(agent)) {
+        this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset);
+      }
+    }
+    // 2. run through all prioritized grants and remove overriden rules (normal
+    // denials and grants)
+    iterator = this.priorityGrants.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<ResponsibleAgent, Long> next = iterator.next();
+      final ResponsibleAgent agent = next.getKey();
+      long bitset = next.getValue();
+      if (bitset == 0L) {
+        iterator.remove();
+        continue;
+      }
+      if (this.normalDenials.containsKey(agent)) {
+        this.normalDenials.put(agent, this.normalDenials.get(agent) & ~bitset);
+      }
+      if (this.normalGrants.containsKey(agent)) {
+        this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset);
       }
     }
-    for (final Entry<ResponsibleAgent, Long> set : this.priorityGrants.entrySet()) {
-      if (this.normalDenials.containsKey(set.getKey())) {
-        this.normalDenials.put(
-            set.getKey(), this.normalDenials.get(set.getKey()) & ~set.getValue());
+    // 3. run through all normal denials and remove overriden rules (normal grants)
+    iterator = this.normalDenials.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<ResponsibleAgent, Long> next = iterator.next();
+      final ResponsibleAgent agent = next.getKey();
+      long bitset = next.getValue();
+      if (bitset == 0L) {
+        iterator.remove();
+        continue;
       }
-      if (this.normalGrants.containsKey(set.getKey())) {
-        this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue());
+      if (this.normalGrants.containsKey(agent)) {
+        this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset);
       }
     }
-    for (final Entry<ResponsibleAgent, Long> set : this.normalDenials.entrySet()) {
-      if (this.normalGrants.containsKey(set.getKey())) {
-        this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue());
+    // finally, remove all remaining empty grants
+    iterator = this.normalGrants.entrySet().iterator();
+    while (iterator.hasNext()) {
+      Entry<ResponsibleAgent, Long> next = iterator.next();
+      long bitset = next.getValue();
+      if (bitset == 0L) {
+        iterator.remove();
       }
     }
   }
@@ -206,4 +250,63 @@ public abstract class AbstractEntityACLFactory<T extends EntityACL> {
   }
 
   protected abstract T create(Collection<EntityACI> acis);
+
+  /**
+   * Remove all rules of the `other` EntityACL from this factory.
+   *
+   * <p>This is mainly used for removing all rules which belong to the global entity ACL from this
+   * ACL before storing it to the backend.
+   *
+   * @param other
+   * @return the same object with changed rule set.
+   */
+  public AbstractEntityACLFactory<T> remove(EntityACL other) {
+    if (other != null) {
+      normalize();
+      for (EntityACI aci : other.getRules()) {
+        if (EntityACL.isAllowance(aci.getBitSet())) {
+          if (EntityACL.isPriorityBitSet(aci.getBitSet())) {
+            Long bitset = this.priorityGrants.get(aci.getResponsibleAgent());
+            if (bitset == null) {
+              continue;
+            }
+            long bitset2 = bitset;
+            bitset2 &= aci.getBitSet();
+            bitset ^= bitset2;
+            this.priorityGrants.put(aci.getResponsibleAgent(), bitset);
+          } else {
+            Long bitset = this.normalGrants.get(aci.getResponsibleAgent());
+            if (bitset == null) {
+              continue;
+            }
+            long bitset2 = bitset;
+            bitset2 &= aci.getBitSet();
+            bitset ^= bitset2;
+            this.normalGrants.put(aci.getResponsibleAgent(), bitset);
+          }
+        } else {
+          if (EntityACL.isPriorityBitSet(aci.getBitSet())) {
+            Long bitset = this.priorityDenials.get(aci.getResponsibleAgent());
+            if (bitset == null) {
+              continue;
+            }
+            long bitset2 = bitset;
+            bitset2 &= aci.getBitSet();
+            bitset ^= bitset2;
+            this.priorityDenials.put(aci.getResponsibleAgent(), bitset);
+          } else {
+            Long bitset = this.normalDenials.get(aci.getResponsibleAgent());
+            if (bitset == null) {
+              continue;
+            }
+            long bitset2 = bitset;
+            bitset2 &= aci.getBitSet();
+            bitset ^= bitset2;
+            this.normalDenials.put(aci.getResponsibleAgent(), bitset);
+          }
+        }
+      }
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/permissions/CaosPermission.java b/src/main/java/org/caosdb/server/permissions/CaosPermission.java
index 1b793bb27f6a14bcf7fd0c5f828c20b73f3d11e5..bfbb7eb2e8515f71bb2d611d4fd75916cfae50e7 100644
--- a/src/main/java/org/caosdb/server/permissions/CaosPermission.java
+++ b/src/main/java/org/caosdb/server/permissions/CaosPermission.java
@@ -24,7 +24,9 @@ package org.caosdb.server.permissions;
 
 import java.util.HashSet;
 import java.util.Map;
+import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authz.Permission;
+import org.apache.shiro.subject.Subject;
 import org.eclipse.jetty.util.ajax.JSON;
 
 public class CaosPermission extends HashSet<PermissionRule> implements Permission {
@@ -52,9 +54,10 @@ public class CaosPermission extends HashSet<PermissionRule> implements Permissio
     boolean grant = false;
     boolean deny = false;
     boolean grant_priority = false;
+    Subject subject = SecurityUtils.getSubject();
 
     for (final PermissionRule r : this) {
-      if (r.getPermission().implies(p)) {
+      if (r.getPermission(subject).implies(p)) {
         if (r.isGrant()) {
           if (r.isPriority()) {
             grant_priority = true;
diff --git a/src/main/java/org/caosdb/server/permissions/EntityACL.java b/src/main/java/org/caosdb/server/permissions/EntityACL.java
index df1915bd460079eb52dfc6d4f3bbf4fe42795918..cfa436d59ae25971a08d4314a7f668a70cf75bbf 100644
--- a/src/main/java/org/caosdb/server/permissions/EntityACL.java
+++ b/src/main/java/org/caosdb/server/permissions/EntityACL.java
@@ -253,7 +253,9 @@ public class EntityACL {
   public final Element toElement() {
     final Element ret = new Element("EntityACL");
 
-    for (final EntityACI aci : this.acl) {
+    final List<EntityACI> localAcl = new ArrayList<>(this.acl);
+    localAcl.addAll(GLOBAL_PERMISSIONS.acl);
+    for (final EntityACI aci : localAcl) {
       final boolean isDenial = isDenial(aci.getBitSet());
       final boolean isPriority = isPriorityBitSet(aci.getBitSet());
       final Element e = new Element(isDenial ? "Deny" : "Grant");
@@ -323,7 +325,7 @@ public class EntityACL {
         }
       }
     }
-    return factory.create();
+    return factory.remove(GLOBAL_PERMISSIONS).create();
   }
 
   public static BitSet convert(final long value) {
@@ -379,7 +381,9 @@ public class EntityACL {
 
   public Element getPermissionsFor(final Subject subject) {
     final Element ret = new Element("Permissions");
-    final Set<EntityPermission> permissionsFor = getPermissionsFor(subject, this.acl);
+    final List<EntityACI> localAcl = new ArrayList<>(this.acl);
+    localAcl.addAll(GLOBAL_PERMISSIONS.acl);
+    final Set<EntityPermission> permissionsFor = getPermissionsFor(subject, localAcl);
     for (final EntityPermission p : permissionsFor) {
       ret.addContent(p.toElement());
     }
diff --git a/src/main/java/org/caosdb/server/permissions/PermissionRule.java b/src/main/java/org/caosdb/server/permissions/PermissionRule.java
index b6ee915156771e1164f3a0ee221d3e14ab964833..85d3b62834a67a4fc46ea9b3c38d19e4b8261d74 100644
--- a/src/main/java/org/caosdb/server/permissions/PermissionRule.java
+++ b/src/main/java/org/caosdb/server/permissions/PermissionRule.java
@@ -26,23 +26,21 @@ import java.util.HashMap;
 import java.util.Map;
 import org.apache.shiro.authz.Permission;
 import org.apache.shiro.authz.permission.WildcardPermission;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.server.accessControl.Principal;
 import org.jdom2.Element;
 
 public class PermissionRule {
 
-  private final WildcardPermission permission;
+  private final String permission;
   private final boolean priority;
   private final boolean grant;
 
   public PermissionRule(final String grant, final String priority, final String permission) {
-    this(
-        Boolean.parseBoolean(grant),
-        Boolean.parseBoolean(priority),
-        new WildcardPermission(permission));
+    this(Boolean.parseBoolean(grant), Boolean.parseBoolean(priority), permission);
   }
 
-  public PermissionRule(
-      final boolean grant, final boolean priority, final WildcardPermission permission) {
+  public PermissionRule(final boolean grant, final boolean priority, final String permission) {
     this.grant = grant;
     this.priority = priority;
     this.permission = permission;
@@ -56,8 +54,9 @@ public class PermissionRule {
     return this.priority;
   }
 
-  public Permission getPermission() {
-    return this.permission;
+  public Permission getPermission(String realm, String username) {
+    return new WildcardPermission(
+        permission.replaceAll("\\?REALM\\?", realm).replaceAll("\\?USERNAME\\?", username));
   }
 
   public static PermissionRule parse(final Map<String, String> rule) {
@@ -69,7 +68,7 @@ public class PermissionRule {
     if (isPriority()) {
       ret.setAttribute("priority", Boolean.toString(true));
     }
-    ret.setAttribute("permission", getPermission().toString());
+    ret.setAttribute("permission", permission);
     return ret;
   }
 
@@ -77,14 +76,19 @@ public class PermissionRule {
     return new PermissionRule(
         e.getName().equalsIgnoreCase("Grant"),
         e.getAttribute("priority") != null && Boolean.parseBoolean(e.getAttributeValue("priority")),
-        new WildcardPermission(e.getAttributeValue("permission")));
+        e.getAttributeValue("permission"));
   }
 
   public Map<String, String> getMap() {
     final HashMap<String, String> ret = new HashMap<String, String>();
     ret.put("priority", Boolean.toString(isPriority()));
     ret.put("grant", Boolean.toString(isGrant()));
-    ret.put("permission", getPermission().toString());
+    ret.put("permission", permission);
     return ret;
   }
+
+  public Permission getPermission(Subject subject) {
+    Principal principal = (Principal) subject.getPrincipal();
+    return getPermission(principal.getRealm(), principal.getUsername());
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java
index fd7bae8a82e95554d527135f76124d397c23f0c4..19f840e92a1a6812e2ae2610ed3ace23b7e3cee7 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(getAccessManager().acquireReadAccess(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 c2bd7e0feb62fa31dadd5580990350b829ff4591..97ed4954b93c55830d1bed5f74bad6e157e70776 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));
@@ -104,17 +105,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra
 
       // additionally load datatype job
       if (e.hasValue()) {
-        boolean found = false;
-        for (final Job j : loadJobs) {
-
-          if (CheckDatatypePresent.class.isInstance(j)
-              && ((CheckDatatypePresent) j).getEntity() == e) {
-            found = true;
-          }
-        }
-        if (!found) {
-          this.schedule.add(new CheckDatatypePresent().init(Mode.MUST, e, this));
-        }
+        this.schedule.add(new CheckDatatypePresent().init(Mode.MUST, e, this));
       }
 
       // load pickup job if necessary
diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
index abda78b6feeb0de7104de5365877b28d474058fe..570fce2c2b001699fdb48c1379e807ae05d62996 100644
--- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
@@ -215,13 +215,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
                     .setFile(oldEntity.getFileProperties().retrieveFromFileSystem());
               }
 
-              try {
-                checkPermissions(entity, deriveUpdate(entity, oldEntity));
-              } catch (final AuthorizationException exc) {
-                entity.setEntityStatus(EntityStatus.UNQUALIFIED);
-                entity.addError(ServerMessages.AUTHORIZATION_ERROR);
-                entity.addInfo(exc.getMessage());
-              }
+              ((UpdateEntity) entity).setOriginal(oldEntity);
             }
             break innerLoop;
           }
@@ -290,6 +284,18 @@ public class WriteTransaction extends Transaction<WritableContainer>
   @Override
   protected void preCheck() throws InterruptedException, Exception {
     for (final EntityInterface entity : getContainer()) {
+      try {
+        if (entity.getEntityStatus() == EntityStatus.QUALIFIED) {
+          checkPermissions(entity, deriveUpdate(entity, ((UpdateEntity) entity).getOriginal()));
+        }
+      } catch (final AuthorizationException exc) {
+        entity.setEntityStatus(EntityStatus.UNQUALIFIED);
+        entity.addError(ServerMessages.AUTHORIZATION_ERROR);
+        entity.addInfo(exc.getMessage());
+      } catch (ClassCastException exc) {
+        // not an update entity. ignore.
+      }
+
       // set default EntityACL if none present
       if (entity.getEntityACL() == null) {
         entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject()));
diff --git a/src/main/java/org/caosdb/server/utils/AbstractObservable.java b/src/main/java/org/caosdb/server/utils/AbstractObservable.java
index a19fff9d44ce7339a9cdc6712e0806ef42bdfd40..abc7f0a0ae1cdcbeff613c2778637de97a507c57 100644
--- a/src/main/java/org/caosdb/server/utils/AbstractObservable.java
+++ b/src/main/java/org/caosdb/server/utils/AbstractObservable.java
@@ -37,6 +37,7 @@ public abstract class AbstractObservable implements Observable {
     return this.observers.add(o);
   }
 
+  /** @param e A String denoting the notification event. */
   @Override
   public void notifyObservers(final String e) {
     if (this.observers != null) {
diff --git a/src/main/java/org/caosdb/server/utils/Observer.java b/src/main/java/org/caosdb/server/utils/Observer.java
index be89dff4b882adf24bcfd250c3dea1387997360d..b4c8f5444b957a1d18efc7f864c67b3daff8ca33 100644
--- a/src/main/java/org/caosdb/server/utils/Observer.java
+++ b/src/main/java/org/caosdb/server/utils/Observer.java
@@ -25,6 +25,8 @@ package org.caosdb.server.utils;
 public interface Observer {
 
   /**
+   * Notify this observer that an event {@code e} has happened to the {@code sender}.
+   *
    * @param e
    * @param sender
    * @return true, iff the Observable has to keep it in the list of observers.
diff --git a/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java b/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java
deleted file mode 100644
index ad643a0aade5e8285fa0aea7a92026995b9ccd61..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java
+++ /dev/null
@@ -1,39 +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 org.caosdb.server.utils.fsm;
-
-public class ActionNotAllowedException extends RuntimeException {
-
-  private final String action;
-
-  public ActionNotAllowedException(final String action) {
-    this.action = action;
-  }
-
-  private static final long serialVersionUID = -4324962066954446942L;
-
-  @Override
-  public String getMessage() {
-    return "The action `" + this.action + "` is not allowed in the current state.";
-  }
-}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java b/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java
deleted file mode 100644
index f8bdccac726bfe25d7644f6dc689b54113f981c7..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java
+++ /dev/null
@@ -1,88 +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 org.caosdb.server.utils.fsm;
-
-import java.util.List;
-import java.util.Map;
-
-public abstract class FiniteStateMachine<S extends State, T extends Transition> {
-
-  public FiniteStateMachine(final S initial, final Map<S, Map<T, S>> transitions)
-      throws StateNotReachableException {
-    this.currentState = initial;
-    this.transitions = transitions;
-    checkEveryStateReachable();
-  }
-
-  private void checkEveryStateReachable() throws StateNotReachableException {
-    for (final State s : getAllStates()) {
-      if (!s.equals(this.currentState) && !stateIsReachable(s)) {
-        throw new StateNotReachableException(s);
-      }
-    }
-  }
-
-  private boolean stateIsReachable(final State s) {
-    for (final Map<T, S> map : this.transitions.values()) {
-      if (map.containsValue(s)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private final Map<S, Map<T, S>> transitions;
-  private S currentState = null;
-
-  public void trigger(final T t) throws TransitionNotAllowedException {
-    final S old = this.currentState;
-    this.currentState = getNextState(t);
-    onAfterTransition(old, t, this.currentState);
-  }
-
-  S getNextState(final T t) throws TransitionNotAllowedException {
-    final Map<T, S> map = this.transitions.get(this.currentState);
-    if (map != null && map.containsKey(t)) {
-      return map.get(t);
-    }
-    throw new TransitionNotAllowedException(this.getCurrentState(), t);
-  }
-
-  public S getCurrentState() {
-    return this.currentState;
-  }
-
-  public List<? extends State> getAllStates() {
-    return this.currentState.getAllStates();
-  }
-
-  /**
-   * Override this method in subclasses. The method is called immediately after a transition
-   * finished.
-   *
-   * @param from
-   * @param transition
-   * @param to
-   */
-  protected void onAfterTransition(final S from, final T transition, final S to) {}
-}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java b/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java
deleted file mode 100644
index 8282e5dd7e7bea4368acf2c7e77b536fab301ad6..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java
+++ /dev/null
@@ -1,32 +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 org.caosdb.server.utils.fsm;
-
-public class MissingImplementationException extends Exception {
-
-  public MissingImplementationException(final State s) {
-    super("The state `" + s.toString() + "` has no implementation.");
-  }
-
-  private static final long serialVersionUID = -1138551658177420875L;
-}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/State.java b/src/main/java/org/caosdb/server/utils/fsm/State.java
deleted file mode 100644
index 81b76aa599a90de2c8b3c8ddd295e62ce47dae12..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/State.java
+++ /dev/null
@@ -1,30 +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 org.caosdb.server.utils.fsm;
-
-import java.util.List;
-
-public interface State {
-
-  public List<State> getAllStates();
-}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java b/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java
deleted file mode 100644
index 3970f60e5cf6e86096b0992419b14350c43b42ee..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java
+++ /dev/null
@@ -1,32 +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 org.caosdb.server.utils.fsm;
-
-public class StateNotReachableException extends Exception {
-
-  public StateNotReachableException(final State s) {
-    super("The state `" + s.toString() + "` is not reachable.");
-  }
-
-  private static final long serialVersionUID = -1826791324169513493L;
-}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java b/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java
deleted file mode 100644
index d37d83783bd7f4aaa1db573f76315d0b51bd5fbb..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java
+++ /dev/null
@@ -1,60 +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 org.caosdb.server.utils.fsm;
-
-import java.util.Map;
-
-public class StrategyFiniteStateMachine<S extends State, T extends Transition, I>
-    extends FiniteStateMachine<S, T> {
-
-  public StrategyFiniteStateMachine(
-      final S initial, final Map<S, I> stateImplementations, final Map<S, Map<T, S>> transitions)
-      throws MissingImplementationException, StateNotReachableException {
-    super(initial, transitions);
-    this.stateImplementations = stateImplementations;
-    checkImplementationsComplete();
-  }
-
-  /**
-   * Check if every state has it's implementation.
-   *
-   * @throws MissingImplementationException
-   */
-  private void checkImplementationsComplete() throws MissingImplementationException {
-    for (final State s : getAllStates()) {
-      if (!this.stateImplementations.containsKey(s)) {
-        throw new MissingImplementationException(s);
-      }
-    }
-  }
-
-  private final Map<S, I> stateImplementations;
-
-  public I getImplementation() {
-    return getImplementation(getCurrentState());
-  }
-
-  public I getImplementation(final S state) {
-    return this.stateImplementations.get(state);
-  }
-}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/Transition.java b/src/main/java/org/caosdb/server/utils/fsm/Transition.java
deleted file mode 100644
index 41921b76cfc7ceed6e396551807e4b199486f0cf..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/Transition.java
+++ /dev/null
@@ -1,25 +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 org.caosdb.server.utils.fsm;
-
-public interface Transition {}
diff --git a/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java b/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java
deleted file mode 100644
index f0547704e250db5c5088c5019a85974c2f4e6254..0000000000000000000000000000000000000000
--- a/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java
+++ /dev/null
@@ -1,37 +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 org.caosdb.server.utils.fsm;
-
-public class TransitionNotAllowedException extends Exception {
-
-  private static final long serialVersionUID = -7236981582249457939L;
-
-  public TransitionNotAllowedException(final State state, final Transition transition) {
-    super(
-        "The transition `"
-            + transition.toString()
-            + "` is not allowed in state `"
-            + state.toString()
-            + ".");
-  }
-}
diff --git a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java
index 0e0e3ee121b09332b6cbb7d86a22458bb5fbf17f..1787c902f48124d692f8c53e4a73ed04564dfe8f 100644
--- a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java
+++ b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java
@@ -22,6 +22,7 @@
  */
 package org.caosdb.server.permissions;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -307,7 +308,7 @@ public class EntityACLTest {
 
   @Test
   public void testFactory() {
-    final EntityACLFactory f = new EntityACLFactory();
+    final AbstractEntityACLFactory<EntityACL> f = new EntityACLFactory();
 
     org.caosdb.server.permissions.Role role1 = org.caosdb.server.permissions.Role.create("role1");
     Config config1 = new Config();
@@ -349,60 +350,49 @@ public class EntityACLTest {
     Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME)));
   }
 
-  //   @Test
-  //   public void niceFactoryStuff() {
-  //   final EntityACLFactory f = new EntityACLFactory();
-  //   f.grant("user1", "*");
-  //   final EntityACL acl1 = f.create();
-  //   Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL));
-  //   Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE));
-  //   Assert.assertTrue(acl1.isPermitted("user1",
-  //   EntityPermission.RETRIEVE_ENTITY));
-  //   Assert.assertTrue(acl1.isPermitted("user1",
-  //   EntityPermission.UPDATE_DATA_TYPE));
-  //   Assert.assertTrue(acl1.isPermitted("user1",
-  //   EntityPermission.USE_AS_PROPERTY));
-  //
-  //   f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*",
-  //   "USE:*");
-  //   f.grant("user2", "EDIT:ACL");
-  //   final EntityACL acl2 = f.create();
-  //   Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL));
-  //   Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE));
-  //   Assert.assertTrue(acl2.isPermitted("user2",
-  //   EntityPermission.RETRIEVE_ENTITY));
-  //   Assert.assertTrue(acl2.isPermitted("user2",
-  //   EntityPermission.UPDATE_DATA_TYPE));
-  //   Assert.assertTrue(acl2.isPermitted("user2",
-  //   EntityPermission.USE_AS_PROPERTY));
-  //
-  //   }
-  //
-  //   @Test
-  //   public void testDeny() {
-  //   EntityACLFactory f = new EntityACLFactory();
-  //   f.deny("test", "DELETE");
-  //   Assert.assertFalse(f.create().isPermitted("test",
-  //   EntityPermission.DELETE));
-  //
-  //   System.out.println(Utils.element2String(f.create().toElement()));
-  //
-  //   System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement()));
-  //
-  //   f.grant("test", "USE:*");
-  //   Assert.assertFalse(f.create().isPermitted("test",
-  //   EntityPermission.DELETE));
-  //
-  //   System.out.println(Utils.element2String(f.create().toElement()));
-  //
-  //   f = new EntityACLFactory();
-  //   f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*");
-  //   f.deny(EntityACL.OTHER_ROLE, "DELETE");
-  //   final EntityACL a = f.create();
-  //
-  //   System.out.println(Utils.element2String(a.toElement()));
-  //
-  //   System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement()));
-  //   }
+  @Test
+  public void testRemove() {
+    EntityACLFactory f = new EntityACLFactory();
+    f.grant(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE);
+    f.deny(org.caosdb.server.permissions.Role.create("role2"), false, EntityPermission.EDIT_ACL);
+    f.grant(
+        org.caosdb.server.permissions.Role.create("role3"), true, EntityPermission.RETRIEVE_ACL);
+    f.deny(
+        org.caosdb.server.permissions.Role.create("role4"), true, EntityPermission.RETRIEVE_ENTITY);
+
+    EntityACL other = f.create();
+
+    f.grant(org.caosdb.server.permissions.Role.create("role2"), false, EntityPermission.EDIT_ACL);
+    f.grant(
+        org.caosdb.server.permissions.Role.create("role5"), false, EntityPermission.RETRIEVE_FILE);
+
+    f.remove(other); // normalize and remove "other"
+
+    EntityACL tester = f.create();
+    assertEquals(
+        "only the very last rule survived, the others have been overriden or removed",
+        1,
+        tester.getRules().size());
+    for (EntityACI aci : tester.getRules()) {
+      assertEquals(aci.getResponsibleAgent(), org.caosdb.server.permissions.Role.create("role5"));
+    }
+  }
 
+  @Test
+  public void testNormalize() {
+    EntityACLFactory f = new EntityACLFactory();
+    f.grant(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE);
+    f.deny(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE);
+    f.grant(org.caosdb.server.permissions.Role.create("role1"), true, EntityPermission.DELETE);
+    f.deny(org.caosdb.server.permissions.Role.create("role1"), true, EntityPermission.DELETE);
+
+    // priority denail overrides everything else
+    EntityACL denyDelete = f.create();
+    assertEquals(1, denyDelete.getRules().size());
+    for (EntityACI aci : denyDelete.getRules()) {
+      assertEquals(org.caosdb.server.permissions.Role.create("role1"), aci.getResponsibleAgent());
+      assertTrue(EntityACL.isDenial(aci.getBitSet()));
+      assertTrue(EntityACL.isPriorityBitSet(aci.getBitSet()));
+    }
+  }
 }
diff --git a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java
index 845f589a9700a9d3d25eb8da2878c13ee114cd7d..7f7434528678dbd2e1886fade2116d0c9f766740 100644
--- a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java
+++ b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java
@@ -29,7 +29,6 @@ import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.authz.permission.WildcardPermission;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.accessControl.AnonymousAuthenticationToken;
@@ -95,9 +94,7 @@ public class TestScriptingResource {
       HashSet<PermissionRule> result = new HashSet<>();
       result.add(
           new PermissionRule(
-              true,
-              false,
-              new WildcardPermission(ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok"))));
+              true, false, ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok")));
       return result;
     }
 
diff --git a/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java b/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java
deleted file mode 100644
index 2a7b726b84464f3a7d287434b2767a34baaf4b50..0000000000000000000000000000000000000000
--- a/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java
+++ /dev/null
@@ -1,90 +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 org.caosdb.server.utils.fsm;
-
-import static org.junit.Assert.assertEquals;
-
-import com.google.common.collect.Lists;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-class SimpleFiniteStateMachine extends FiniteStateMachine<State, Transition> {
-
-  public SimpleFiniteStateMachine(
-      final State initial, final Map<State, Map<Transition, State>> transitions)
-      throws StateNotReachableException {
-    super(initial, transitions);
-  }
-}
-
-enum TestState implements State {
-  State1,
-  State2,
-  State3;
-
-  @Override
-  public List<State> getAllStates() {
-    return Lists.newArrayList(values());
-  }
-}
-
-enum TestTransition implements Transition {
-  toState2,
-  toState3
-}
-
-public class TestFiniteStateMachine {
-
-  @Rule public ExpectedException exc = ExpectedException.none();
-
-  @Test
-  public void testTransitionNotAllowedException()
-      throws StateNotReachableException, TransitionNotAllowedException {
-    final Map<State, Map<Transition, State>> map = new HashMap<>();
-    final HashMap<Transition, State> from1 = new HashMap<>();
-    from1.put(TestTransition.toState2, TestState.State2);
-    from1.put(TestTransition.toState3, TestState.State3);
-    map.put(TestState.State1, from1);
-
-    final SimpleFiniteStateMachine fsm = new SimpleFiniteStateMachine(TestState.State1, map);
-    assertEquals(TestState.State1, fsm.getCurrentState());
-    fsm.trigger(TestTransition.toState2);
-    assertEquals(TestState.State2, fsm.getCurrentState());
-
-    // only 1->2 and from 1->3 is allowed. not 2->3
-    this.exc.expect(TransitionNotAllowedException.class);
-    fsm.trigger(TestTransition.toState3);
-  }
-
-  @Test
-  public void testStateNotReachable() throws StateNotReachableException {
-    final Map<State, Map<Transition, State>> empty = new HashMap<>();
-
-    this.exc.expect(StateNotReachableException.class);
-    new SimpleFiniteStateMachine(TestState.State1, empty);
-  }
-}
diff --git a/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java b/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java
deleted file mode 100644
index ebf719b1eaa4b56f3b64bdd9371f121231b4ed68..0000000000000000000000000000000000000000
--- a/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java
+++ /dev/null
@@ -1,50 +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 org.caosdb.server.utils.fsm;
-
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class TestStrategyFiniteStateMachine {
-
-  @Rule public ExpectedException exc = ExpectedException.none();
-
-  @Test
-  public void testStateHasNoImplementation()
-      throws MissingImplementationException, StateNotReachableException {
-    final Map<State, Map<Transition, State>> map = new HashMap<>();
-    final HashMap<Transition, State> from1 = new HashMap<>();
-    from1.put(TestTransition.toState2, TestState.State2);
-    from1.put(TestTransition.toState3, TestState.State3);
-    map.put(TestState.State1, from1);
-
-    final Map<State, Object> stateImplementations = new HashMap<>();
-
-    this.exc.expect(MissingImplementationException.class);
-    new StrategyFiniteStateMachine<State, Transition, Object>(
-        TestState.State1, stateImplementations, map);
-  }
-}