From 13daf6b96e18b1f2a862261a8f18b36829bcbdd4 Mon Sep 17 00:00:00 2001
From: cramakri <cramakri>
Date: Tue, 22 May 2012 11:34:44 +0000
Subject: [PATCH] SP-45 BIS-21 : Log invocations to performEntityOperations

SVN: 25335
---
 .../openbis/generic/server/ETLService.java    |  40 +++--
 .../generic/server/ETLServiceLogger.java      |   7 +
 .../business/bo/AbstractBusinessObject.java   |   6 +
 .../server/dataaccess/IDAOFactory.java        |   3 +
 .../dataaccess/IEntityOperationsLogDAO.java   |  17 ++
 .../server/dataaccess/db/DAOFactory.java      |   9 +
 .../dataaccess/db/DatabaseVersionHolder.java  |   2 +-
 .../dataaccess/db/EntityOperationsLogDAO.java |  60 +++++++
 .../generic/shared/IETLLIMSService.java       | 157 +++++++-----------
 .../dto/AtomicEntityOperationDetails.java     |  22 ++-
 .../generic/shared/dto/ColumnNames.java       |   2 +
 .../dto/EntityOperationsLogEntryPE.java       |  61 +++++++
 .../generic/shared/dto/SequenceNames.java     |   2 +
 .../generic/shared/dto/TableNames.java        |   2 +
 .../shared/dto/ValidationMessages.java        |   3 +
 openbis/source/java/hibernateContext.xml      |   1 +
 openbis/source/sql/generic/105/schema-105.sql |   8 +-
 .../source/sql/postgresql/105/grants-105.sql  |   2 +
 .../migration/migration-104-105.sql           |  10 +-
 .../generic/server/ETLServiceTest.java        |  97 ++++++++++-
 .../shared/AbstractServerTestCase.java        |   6 +
 .../shared/IETLLIMSService.java.expected      | 157 +++++++-----------
 .../dto/AtomicEntityOperationDetailsTest.java |  10 +-
 .../sql/postgresql/105/finish-105.sql         |   3 +
 .../sql/postgresql/105/schema-105.sql         |  11 ++
 25 files changed, 481 insertions(+), 217 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IEntityOperationsLogDAO.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/EntityOperationsLogDAO.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/EntityOperationsLogEntryPE.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
index b6358c8fcc2..c3e014040a7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
@@ -125,6 +125,7 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.DataStoreServicePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.DatastoreServiceDescriptions;
 import ch.systemsx.cisd.openbis.generic.shared.dto.EntityCollectionForCreationOrUpdate;
+import ch.systemsx.cisd.openbis.generic.shared.dto.EntityOperationsLogEntryPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.EntityTypePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentTypePE;
@@ -1250,19 +1251,16 @@ public class ETLService extends AbstractCommonServer<IETLLIMSService> implements
     public AtomicEntityOperationResult performEntityOperations(String sessionToken,
             AtomicEntityOperationDetails operationDetails)
     {
-        return this.performEntityOperations(sessionToken, operationDetails,
-                new ProgressListener()
-                    {
+        return this.performEntityOperations(sessionToken, operationDetails, new ProgressListener()
+            {
 
-                        public void update(String label, int totalItemsToProcess,
-                                int numItemsProcessed)
-                        {
-                        }
-                    });
+                public void update(String label, int totalItemsToProcess, int numItemsProcessed)
+                {
+                }
+            });
     }
 
-    public AtomicEntityOperationResult performEntityOperations(
-            String sessionToken,
+    public AtomicEntityOperationResult performEntityOperations(String sessionToken,
             AtomicEntityOperationDetails operationDetails, ProgressListener progressListener)
     {
 
@@ -1288,10 +1286,31 @@ public class ETLService extends AbstractCommonServer<IETLLIMSService> implements
         List<ExternalData> dataSetsUpdated =
                 updateDataSets(session, operationDetails, progressListener);
 
+        // If the id is not null, the caller wants to persist the fact that the operation was
+        // invoked and completed;
+        // if the id is null, the caller does not care.
+        TechId registrationId = operationDetails.getRegistrationIdOrNull();
+        if (null != registrationId)
+        {
+            daoFactory.getEntityOperationsLogDAO().addLogEntry(registrationId.getId());
+        }
+
         return new AtomicEntityOperationResult(spacesCreated, projectsCreated, experimentsCreated,
                 samplesUpdated, samplesCreated, materialsCreated, dataSetsCreated, dataSetsUpdated);
     }
 
+    public Boolean didEntityOperationsSucceed(String token, TechId registrationId)
+    {
+        if (registrationId == null)
+        {
+            return false;
+        }
+
+        EntityOperationsLogEntryPE logEntry =
+                daoFactory.getEntityOperationsLogDAO().tryFindLogEntry(registrationId.getId());
+        return logEntry != null;
+    }
+
     private List<Space> createSpaces(Session session,
             AtomicEntityOperationDetails operationDetails, ProgressListener progress)
     {
@@ -1715,5 +1734,4 @@ public class ETLService extends AbstractCommonServer<IETLLIMSService> implements
         }
         return result;
     }
-
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
index 25d9d77f383..e0c3493135f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
@@ -581,4 +581,11 @@ public class ETLServiceLogger extends AbstractServerLogger implements IETLLIMSSe
         return null;
     }
 
+    public Boolean didEntityOperationsSucceed(String token, TechId registrationId)
+    {
+        logAccess(Level.DEBUG, token, "didEntityOperationsSucceed", "REGISTRATION_ID(%s)",
+                registrationId);
+        return null;
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/AbstractBusinessObject.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/AbstractBusinessObject.java
index 85b364db4ce..f0844d35307 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/AbstractBusinessObject.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/AbstractBusinessObject.java
@@ -33,6 +33,7 @@ import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDataSetTypeDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDataStoreDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDatabaseInstanceDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDeletionDAO;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityOperationsLogDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityPropertyHistoryDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityPropertyTypeDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityTypeDAO;
@@ -390,4 +391,9 @@ abstract class AbstractBusinessObject implements IDAOFactory
     {
         return daoFactory.getPostRegistrationDAO();
     }
+
+    public IEntityOperationsLogDAO getEntityOperationsLogDAO()
+    {
+        return daoFactory.getEntityOperationsLogDAO();
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IDAOFactory.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IDAOFactory.java
index fc467a310c9..1cbac222b2f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IDAOFactory.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IDAOFactory.java
@@ -109,4 +109,7 @@ public interface IDAOFactory extends IAuthorizationDAOFactory
     /** Returns an implementation of {@link IPostRegistrationDAO}. */
     public IPostRegistrationDAO getPostRegistrationDAO();
 
+    /** Returns an implementation of {@link IEntityOperationsLogDAO}. */
+    public IEntityOperationsLogDAO getEntityOperationsLogDAO();
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IEntityOperationsLogDAO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IEntityOperationsLogDAO.java
new file mode 100644
index 00000000000..bf336f2ac08
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/IEntityOperationsLogDAO.java
@@ -0,0 +1,17 @@
+package ch.systemsx.cisd.openbis.generic.server.dataaccess;
+
+import ch.systemsx.cisd.openbis.generic.shared.dto.EntityOperationsLogEntryPE;
+
+public interface IEntityOperationsLogDAO
+{
+    /**
+     * Adds an entry to the log for the given registrationId.
+     */
+    public void addLogEntry(Long registrationId);
+
+    /**
+     * Return the entry with the given registrationId or null if none exists
+     */
+    public EntityOperationsLogEntryPE tryFindLogEntry(Long registrationId);
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DAOFactory.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DAOFactory.java
index 20051f169cc..42ba37ac55f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DAOFactory.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DAOFactory.java
@@ -30,6 +30,7 @@ import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDataSetTypeDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDataStoreDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDynamicPropertyEvaluationScheduler;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityOperationsLogDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityPropertyHistoryDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityPropertyTypeDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityTypeDAO;
@@ -108,6 +109,8 @@ public final class DAOFactory extends AuthorizationDAOFactory implements IDAOFac
 
     private final IPostRegistrationDAO postRegistrationDAO;
 
+    private final IEntityOperationsLogDAO entityOperationsLogDAO;
+
     private EntityPropertyHistoryDAO entityPropertyHistoryDAO;
 
     public DAOFactory(final DatabaseConfigurationContext context,
@@ -138,6 +141,7 @@ public final class DAOFactory extends AuthorizationDAOFactory implements IDAOFac
         scriptDAO = new ScriptDAO(sessionFactory, databaseInstance);
         corePluginDAO = new CorePluginDAO(sessionFactory, databaseInstance);
         postRegistrationDAO = new PostRegistrationDAO(sessionFactory, databaseInstance);
+        entityOperationsLogDAO = new EntityOperationsLogDAO(sessionFactory, databaseInstance);
         final EntityKind[] entityKinds = EntityKind.values();
         for (final EntityKind entityKind : entityKinds)
         {
@@ -269,4 +273,9 @@ public final class DAOFactory extends AuthorizationDAOFactory implements IDAOFac
     {
         return postRegistrationDAO;
     }
+
+    public IEntityOperationsLogDAO getEntityOperationsLogDAO()
+    {
+        return entityOperationsLogDAO;
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DatabaseVersionHolder.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DatabaseVersionHolder.java
index 7027d79c1c8..87775343efd 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DatabaseVersionHolder.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/DatabaseVersionHolder.java
@@ -24,7 +24,7 @@ package ch.systemsx.cisd.openbis.generic.server.dataaccess.db;
 public final class DatabaseVersionHolder
 {
     /** Current version of the database. */
-    private static final String DATABASE_VERSION = "105"; // S131
+    private static final String DATABASE_VERSION = "105"; // S132
 
     private DatabaseVersionHolder()
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/EntityOperationsLogDAO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/EntityOperationsLogDAO.java
new file mode 100644
index 00000000000..2e7b12ddea5
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/EntityOperationsLogDAO.java
@@ -0,0 +1,60 @@
+package ch.systemsx.cisd.openbis.generic.server.dataaccess.db;
+
+import org.apache.log4j.Logger;
+import org.hibernate.Criteria;
+import org.hibernate.SessionFactory;
+import org.hibernate.criterion.Restrictions;
+import org.springframework.orm.hibernate3.HibernateTemplate;
+
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityOperationsLogDAO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ColumnNames;
+import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.EntityOperationsLogEntryPE;
+
+public class EntityOperationsLogDAO extends AbstractGenericEntityDAO<EntityOperationsLogEntryPE>
+        implements IEntityOperationsLogDAO
+{
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            EntityOperationsLogDAO.class);
+
+    protected EntityOperationsLogDAO(SessionFactory sessionFactory,
+            DatabaseInstancePE databaseInstance)
+    {
+        super(sessionFactory, databaseInstance, EntityOperationsLogEntryPE.class);
+    }
+
+    public void addLogEntry(Long registrationId)
+    {
+        EntityOperationsLogEntryPE logEntry = new EntityOperationsLogEntryPE();
+        logEntry.setRegistrationId(registrationId);
+
+        HibernateTemplate template = getHibernateTemplate();
+        template.persist(logEntry);
+        template.flush();
+
+        operationLog.info(String.format("Add entity operation log entry for registration id '%d'.",
+                registrationId));
+    }
+
+    public EntityOperationsLogEntryPE tryFindLogEntry(Long registrationId)
+    {
+        assert registrationId != null : "Unspecified registration id.";
+
+        final Criteria criteria = getSession().createCriteria(getEntityClass());
+        criteria.add(Restrictions.eq(ColumnNames.REGISTRATION_ID, registrationId));
+        EntityOperationsLogEntryPE result = (EntityOperationsLogEntryPE) criteria.uniqueResult();
+        if (null != result)
+        {
+            operationLog.info(String.format("Found a log entry for registration id '%d'.",
+                    registrationId));
+        } else
+        {
+            operationLog.info(String.format("Did not find a log entry for registration id '%d'.",
+                    registrationId));
+        }
+        return result;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
index e033fecd653..e52fbe8991e 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
@@ -121,11 +121,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public Experiment tryToGetExperiment(
-            String sessionToken,
+    public Experiment tryToGetExperiment(String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class)
-            ExperimentIdentifier experimentIdentifier)
-            throws UserFailureException;
+            ExperimentIdentifier experimentIdentifier) throws UserFailureException;
 
     /**
      * For given {@link MaterialIdentifier} returns the corresponding {@link Material}.
@@ -164,11 +162,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
      */
     @Transactional(readOnly = true)
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
-    public Sample tryGetSampleWithExperiment(
-            final String sessionToken,
+    public Sample tryGetSampleWithExperiment(final String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSampleOwnerIdentifierPredicate.class)
-            final SampleIdentifier sampleIdentifier)
-            throws UserFailureException;
+            final SampleIdentifier sampleIdentifier) throws UserFailureException;
 
     /**
      * Returns a list of terms belonging to given vocabulary.
@@ -203,11 +199,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<ExternalData> listDataSetsByExperimentID(
-            final String sessionToken,
+    public List<ExternalData> listDataSetsByExperimentID(final String sessionToken,
             @AuthorizationGuard(guardClass = ExperimentTechIdPredicate.class)
-            final TechId experimentID)
-            throws UserFailureException;
+            final TechId experimentID) throws UserFailureException;
 
     /**
      * For given sample {@link TechId} returns the corresponding list of {@link ExternalData}.
@@ -219,8 +213,8 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     public List<ExternalData> listDataSetsBySampleID(final String sessionToken,
             @AuthorizationGuard(guardClass = SampleTechIdPredicate.class)
-            final TechId sampleId,
-            final boolean showOnlyDirectlyConnected) throws UserFailureException;
+            final TechId sampleId, final boolean showOnlyDirectlyConnected)
+            throws UserFailureException;
 
     /**
      * Returns all data sets found for specified data set codes.
@@ -230,11 +224,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<ExternalData> listDataSetsByCode(
-            String sessionToken,
+    public List<ExternalData> listDataSetsByCode(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> dataSetCodes)
-            throws UserFailureException;
+            List<String> dataSetCodes) throws UserFailureException;
 
     /**
      * Lists samples using given configuration.
@@ -245,8 +237,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @ReturnValueFilter(validatorClass = SampleValidator.class)
-    public List<Sample> listSamples(
-            final String sessionToken,
+    public List<Sample> listSamples(final String sessionToken,
             @AuthorizationGuard(guardClass = ListSampleCriteriaPredicate.class)
             final ListSampleCriteria criteria);
 
@@ -262,11 +253,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public IEntityProperty[] tryToGetPropertiesOfTopSampleRegisteredFor(
-            final String sessionToken,
+    public IEntityProperty[] tryToGetPropertiesOfTopSampleRegisteredFor(final String sessionToken,
             @AuthorizationGuard(guardClass = SampleOwnerIdentifierPredicate.class)
-            final SampleIdentifier sampleIdentifier)
-            throws UserFailureException;
+            final SampleIdentifier sampleIdentifier) throws UserFailureException;
 
     /**
      * Registers/updates various entities in one transaction.
@@ -286,8 +275,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseCreateOrDeleteModification(value = ObjectKind.EXPERIMENT)
     public long registerExperiment(String sessionToken,
             @AuthorizationGuard(guardClass = NewExperimentPredicate.class)
-            NewExperiment experiment)
-            throws UserFailureException;
+            NewExperiment experiment) throws UserFailureException;
 
     /**
      * Registers samples in batches.
@@ -295,11 +283,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.SAMPLE)
-    public void registerSamples(
-            final String sessionToken,
+    public void registerSamples(final String sessionToken,
             @AuthorizationGuard(guardClass = NewSamplesWithTypePredicate.class)
-            final List<NewSamplesWithTypes> newSamplesWithType,
-            String userIdOrNull) throws UserFailureException;
+            final List<NewSamplesWithTypes> newSamplesWithType, String userIdOrNull)
+            throws UserFailureException;
 
     /**
      * Registers a new sample.
@@ -309,8 +296,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseCreateOrDeleteModification(value = ObjectKind.SAMPLE)
     public long registerSample(final String sessionToken,
             @AuthorizationGuard(guardClass = NewSamplePredicate.class)
-            final NewSample newSample,
-            String userIDOrNull) throws UserFailureException;
+            final NewSample newSample, String userIDOrNull) throws UserFailureException;
 
     /**
      * Saves changed sample.
@@ -336,11 +322,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseCreateOrDeleteModification(value = ObjectKind.DATA_SET)
-    public void registerDataSet(
-            final String sessionToken,
+    public void registerDataSet(final String sessionToken,
             @AuthorizationGuard(guardClass = SampleOwnerIdentifierPredicate.class)
-            final SampleIdentifier sampleIdentifier,
-            final NewExternalData externalData) throws UserFailureException;
+            final SampleIdentifier sampleIdentifier, final NewExternalData externalData)
+            throws UserFailureException;
 
     /**
      * Registers the specified data connected to an experiment.
@@ -356,11 +341,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseCreateOrDeleteModification(value = ObjectKind.DATA_SET)
-    public void registerDataSet(
-            final String sessionToken,
+    public void registerDataSet(final String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
-            final ExperimentIdentifier experimentIdentifier,
-            final NewExternalData externalData) throws UserFailureException;
+            final ExperimentIdentifier experimentIdentifier, final NewExternalData externalData)
+            throws UserFailureException;
 
     /**
      * Checks that the user of specified session has INSTANCE_ADMIN access rights.
@@ -384,8 +368,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
     public void checkDataSetAccess(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode)
-            throws UserFailureException;
+            String dataSetCode) throws UserFailureException;
 
     /**
      * Check if the current user can access all the data sets in the list
@@ -395,8 +378,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
      */
     @Transactional(readOnly = true)
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
-    public void checkDataSetCollectionAccess(
-            String sessionToken,
+    public void checkDataSetCollectionAccess(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
             List<String> dataSetCodes);
 
@@ -408,8 +390,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     public ExternalData tryGetDataSet(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode)
-            throws UserFailureException;
+            String dataSetCode) throws UserFailureException;
 
     /**
      * Creates and returns a unique code for a new data set. TODO KE: 2011-04-19 remove this method.
@@ -441,11 +422,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Sample> listSamplesByCriteria(
-            final String sessionToken,
+    public List<Sample> listSamplesByCriteria(final String sessionToken,
             @AuthorizationGuard(guardClass = ListSamplesByPropertyPredicate.class)
-            final ListSamplesByPropertyCriteria criteria)
-            throws UserFailureException;
+            final ListSamplesByPropertyCriteria criteria) throws UserFailureException;
 
     /**
      * Lists share ids of all data sets belonging to chosen data store (even the ones in trash!).
@@ -493,8 +472,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(value =
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> listExperiments(
-            String sessionToken,
+    public List<Experiment> listExperiments(String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
             ProjectIdentifier projectIdentifier);
 
@@ -504,8 +482,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> listExperiments(
-            String sessionToken,
+    public List<Experiment> listExperiments(String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
             List<ExperimentIdentifier> experimentIdentifiers,
             ExperimentFetchOptions experimentFetchOptions);
@@ -516,8 +493,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> listExperimentsForProjects(
-            String sessionToken,
+    public List<Experiment> listExperimentsForProjects(String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
             List<ProjectIdentifier> projectIdentifiers,
             ExperimentFetchOptions experimentFetchOptions);
@@ -548,13 +524,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public void addPropertiesToDataSet(
-            String sessionToken,
-            List<NewProperty> properties,
-            String dataSetCode,
-            @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
-            final SpaceIdentifier identifier)
-            throws UserFailureException;
+    public void addPropertiesToDataSet(String sessionToken, List<NewProperty> properties,
+            String dataSetCode, @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
+            final SpaceIdentifier identifier) throws UserFailureException;
 
     /**
      * Updates share id and size of specified data set.
@@ -564,8 +536,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
     public void updateShareIdAndSize(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode,
-            String shareId, long size) throws UserFailureException;
+            String dataSetCode, String shareId, long size) throws UserFailureException;
 
     /**
      * Updates status of given data sets.
@@ -573,12 +544,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public void updateDataSetStatuses(
-            String sessionToken,
+    public void updateDataSetStatuses(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> dataSetCodes,
-            final DataSetArchivingStatus newStatus, boolean presentInArchive)
-            throws UserFailureException;
+            List<String> dataSetCodes, final DataSetArchivingStatus newStatus,
+            boolean presentInArchive) throws UserFailureException;
 
     /**
      * Set the status for a given dataset to the given new status value if the current status equals
@@ -604,11 +573,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public int archiveDatasets(
-            String sessionToken,
+    public int archiveDatasets(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> datasetCodes,
-            boolean removeFromDataStore);
+            List<String> datasetCodes, boolean removeFromDataStore);
 
     /**
      * Schedules unarchiving of specified data sets.
@@ -618,8 +585,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public int unarchiveDatasets(
-            String sessionToken,
+    public int unarchiveDatasets(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
             List<String> datasetCodes);
 
@@ -642,8 +608,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     public ExternalData tryGetDataSetForServer(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode)
-            throws UserFailureException;
+            String dataSetCode) throws UserFailureException;
 
     /**
      * Returns a list of unique codes.
@@ -685,8 +650,8 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { ObjectKind.SAMPLE, ObjectKind.DATA_SET })
     public Sample registerSampleAndDataSet(final String sessionToken,
             @AuthorizationGuard(guardClass = NewSamplePredicate.class)
-            final NewSample newSample,
-            final NewExternalData externalData, String userIdOrNull) throws UserFailureException;
+            final NewSample newSample, final NewExternalData externalData, String userIdOrNull)
+            throws UserFailureException;
 
     /**
      * Updates a sample and registers a data set connected to that sample in one transaction.
@@ -703,11 +668,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.SAMPLE)
     @DatabaseCreateOrDeleteModification(value = ObjectKind.DATA_SET)
-    public Sample updateSampleAndRegisterDataSet(
-            String sessionToken,
+    public Sample updateSampleAndRegisterDataSet(String sessionToken,
             @AuthorizationGuard(guardClass = SampleUpdatesPredicate.class)
-            SampleUpdatesDTO updates,
-            NewExternalData externalData);
+            SampleUpdatesDTO updates, NewExternalData externalData);
 
     /**
      * Updates a sample and registers a data set connected to that sample in one transaction.
@@ -724,8 +687,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseCreateOrDeleteModification(value =
         { ObjectKind.SPACE, ObjectKind.PROJECT, ObjectKind.SAMPLE, ObjectKind.EXPERIMENT,
                 ObjectKind.DATA_SET })
-    public AtomicEntityOperationResult performEntityOperations(
-            String sessionToken,
+    public AtomicEntityOperationResult performEntityOperations(String sessionToken,
             @AuthorizationGuard(guardClass = AtomicOperationsPredicate.class)
             AtomicEntityOperationDetails operationDetails);
 
@@ -735,8 +697,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(value =
         { RoleWithHierarchy.SPACE_ETL_SERVER })
-    public Space tryGetSpace(
-            String sessionToken,
+    public Space tryGetSpace(String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class)
             SpaceIdentifier spaceIdentifier);
 
@@ -746,8 +707,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(value =
         { RoleWithHierarchy.SPACE_ETL_SERVER })
-    public Project tryGetProject(
-            String sessionToken,
+    public Project tryGetProject(String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class)
             ProjectIdentifier projectIdentifier);
 
@@ -782,11 +742,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { ObjectKind.SAMPLE, ObjectKind.EXPERIMENT })
     @DatabaseCreateOrDeleteModification(value =
         { ObjectKind.DATA_SET })
-    public void removeDataSetsPermanently(
-            String sessionToken,
+    public void removeDataSetsPermanently(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> dataSetCodes,
-            String reason);
+            List<String> dataSetCodes, String reason);
 
     /**
      * updates a data set.
@@ -795,8 +753,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value =
         { ObjectKind.EXPERIMENT, ObjectKind.SAMPLE, ObjectKind.DATA_SET })
-    public void updateDataSet(
-            String sessionToken,
+    public void updateDataSet(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetUpdatesPredicate.class)
             DataSetUpdatesDTO dataSetUpdates);
 
@@ -837,4 +794,12 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     public List<ExternalData> listDataSetsForPostRegistration(String token, String dataStoreCode);
+
+    /**
+     * Return true if the log indicates that the performEntityOperations invocation for the given
+     * registrationId succeeded.
+     */
+    @Transactional(readOnly = true)
+    @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
+    public Boolean didEntityOperationsSucceed(String token, TechId registrationId);
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AtomicEntityOperationDetails.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AtomicEntityOperationDetails.java
index e768b2523da..6d2d710fa65 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AtomicEntityOperationDetails.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/AtomicEntityOperationDetails.java
@@ -25,6 +25,7 @@ import java.util.TreeMap;
 import org.apache.commons.lang.builder.ToStringBuilder;
 import org.apache.commons.lang.builder.ToStringStyle;
 
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewMaterial;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewProject;
@@ -41,6 +42,9 @@ public class AtomicEntityOperationDetails implements Serializable
 {
     private static final long serialVersionUID = 1L;
 
+    // The unique identifier for this registration
+    private final TechId registrationIdOrNull;
+
     // The userid on whose behalf the operations are done.
     private final String userIdOrNull;
 
@@ -63,14 +67,16 @@ public class AtomicEntityOperationDetails implements Serializable
     private final List<? extends NewExternalData> dataSetRegistrations;
 
     private final List<DataSetUpdatesDTO> dataSetUpdates;
-    
-    public AtomicEntityOperationDetails(String userIdOrNull, List<NewSpace> spaceRegistrations,
-            List<NewProject> projectRegistrations, List<NewExperiment> experimentRegistrations,
-            List<SampleUpdatesDTO> sampleUpdates, List<NewSample> sampleRegistrations,
+
+    public AtomicEntityOperationDetails(TechId registrationId, String userIdOrNull,
+            List<NewSpace> spaceRegistrations, List<NewProject> projectRegistrations,
+            List<NewExperiment> experimentRegistrations, List<SampleUpdatesDTO> sampleUpdates,
+            List<NewSample> sampleRegistrations,
             Map<String, List<NewMaterial>> materialRegistrations,
             List<? extends NewExternalData> dataSetRegistrations,
             List<DataSetUpdatesDTO> dataSetUpdates)
     {
+        this.registrationIdOrNull = registrationId;
         this.userIdOrNull = userIdOrNull;
         this.spaceRegistrations = new ArrayList<NewSpace>(spaceRegistrations);
         this.projectRegistrations = new ArrayList<NewProject>(projectRegistrations);
@@ -83,6 +89,11 @@ public class AtomicEntityOperationDetails implements Serializable
         this.dataSetUpdates = new ArrayList<DataSetUpdatesDTO>(dataSetUpdates);
     }
 
+    public TechId getRegistrationIdOrNull()
+    {
+        return registrationIdOrNull;
+    }
+
     public String tryUserIdOrNull()
     {
         return userIdOrNull;
@@ -117,7 +128,7 @@ public class AtomicEntityOperationDetails implements Serializable
     {
         return dataSetUpdates;
     }
-    
+
     public List<NewSpace> getSpaceRegistrations()
     {
         return spaceRegistrations;
@@ -137,6 +148,7 @@ public class AtomicEntityOperationDetails implements Serializable
     public String toString()
     {
         ToStringBuilder sb = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
+        sb.append("registrationIdOrNull", registrationIdOrNull);
         sb.append("userIdOrNull", userIdOrNull);
         sb.append("spaceRegistrations", spaceRegistrations);
         sb.append("projectRegistrations", projectRegistrations);
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ColumnNames.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ColumnNames.java
index 65f08fc800d..93394a69693 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ColumnNames.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ColumnNames.java
@@ -298,6 +298,8 @@ public final class ColumnNames
 
     public static final String DELETION_DISALLOW = "deletion_disallow";
 
+    public static final String REGISTRATION_ID = "registration_id";
+
     private ColumnNames()
     {
         // Can not be instantiated.
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/EntityOperationsLogEntryPE.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/EntityOperationsLogEntryPE.java
new file mode 100644
index 00000000000..1cf09e37672
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/EntityOperationsLogEntryPE.java
@@ -0,0 +1,61 @@
+package ch.systemsx.cisd.openbis.generic.shared.dto;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.SequenceGenerator;
+import javax.persistence.Table;
+import javax.validation.constraints.NotNull;
+
+import ch.systemsx.cisd.openbis.generic.server.ETLService;
+import ch.systemsx.cisd.openbis.generic.shared.IServer;
+import ch.systemsx.cisd.openbis.generic.shared.basic.IIdHolder;
+
+/**
+ * Persistent entity representing an invocation of the
+ * {@link ETLService#performEntityOperations(String, AtomicEntityOperationDetails)} method. This
+ * table is used to check if the results of an invocation of this method made it into the database.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+
+@Entity
+@Table(name = TableNames.ENTITY_OPERATIONS_LOG_TABLE)
+public class EntityOperationsLogEntryPE implements IIdHolder, Serializable
+{
+    private static final long serialVersionUID = IServer.VERSION;
+
+    private Long id;
+
+    private Long registrationId;
+
+    @SequenceGenerator(name = SequenceNames.ENTITY_OPERATIONS_LOG_SEQUENCE, sequenceName = SequenceNames.ENTITY_OPERATIONS_LOG_SEQUENCE, allocationSize = 1)
+    @Id
+    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SequenceNames.ENTITY_OPERATIONS_LOG_SEQUENCE)
+    public final Long getId()
+    {
+        return id;
+    }
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    @NotNull(message = ValidationMessages.REGISTRATION_ID_NOT_NULL_MESSAGE)
+    @Column(name = ColumnNames.REGISTRATION_ID)
+    public Long getRegistrationId()
+    {
+        return registrationId;
+    }
+
+    public void setRegistrationId(Long registrationId)
+    {
+        this.registrationId = registrationId;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SequenceNames.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SequenceNames.java
index e1edbd93807..fead77eccb6 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SequenceNames.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SequenceNames.java
@@ -124,6 +124,8 @@ public final class SequenceNames
     public static final String POST_REGISTRATION_DATASET_QUEUE_SEQUENCE =
             "POST_REGISTRATION_DATASET_QUEUE_ID_SEQ";
 
+    public static final String ENTITY_OPERATIONS_LOG_SEQUENCE = "ENTITY_OPERATIONS_LOG_ID_SEQ";
+
     private SequenceNames()
     {
         // Can not be instantiated.
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/TableNames.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/TableNames.java
index f486a6ebfb2..e6bd958a9d0 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/TableNames.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/TableNames.java
@@ -150,6 +150,8 @@ public final class TableNames
     public static final String POST_REGISTRATION_DATASET_QUEUE_TABLE =
             "post_registration_dataset_queue";
 
+    public static final String ENTITY_OPERATIONS_LOG_TABLE = "entity_operations_log";
+
     private TableNames()
     {
         // This class can not be instantiated.
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ValidationMessages.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ValidationMessages.java
index ce3d1d1508f..12b44c211fe 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ValidationMessages.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ValidationMessages.java
@@ -230,6 +230,9 @@ public final class ValidationMessages
 
     public static final String SCRIPT_TYPE_NOT_NULL_MESSAGE = "Script Type" + CAN_NOT_BE_NULL;
 
+    public static final String REGISTRATION_ID_NOT_NULL_MESSAGE = "Regirtation id"
+            + CAN_NOT_BE_NULL;
+
     private ValidationMessages()
     {
         // Can not be instantiated.
diff --git a/openbis/source/java/hibernateContext.xml b/openbis/source/java/hibernateContext.xml
index f5db7f2e970..4dfb4c1eef0 100644
--- a/openbis/source/java/hibernateContext.xml
+++ b/openbis/source/java/hibernateContext.xml
@@ -97,6 +97,7 @@
                 <value>ch.systemsx.cisd.openbis.generic.shared.dto.CorePluginPE</value>
                 <value>ch.systemsx.cisd.openbis.generic.shared.dto.DataSetRelationshipPE</value>
                 <value>ch.systemsx.cisd.openbis.generic.shared.dto.PostRegistrationPE</value>
+                <value>ch.systemsx.cisd.openbis.generic.shared.dto.EntityOperationsLogEntryPE</value>
 
             </list>
         </property>
diff --git a/openbis/source/sql/generic/105/schema-105.sql b/openbis/source/sql/generic/105/schema-105.sql
index e82f5ff0e33..e92d4a4c7c0 100644
--- a/openbis/source/sql/generic/105/schema-105.sql
+++ b/openbis/source/sql/generic/105/schema-105.sql
@@ -56,7 +56,9 @@ CREATE TABLE scripts (ID TECH_ID NOT NULL,DBIN_ID TECH_ID NOT NULL,NAME VARCHAR(
 
 CREATE TABLE CORE_PLUGINS (ID TECH_ID NOT NULL, NAME VARCHAR(200) NOT NULL, VERSION INTEGER NOT NULL, REGISTRATION_TIMESTAMP TIME_STAMP_DFL NOT NULL DEFAULT CURRENT_TIMESTAMP, MASTER_REG_SCRIPT TEXT_VALUE);
 
-CREATE TABLE POST_REGISTRATION_DATASET_QUEUE (ID TECH_ID NOT NULL, DS_ID TECH_ID NOT NULL);   
+CREATE TABLE POST_REGISTRATION_DATASET_QUEUE (ID TECH_ID NOT NULL, DS_ID TECH_ID NOT NULL);
+
+CREATE TABLE ENTITY_OPERATIONS_LOG (ID TECH_ID NOT NULL, REGISTRATION_ID TECH_ID NOT NULL);
 
 -- Creating views - copied from schema generated for tests, '*' can't be used because of PgDiffViews limitation in view comparison
 
@@ -145,6 +147,7 @@ CREATE SEQUENCE SAMPLE_RELATIONSHIP_ID_SEQ;
 CREATE SEQUENCE SCRIPT_ID_SEQ;
 CREATE SEQUENCE CORE_PLUGIN_ID_SEQ;
 CREATE SEQUENCE POST_REGISTRATION_DATASET_QUEUE_ID_SEQ;
+CREATE SEQUENCE ENTITY_OPERATIONS_LOG_ID_SEQ;
 
 -- Creating primary key constraints
 
@@ -195,6 +198,7 @@ ALTER TABLE relationship_types ADD CONSTRAINT rety_pk PRIMARY KEY (id);
 ALTER TABLE sample_relationships_all ADD CONSTRAINT sare_pk PRIMARY KEY (id);
 ALTER TABLE SCRIPTS ADD CONSTRAINT SCRI_PK PRIMARY KEY(ID);
 ALTER TABLE POST_REGISTRATION_DATASET_QUEUE ADD CONSTRAINT PRDQ_PK PRIMARY KEY(ID);
+ALTER TABLE ENTITY_OPERATIONS_LOG ADD CONSTRAINT EOL_PK PRIMARY KEY(ID);
  
 -- Creating unique constraints
 
@@ -247,6 +251,7 @@ ALTER TABLE sample_relationships_all ADD CONSTRAINT sare_bk_uk UNIQUE(sample_id_
 ALTER TABLE relationship_types ADD CONSTRAINT rety_uk UNIQUE(code,dbin_id);
 ALTER TABLE SCRIPTS ADD CONSTRAINT SCRI_UK UNIQUE(NAME,DBIN_ID);
 ALTER TABLE CORE_PLUGINS ADD CONSTRAINT COPL_NAME_VER_UK UNIQUE(NAME,VERSION);
+ALTER TABLE ENTITY_OPERATIONS_LOG ADD CONSTRAINT EOL_REG_ID_UK UNIQUE(REGISTRATION_ID);
 
 
 -- Creating foreign key constraints
@@ -543,3 +548,4 @@ CREATE INDEX GRID_CUSTOM_COLUMNS_PERS_FK_I ON GRID_CUSTOM_COLUMNS (PERS_ID_REGIS
 CREATE INDEX GRID_CUSTOM_COLUMNS_DBIN_FK_I ON GRID_CUSTOM_COLUMNS (DBIN_ID);
 CREATE INDEX SCRIPT_PERS_FK_I ON SCRIPTS (PERS_ID_REGISTERER);
 CREATE INDEX SCRIPT_DBIN_FK_I ON SCRIPTS (DBIN_ID);
+CREATE INDEX ENTITY_OPERATIONS_LOG_RID_I ON ENTITY_OPERATIONS_LOG(REGISTRATION_ID);
diff --git a/openbis/source/sql/postgresql/105/grants-105.sql b/openbis/source/sql/postgresql/105/grants-105.sql
index 58e6a28f7ab..efed69b5ad8 100644
--- a/openbis/source/sql/postgresql/105/grants-105.sql
+++ b/openbis/source/sql/postgresql/105/grants-105.sql
@@ -41,6 +41,7 @@ GRANT SELECT ON SEQUENCE authorization_group_id_seq TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON SEQUENCE filter_id_seq TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON SEQUENCE query_id_seq TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON SEQUENCE POST_REGISTRATION_DATASET_QUEUE_ID_SEQ TO GROUP OPENBIS_READONLY;
+GRANT SELECT ON SEQUENCE ENTITY_OPERATIONS_LOG_ID_SEQ TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON TABLE attachment_contents TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON TABLE attachments TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON TABLE controlled_vocabularies TO GROUP OPENBIS_READONLY;
@@ -98,3 +99,4 @@ GRANT SELECT ON TABLE filters TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON TABLE queries TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON TABLE scripts TO GROUP OPENBIS_READONLY;
 GRANT SELECT ON TABLE POST_REGISTRATION_DATASET_QUEUE TO GROUP OPENBIS_READONLY;
+GRANT SELECT ON TABLE ENTITY_OPERATIONS_LOG TO GROUP OPENBIS_READONLY;
diff --git a/openbis/source/sql/postgresql/migration/migration-104-105.sql b/openbis/source/sql/postgresql/migration/migration-104-105.sql
index 4a68c04938a..9eca380426f 100644
--- a/openbis/source/sql/postgresql/migration/migration-104-105.sql
+++ b/openbis/source/sql/postgresql/migration/migration-104-105.sql
@@ -1,3 +1,11 @@
 -- Migration from 104 to 105
 ALTER DOMAIN DATA_STORE_SERVICE_REPORTING_PLUGIN_TYPE DROP CONSTRAINT DATA_STORE_SERVICE_REPORTING_PLUGIN_TYPE_CHECK;
-ALTER DOMAIN DATA_STORE_SERVICE_REPORTING_PLUGIN_TYPE ADD CONSTRAINT DATA_STORE_SERVICE_REPORTING_PLUGIN_TYPE_CHECK CHECK (VALUE IN ('TABLE_MODEL', 'DSS_LINK', 'AGGREGATION_TABLE_MODEL'));
\ No newline at end of file
+ALTER DOMAIN DATA_STORE_SERVICE_REPORTING_PLUGIN_TYPE ADD CONSTRAINT DATA_STORE_SERVICE_REPORTING_PLUGIN_TYPE_CHECK CHECK (VALUE IN ('TABLE_MODEL', 'DSS_LINK', 'AGGREGATION_TABLE_MODEL'));
+
+CREATE TABLE ENTITY_OPERATIONS_LOG (ID TECH_ID NOT NULL, REGISTRATION_ID TECH_ID NOT NULL);
+CREATE SEQUENCE ENTITY_OPERATIONS_LOG_ID_SEQ;
+ALTER TABLE ENTITY_OPERATIONS_LOG ADD CONSTRAINT EOL_PK PRIMARY KEY(ID);
+ALTER TABLE ENTITY_OPERATIONS_LOG ADD CONSTRAINT EOL_REG_ID_UK UNIQUE(REGISTRATION_ID);
+CREATE INDEX ENTITY_OPERATIONS_LOG_RID_I ON ENTITY_OPERATIONS_LOG(REGISTRATION_ID);
+GRANT SELECT ON SEQUENCE ENTITY_OPERATIONS_LOG_ID_SEQ TO GROUP OPENBIS_READONLY;
+GRANT SELECT ON TABLE ENTITY_OPERATIONS_LOG TO GROUP OPENBIS_READONLY;
\ No newline at end of file
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceTest.java
index f737c2e1998..061fe17ca45 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceTest.java
@@ -50,6 +50,7 @@ import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchCriteria;
 import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchCriteria.MatchClause;
 import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchCriteria.MatchClauseAttribute;
 import ch.systemsx.cisd.openbis.generic.shared.basic.BasicConstant;
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataStoreServiceKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataTypeCode;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatastoreServiceDescription;
@@ -975,7 +976,7 @@ public class ETLServiceTest extends AbstractServerTestCase
     }
 
     @Test
-    public void testPerformOperations()
+    public void testPerformOperationsWithoutRegistrationId()
     {
         prepareGetSession();
 
@@ -1019,6 +1020,41 @@ public class ETLServiceTest extends AbstractServerTestCase
         dataSetUpdate.setModifiedContainedDatasetCodesOrNull(new String[]
             { "c1", "c2" });
 
+        prepareEntityOperationsExpectations(samplePE, sampleUpdate, material, materialType,
+                newMaterial, newSamplePE, newSampleIdentifier, newSample, externalData,
+                updatedDataSetCode, dataSetUpdate);
+
+        AtomicEntityOperationDetails details =
+                new AtomicEntityOperationDetails(null, null, new ArrayList<NewSpace>(),
+                        new ArrayList<NewProject>(), new ArrayList<NewExperiment>(),
+                        Collections.singletonList(sampleUpdate),
+                        Collections.singletonList(newSample), materialRegistrations,
+                        Collections.singletonList(externalData),
+                        Collections.singletonList(dataSetUpdate));
+
+        AtomicEntityOperationResult result =
+                createService().performEntityOperations(SESSION_TOKEN, details);
+        assertNotNull(result);
+        assertEquals(sampleUpdate.getSampleIdentifier().toString(),
+                result.getSamplesUpdated().get(0).getIdentifier());
+        assertEquals(experiment.getIdentifier(), result.getSamplesUpdated().get(0).getExperiment()
+                .getIdentifier());
+
+        assertEquals(newSample.getIdentifier(), result.getSamplesCreated().get(0).getIdentifier());
+        assertEquals(experiment.getIdentifier(), result.getSamplesCreated().get(0).getExperiment()
+                .getIdentifier());
+        assertEquals(updatedDataSetCode, result.getDataSetsUpdated().get(0).getCode());
+
+        context.assertIsSatisfied();
+    }
+
+    private void prepareEntityOperationsExpectations(final SamplePE samplePE,
+            final SampleUpdatesDTO sampleUpdate, final MaterialPE material,
+            final MaterialTypePE materialType, final NewMaterial newMaterial,
+            final SamplePE newSamplePE, final SampleIdentifier newSampleIdentifier,
+            final NewSample newSample, final NewExternalData externalData,
+            final String updatedDataSetCode, final DataSetUpdatesDTO dataSetUpdate)
+    {
         context.checking(new Expectations()
             {
                 {
@@ -1106,9 +1142,66 @@ public class ETLServiceTest extends AbstractServerTestCase
                     will(returnValue(updatedDataSet));
                 }
             });
+    }
+
+    @Test
+    public void testPerformOperations()
+    {
+        prepareGetSession();
+
+        final ExperimentPE experiment = createExperiment("TYPE", "EXP1", "G1");
+        final SamplePE samplePE = createSampleWithExperiment(experiment);
+        final SampleIdentifier sampleIdentifier = samplePE.getSampleIdentifier();
+
+        final Date version = new Date();
+        final Collection<NewAttachment> attachments = Collections.<NewAttachment> emptyList();
+
+        final SampleUpdatesDTO sampleUpdate =
+                new SampleUpdatesDTO(CommonTestUtils.TECH_ID, null, null, attachments, version,
+                        sampleIdentifier, null, null);
+
+        final MaterialPE material = new MaterialPE();
+        material.setCode("new-material");
+        final MaterialTypePE materialType = new MaterialTypePE();
+        materialType.setCode("new-material-type");
+        materialType.setDatabaseInstance(new DatabaseInstancePEBuilder().code("DB")
+                .getDatabaseInstance());
+        final NewMaterial newMaterial = new NewMaterial(material.getCode());
+        Map<String, List<NewMaterial>> materialRegistrations =
+                new HashMap<String, List<NewMaterial>>();
+        materialRegistrations.put(materialType.getCode(), Arrays.asList(newMaterial));
+
+        final SamplePE newSamplePE = createSampleWithExperiment(experiment);
+        newSamplePE.setCode("SAMPLE_CODE_NEW");
+        final SampleIdentifier newSampleIdentifier = newSamplePE.getSampleIdentifier();
+        final NewSample newSample = new NewSample();
+        newSample.setIdentifier(newSampleIdentifier.toString());
+
+        final NewExternalData externalData = new NewExternalData();
+        externalData.setCode("dc");
+        externalData.setMeasured(true);
+        externalData.setSampleIdentifierOrNull(newSampleIdentifier);
+
+        final String updatedDataSetCode = "updateDataSetCode";
+        final DataSetUpdatesDTO dataSetUpdate = new DataSetUpdatesDTO();
+        dataSetUpdate.setDatasetId(CommonTestUtils.TECH_ID);
+        dataSetUpdate.setFileFormatTypeCode("new-file-format");
+        dataSetUpdate.setModifiedContainedDatasetCodesOrNull(new String[]
+            { "c1", "c2" });
+
+        prepareEntityOperationsExpectations(samplePE, sampleUpdate, material, materialType,
+                newMaterial, newSamplePE, newSampleIdentifier, newSample, externalData,
+                updatedDataSetCode, dataSetUpdate);
+
+        context.checking(new Expectations()
+            {
+                {
+                    one(entityOperationsLogDAO).addLogEntry(new Long(1));
+                }
+            });
 
         AtomicEntityOperationDetails details =
-                new AtomicEntityOperationDetails(null, new ArrayList<NewSpace>(),
+                new AtomicEntityOperationDetails(new TechId(1), null, new ArrayList<NewSpace>(),
                         new ArrayList<NewProject>(), new ArrayList<NewExperiment>(),
                         Collections.singletonList(sampleUpdate),
                         Collections.singletonList(newSample), materialRegistrations,
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/AbstractServerTestCase.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/AbstractServerTestCase.java
index fcad72be014..2ca9c55e4e5 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/AbstractServerTestCase.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/AbstractServerTestCase.java
@@ -60,6 +60,7 @@ import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDataSetTypeDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDataStoreDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDatabaseInstanceDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDeletionDAO;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityOperationsLogDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IEntityTypeDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IExperimentDAO;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IFileFormatTypeDAO;
@@ -197,6 +198,8 @@ public abstract class AbstractServerTestCase extends AssertJUnit
 
     protected IHibernateSearchDAO hibernateSearchDAO;
 
+    protected IEntityOperationsLogDAO entityOperationsLogDAO;
+
     @BeforeMethod
     @SuppressWarnings("unchecked")
     public void setUp()
@@ -229,6 +232,7 @@ public abstract class AbstractServerTestCase extends AssertJUnit
         queryDAO = context.mock(IQueryDAO.class);
         deletionDAO = context.mock(IDeletionDAO.class);
         hibernateSearchDAO = context.mock(IHibernateSearchDAO.class);
+        entityOperationsLogDAO = context.mock(IEntityOperationsLogDAO.class);
         // BO
         groupBO = context.mock(IGroupBO.class);
         entityTypeBO = context.mock(IEntityTypeBO.class);
@@ -288,6 +292,8 @@ public abstract class AbstractServerTestCase extends AssertJUnit
                     will(returnValue(deletionDAO));
                     allowing(daoFactory).getHibernateSearchDAO();
                     will(returnValue(hibernateSearchDAO));
+                    allowing(daoFactory).getEntityOperationsLogDAO();
+                    will(returnValue(entityOperationsLogDAO));
                 }
             });
     }
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java.expected b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java.expected
index e033fecd653..e52fbe8991e 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java.expected
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java.expected
@@ -121,11 +121,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public Experiment tryToGetExperiment(
-            String sessionToken,
+    public Experiment tryToGetExperiment(String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class)
-            ExperimentIdentifier experimentIdentifier)
-            throws UserFailureException;
+            ExperimentIdentifier experimentIdentifier) throws UserFailureException;
 
     /**
      * For given {@link MaterialIdentifier} returns the corresponding {@link Material}.
@@ -164,11 +162,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
      */
     @Transactional(readOnly = true)
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
-    public Sample tryGetSampleWithExperiment(
-            final String sessionToken,
+    public Sample tryGetSampleWithExperiment(final String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSampleOwnerIdentifierPredicate.class)
-            final SampleIdentifier sampleIdentifier)
-            throws UserFailureException;
+            final SampleIdentifier sampleIdentifier) throws UserFailureException;
 
     /**
      * Returns a list of terms belonging to given vocabulary.
@@ -203,11 +199,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<ExternalData> listDataSetsByExperimentID(
-            final String sessionToken,
+    public List<ExternalData> listDataSetsByExperimentID(final String sessionToken,
             @AuthorizationGuard(guardClass = ExperimentTechIdPredicate.class)
-            final TechId experimentID)
-            throws UserFailureException;
+            final TechId experimentID) throws UserFailureException;
 
     /**
      * For given sample {@link TechId} returns the corresponding list of {@link ExternalData}.
@@ -219,8 +213,8 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     public List<ExternalData> listDataSetsBySampleID(final String sessionToken,
             @AuthorizationGuard(guardClass = SampleTechIdPredicate.class)
-            final TechId sampleId,
-            final boolean showOnlyDirectlyConnected) throws UserFailureException;
+            final TechId sampleId, final boolean showOnlyDirectlyConnected)
+            throws UserFailureException;
 
     /**
      * Returns all data sets found for specified data set codes.
@@ -230,11 +224,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<ExternalData> listDataSetsByCode(
-            String sessionToken,
+    public List<ExternalData> listDataSetsByCode(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> dataSetCodes)
-            throws UserFailureException;
+            List<String> dataSetCodes) throws UserFailureException;
 
     /**
      * Lists samples using given configuration.
@@ -245,8 +237,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @ReturnValueFilter(validatorClass = SampleValidator.class)
-    public List<Sample> listSamples(
-            final String sessionToken,
+    public List<Sample> listSamples(final String sessionToken,
             @AuthorizationGuard(guardClass = ListSampleCriteriaPredicate.class)
             final ListSampleCriteria criteria);
 
@@ -262,11 +253,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public IEntityProperty[] tryToGetPropertiesOfTopSampleRegisteredFor(
-            final String sessionToken,
+    public IEntityProperty[] tryToGetPropertiesOfTopSampleRegisteredFor(final String sessionToken,
             @AuthorizationGuard(guardClass = SampleOwnerIdentifierPredicate.class)
-            final SampleIdentifier sampleIdentifier)
-            throws UserFailureException;
+            final SampleIdentifier sampleIdentifier) throws UserFailureException;
 
     /**
      * Registers/updates various entities in one transaction.
@@ -286,8 +275,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseCreateOrDeleteModification(value = ObjectKind.EXPERIMENT)
     public long registerExperiment(String sessionToken,
             @AuthorizationGuard(guardClass = NewExperimentPredicate.class)
-            NewExperiment experiment)
-            throws UserFailureException;
+            NewExperiment experiment) throws UserFailureException;
 
     /**
      * Registers samples in batches.
@@ -295,11 +283,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.SAMPLE)
-    public void registerSamples(
-            final String sessionToken,
+    public void registerSamples(final String sessionToken,
             @AuthorizationGuard(guardClass = NewSamplesWithTypePredicate.class)
-            final List<NewSamplesWithTypes> newSamplesWithType,
-            String userIdOrNull) throws UserFailureException;
+            final List<NewSamplesWithTypes> newSamplesWithType, String userIdOrNull)
+            throws UserFailureException;
 
     /**
      * Registers a new sample.
@@ -309,8 +296,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseCreateOrDeleteModification(value = ObjectKind.SAMPLE)
     public long registerSample(final String sessionToken,
             @AuthorizationGuard(guardClass = NewSamplePredicate.class)
-            final NewSample newSample,
-            String userIDOrNull) throws UserFailureException;
+            final NewSample newSample, String userIDOrNull) throws UserFailureException;
 
     /**
      * Saves changed sample.
@@ -336,11 +322,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseCreateOrDeleteModification(value = ObjectKind.DATA_SET)
-    public void registerDataSet(
-            final String sessionToken,
+    public void registerDataSet(final String sessionToken,
             @AuthorizationGuard(guardClass = SampleOwnerIdentifierPredicate.class)
-            final SampleIdentifier sampleIdentifier,
-            final NewExternalData externalData) throws UserFailureException;
+            final SampleIdentifier sampleIdentifier, final NewExternalData externalData)
+            throws UserFailureException;
 
     /**
      * Registers the specified data connected to an experiment.
@@ -356,11 +341,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseCreateOrDeleteModification(value = ObjectKind.DATA_SET)
-    public void registerDataSet(
-            final String sessionToken,
+    public void registerDataSet(final String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
-            final ExperimentIdentifier experimentIdentifier,
-            final NewExternalData externalData) throws UserFailureException;
+            final ExperimentIdentifier experimentIdentifier, final NewExternalData externalData)
+            throws UserFailureException;
 
     /**
      * Checks that the user of specified session has INSTANCE_ADMIN access rights.
@@ -384,8 +368,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
     public void checkDataSetAccess(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode)
-            throws UserFailureException;
+            String dataSetCode) throws UserFailureException;
 
     /**
      * Check if the current user can access all the data sets in the list
@@ -395,8 +378,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
      */
     @Transactional(readOnly = true)
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
-    public void checkDataSetCollectionAccess(
-            String sessionToken,
+    public void checkDataSetCollectionAccess(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
             List<String> dataSetCodes);
 
@@ -408,8 +390,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
     public ExternalData tryGetDataSet(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode)
-            throws UserFailureException;
+            String dataSetCode) throws UserFailureException;
 
     /**
      * Creates and returns a unique code for a new data set. TODO KE: 2011-04-19 remove this method.
@@ -441,11 +422,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Sample> listSamplesByCriteria(
-            final String sessionToken,
+    public List<Sample> listSamplesByCriteria(final String sessionToken,
             @AuthorizationGuard(guardClass = ListSamplesByPropertyPredicate.class)
-            final ListSamplesByPropertyCriteria criteria)
-            throws UserFailureException;
+            final ListSamplesByPropertyCriteria criteria) throws UserFailureException;
 
     /**
      * Lists share ids of all data sets belonging to chosen data store (even the ones in trash!).
@@ -493,8 +472,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(value =
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> listExperiments(
-            String sessionToken,
+    public List<Experiment> listExperiments(String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
             ProjectIdentifier projectIdentifier);
 
@@ -504,8 +482,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> listExperiments(
-            String sessionToken,
+    public List<Experiment> listExperiments(String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
             List<ExperimentIdentifier> experimentIdentifiers,
             ExperimentFetchOptions experimentFetchOptions);
@@ -516,8 +493,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(
         { RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
-    public List<Experiment> listExperimentsForProjects(
-            String sessionToken,
+    public List<Experiment> listExperimentsForProjects(String sessionToken,
             @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
             List<ProjectIdentifier> projectIdentifiers,
             ExperimentFetchOptions experimentFetchOptions);
@@ -548,13 +524,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public void addPropertiesToDataSet(
-            String sessionToken,
-            List<NewProperty> properties,
-            String dataSetCode,
-            @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
-            final SpaceIdentifier identifier)
-            throws UserFailureException;
+    public void addPropertiesToDataSet(String sessionToken, List<NewProperty> properties,
+            String dataSetCode, @AuthorizationGuard(guardClass = SpaceIdentifierPredicate.class)
+            final SpaceIdentifier identifier) throws UserFailureException;
 
     /**
      * Updates share id and size of specified data set.
@@ -564,8 +536,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
     public void updateShareIdAndSize(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode,
-            String shareId, long size) throws UserFailureException;
+            String dataSetCode, String shareId, long size) throws UserFailureException;
 
     /**
      * Updates status of given data sets.
@@ -573,12 +544,10 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public void updateDataSetStatuses(
-            String sessionToken,
+    public void updateDataSetStatuses(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> dataSetCodes,
-            final DataSetArchivingStatus newStatus, boolean presentInArchive)
-            throws UserFailureException;
+            List<String> dataSetCodes, final DataSetArchivingStatus newStatus,
+            boolean presentInArchive) throws UserFailureException;
 
     /**
      * Set the status for a given dataset to the given new status value if the current status equals
@@ -604,11 +573,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public int archiveDatasets(
-            String sessionToken,
+    public int archiveDatasets(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> datasetCodes,
-            boolean removeFromDataStore);
+            List<String> datasetCodes, boolean removeFromDataStore);
 
     /**
      * Schedules unarchiving of specified data sets.
@@ -618,8 +585,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.DATA_SET)
-    public int unarchiveDatasets(
-            String sessionToken,
+    public int unarchiveDatasets(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
             List<String> datasetCodes);
 
@@ -642,8 +608,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     public ExternalData tryGetDataSetForServer(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodePredicate.class)
-            String dataSetCode)
-            throws UserFailureException;
+            String dataSetCode) throws UserFailureException;
 
     /**
      * Returns a list of unique codes.
@@ -685,8 +650,8 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { ObjectKind.SAMPLE, ObjectKind.DATA_SET })
     public Sample registerSampleAndDataSet(final String sessionToken,
             @AuthorizationGuard(guardClass = NewSamplePredicate.class)
-            final NewSample newSample,
-            final NewExternalData externalData, String userIdOrNull) throws UserFailureException;
+            final NewSample newSample, final NewExternalData externalData, String userIdOrNull)
+            throws UserFailureException;
 
     /**
      * Updates a sample and registers a data set connected to that sample in one transaction.
@@ -703,11 +668,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value = ObjectKind.SAMPLE)
     @DatabaseCreateOrDeleteModification(value = ObjectKind.DATA_SET)
-    public Sample updateSampleAndRegisterDataSet(
-            String sessionToken,
+    public Sample updateSampleAndRegisterDataSet(String sessionToken,
             @AuthorizationGuard(guardClass = SampleUpdatesPredicate.class)
-            SampleUpdatesDTO updates,
-            NewExternalData externalData);
+            SampleUpdatesDTO updates, NewExternalData externalData);
 
     /**
      * Updates a sample and registers a data set connected to that sample in one transaction.
@@ -724,8 +687,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @DatabaseCreateOrDeleteModification(value =
         { ObjectKind.SPACE, ObjectKind.PROJECT, ObjectKind.SAMPLE, ObjectKind.EXPERIMENT,
                 ObjectKind.DATA_SET })
-    public AtomicEntityOperationResult performEntityOperations(
-            String sessionToken,
+    public AtomicEntityOperationResult performEntityOperations(String sessionToken,
             @AuthorizationGuard(guardClass = AtomicOperationsPredicate.class)
             AtomicEntityOperationDetails operationDetails);
 
@@ -735,8 +697,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(value =
         { RoleWithHierarchy.SPACE_ETL_SERVER })
-    public Space tryGetSpace(
-            String sessionToken,
+    public Space tryGetSpace(String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class)
             SpaceIdentifier spaceIdentifier);
 
@@ -746,8 +707,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional(readOnly = true)
     @RolesAllowed(value =
         { RoleWithHierarchy.SPACE_ETL_SERVER })
-    public Project tryGetProject(
-            String sessionToken,
+    public Project tryGetProject(String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class)
             ProjectIdentifier projectIdentifier);
 
@@ -782,11 +742,9 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
         { ObjectKind.SAMPLE, ObjectKind.EXPERIMENT })
     @DatabaseCreateOrDeleteModification(value =
         { ObjectKind.DATA_SET })
-    public void removeDataSetsPermanently(
-            String sessionToken,
+    public void removeDataSetsPermanently(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class)
-            List<String> dataSetCodes,
-            String reason);
+            List<String> dataSetCodes, String reason);
 
     /**
      * updates a data set.
@@ -795,8 +753,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     @DatabaseUpdateModification(value =
         { ObjectKind.EXPERIMENT, ObjectKind.SAMPLE, ObjectKind.DATA_SET })
-    public void updateDataSet(
-            String sessionToken,
+    public void updateDataSet(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetUpdatesPredicate.class)
             DataSetUpdatesDTO dataSetUpdates);
 
@@ -837,4 +794,12 @@ public interface IETLLIMSService extends IServer, ISessionProvider, Conversation
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     public List<ExternalData> listDataSetsForPostRegistration(String token, String dataStoreCode);
+
+    /**
+     * Return true if the log indicates that the performEntityOperations invocation for the given
+     * registrationId succeeded.
+     */
+    @Transactional(readOnly = true)
+    @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
+    public Boolean didEntityOperationsSucceed(String token, TechId registrationId);
 }
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java
index 881f36d23e1..e65ac36460e 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java
@@ -85,12 +85,14 @@ public class AtomicEntityOperationDetailsTest extends AssertJUnit
         dataSetUpdates.add(dataSetUpdate);
 
         AtomicEntityOperationDetails details =
-                new AtomicEntityOperationDetails(null, spaceRegistrations, projectRegistrations,
-                        experimentRegistrations, sampleUpdates, sampleRegistrations,
-                        materialRegistrations, dataSetRegistrations, dataSetUpdates);
+                new AtomicEntityOperationDetails(null, null, spaceRegistrations,
+                        projectRegistrations, experimentRegistrations, sampleUpdates,
+                        sampleRegistrations, materialRegistrations, dataSetRegistrations,
+                        dataSetUpdates);
 
         assertEquals(
-                "AtomicEntityOperationDetails[userIdOrNull=<null>"
+                "AtomicEntityOperationDetails[registrationIdOrNull=<null>"
+                        + ",userIdOrNull=<null>"
                         + ",spaceRegistrations=[SPACE1, SPACE2]"
                         + ",projectRegistrations=[/SPACE/P1, /SPACE/P2]"
                         + ",experimentUpdates=[]"
diff --git a/openbis/sourceTest/sql/postgresql/105/finish-105.sql b/openbis/sourceTest/sql/postgresql/105/finish-105.sql
index 7ac24414201..ed0c554c585 100644
--- a/openbis/sourceTest/sql/postgresql/105/finish-105.sql
+++ b/openbis/sourceTest/sql/postgresql/105/finish-105.sql
@@ -64,6 +64,8 @@ ALTER TABLE ONLY data_set_types
     ADD CONSTRAINT dsty_bk_uk UNIQUE (code, dbin_id);
 ALTER TABLE ONLY data_set_types
     ADD CONSTRAINT dsty_pk PRIMARY KEY (id);
+ALTER TABLE ONLY entity_operations_log 
+	ADD CONSTRAINT eol_pk PRIMARY KEY (id);
 ALTER TABLE ONLY experiment_type_property_types
     ADD CONSTRAINT etpt_bk_uk UNIQUE (exty_id, prty_id);
 ALTER TABLE ONLY experiment_type_property_types
@@ -219,6 +221,7 @@ CREATE INDEX dsse_ds_fk_i ON data_store_services USING btree (data_store_id);
 CREATE INDEX dstpt_dsty_fk_i ON data_set_type_property_types USING btree (dsty_id);
 CREATE INDEX dstpt_pers_fk_i ON data_set_type_property_types USING btree (pers_id_registerer);
 CREATE INDEX dstpt_prty_fk_i ON data_set_type_property_types USING btree (prty_id);
+CREATE INDEX entity_operations_log_rid_i ON entity_operations_log USING btree (registration_id);
 CREATE INDEX etpt_exty_fk_i ON experiment_type_property_types USING btree (exty_id);
 CREATE INDEX etpt_pers_fk_i ON experiment_type_property_types USING btree (pers_id_registerer);
 CREATE INDEX etpt_prty_fk_i ON experiment_type_property_types USING btree (prty_id);
diff --git a/openbis/sourceTest/sql/postgresql/105/schema-105.sql b/openbis/sourceTest/sql/postgresql/105/schema-105.sql
index 1eeb638af13..81a91f14ed0 100644
--- a/openbis/sourceTest/sql/postgresql/105/schema-105.sql
+++ b/openbis/sourceTest/sql/postgresql/105/schema-105.sql
@@ -745,6 +745,17 @@ CREATE SEQUENCE dstpt_id_seq
     NO MAXVALUE
     CACHE 1;
 SELECT pg_catalog.setval('dstpt_id_seq', 4, true);
+CREATE TABLE entity_operations_log (
+    id tech_id NOT NULL,
+    registration_id tech_id NOT NULL
+);
+CREATE SEQUENCE entity_operations_log_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+SELECT pg_catalog.setval('entity_operations_log_id_seq', 1, false);
 CREATE SEQUENCE etpt_id_seq
     START WITH 1
     INCREMENT BY 1
-- 
GitLab