From d5dd36f773ca331080af609baba54c0a95efd22a Mon Sep 17 00:00:00 2001
From: cramakri <cramakri>
Date: Tue, 8 Feb 2011 10:27:58 +0000
Subject: [PATCH] LMS-2033 Implemented improved transaction facility.

SVN: 19807
---
 .../cisd/etlserver/BaseDirectoryHolder.java   |  14 +-
 .../systemsx/cisd/etlserver/FileRenamer.java  |  19 +-
 .../etlserver/TransferredDataSetHandler.java  |   2 +-
 ...tOmniscientTopLevelDataSetRegistrator.java |  14 +
 .../DataSetRegistrationService.java           | 133 ++--
 .../registrator/DataSetStorageAlgorithm.java  | 603 ++++++++++++++++++
 .../DataSetStorageAlgorithmRunner.java        | 288 +++++++++
 .../DefaultEntityRegistrationService.java     |  79 +++
 .../IEntityRegistrationService.java           |  32 +
 .../JythonTopLevelDataSetHandler.java         |  15 +-
 .../api/v1/impl/AbstractTransactionState.java | 376 +++++++++++
 .../impl/DataSetRegistrationTransaction.java  | 382 +++--------
 .../dto/AtomicEntityRegistrationDetails.java  |  86 +++
 .../dto/AtomicEntityRegistrationResult.java   |  79 +++
 .../dto/DataSetRegistrationInformation.java   |  42 ++
 .../JythonTopLevelDataSetRegistratorTest.java |   2 +-
 .../DataSetRegistrationTransactionTest.java   |  20 +-
 17 files changed, 1759 insertions(+), 427 deletions(-)
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithm.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithmRunner.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DefaultEntityRegistrationService.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/IEntityRegistrationService.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/AbstractTransactionState.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationDetails.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationResult.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/DataSetRegistrationInformation.java

diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java
index fbe4029b62d..517bcf849a4 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/BaseDirectoryHolder.java
@@ -19,12 +19,12 @@ package ch.systemsx.cisd.etlserver;
 import java.io.File;
 
 /**
- * A tiny class which holds the <i>base directory</i> and ensures that the <i>target file</i>
- * lazily gets computed only once.
+ * A tiny class which holds the <i>base directory</i> and ensures that the <i>target file</i> lazily
+ * gets computed only once.
  * 
  * @author Christian Ribeaud
  */
-final class BaseDirectoryHolder
+public final class BaseDirectoryHolder
 {
 
     private final File baseDirectory;
@@ -35,8 +35,8 @@ final class BaseDirectoryHolder
 
     private File targetFile;
 
-    BaseDirectoryHolder(final IDataStoreStrategy dataStoreStrategy, final File baseDirectory,
-            final File incomingDataSetPath)
+    public BaseDirectoryHolder(final IDataStoreStrategy dataStoreStrategy,
+            final File baseDirectory, final File incomingDataSetPath)
     {
         assert dataStoreStrategy != null : "Data store strategy can not be null.";
         assert baseDirectory != null : "Base directory can not be null";
@@ -51,12 +51,12 @@ final class BaseDirectoryHolder
         return dataStoreStrategy.getTargetPath(baseDirectory, incomingDataSetPath);
     }
 
-    final File getBaseDirectory()
+    public final File getBaseDirectory()
     {
         return baseDirectory;
     }
 
-    final synchronized File getTargetFile()
+    public final synchronized File getTargetFile()
     {
         if (targetFile == null)
         {
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java
index f6a435d4236..598b60a68c7 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/FileRenamer.java
@@ -32,22 +32,21 @@ import ch.systemsx.cisd.common.logging.LogFactory;
  * 
  * @author Franz-Josef Elmer
  */
-final class FileRenamer
+public final class FileRenamer
 {
-    private final static Logger notificationLog =
-            LogFactory.getLogger(LogCategory.NOTIFY, FileRenamer.class);
+    private final static Logger notificationLog = LogFactory.getLogger(LogCategory.NOTIFY,
+            FileRenamer.class);
 
-    final static Logger operationLog =
-            LogFactory.getLogger(LogCategory.OPERATION, FileRenamer.class);
+    final static Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            FileRenamer.class);
 
     /**
      * Renames given <var>sourceFile</var> to given <var>destinationFile</var>.
      * <p>
-     * Internally uses {@link FileOperations} and notifies the administrator if the process
-     * failed.
+     * Internally uses {@link FileOperations} and notifies the administrator if the process failed.
      * </p>
      */
-    static final boolean renameAndLog(final File sourceFile, final File destinationFile)
+    public static final boolean renameAndLog(final File sourceFile, final File destinationFile)
     {
         final String absoluteTargetPath = destinationFile.getAbsolutePath();
         if (destinationFile.exists())
@@ -58,8 +57,8 @@ final class FileRenamer
             return false;
         }
         boolean renamedOK =
-            FileOperations.getMonitoredInstanceForCurrentThread().rename(sourceFile,
-                    destinationFile);
+                FileOperations.getMonitoredInstanceForCurrentThread().rename(sourceFile,
+                        destinationFile);
         if (renamedOK)
         {
             if (operationLog.isInfoEnabled())
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java
index 6f8ca7e277c..6f0dacee878 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/TransferredDataSetHandler.java
@@ -53,7 +53,7 @@ public final class TransferredDataSetHandler extends AbstractTopLevelDataSetRegi
         IDataSetHandler, IExtensibleDataSetHandler
 {
 
-    static final String TARGET_NOT_RELATIVE_TO_STORE_ROOT =
+    public static final String TARGET_NOT_RELATIVE_TO_STORE_ROOT =
             "Target path '%s' is not relative to store root directory '%s'.";
 
     static final Logger notificationLog = LogFactory.getLogger(LogCategory.NOTIFY,
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/AbstractOmniscientTopLevelDataSetRegistrator.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/AbstractOmniscientTopLevelDataSetRegistrator.java
index e64e5b7fed1..e43308aa606 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/AbstractOmniscientTopLevelDataSetRegistrator.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/AbstractOmniscientTopLevelDataSetRegistrator.java
@@ -283,6 +283,20 @@ public abstract class AbstractOmniscientTopLevelDataSetRegistrator extends
                 throwable).doRollback();
     }
 
+    /**
+     * Rollback a failure when trying to commit a transaction.
+     * <p>
+     * Subclasses may override, but should call super.
+     */
+
+    public <T extends DataSetInformation> void rollbackTransaction(
+            DataSetRegistrationService dataSetRegistrationService,
+            DataSetRegistrationTransaction<T> transaction,
+            DataSetStorageAlgorithmRunner<T> algorithm, Throwable ex)
+    {
+        updateStopped(ex instanceof InterruptedExceptionUnchecked);
+    }
+
     /**
      * Rollback a failure that occurs outside of any *particular* data set registration, but with
      * the whole processing of the incoming folder itself.
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetRegistrationService.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetRegistrationService.java
index d72695030c0..3e583a04c8f 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetRegistrationService.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetRegistrationService.java
@@ -53,9 +53,6 @@ public class DataSetRegistrationService implements IRollbackDelegate
     private final ArrayList<DataSetRegistrationAlgorithm> dataSetRegistrations =
             new ArrayList<DataSetRegistrationAlgorithm>();
 
-    // Any parent services
-    private final DataSetRegistrationService parentServiceOrNull;
-
     /**
      * The currently live child transaction.
      */
@@ -93,21 +90,6 @@ public class DataSetRegistrationService implements IRollbackDelegate
         this.registrator = registrator;
         this.registratorState = registrator.getRegistratorState();
         this.globalCleanAfterwardsAction = globalCleanAfterwardsAction;
-        this.parentServiceOrNull = null;
-    }
-
-    /**
-     * A copy constuctor. Used in the creation of transactions. Subclasses will want to override
-     * this.
-     * 
-     * @param other
-     */
-    public DataSetRegistrationService(DataSetRegistrationService other)
-    {
-        this.registrator = other.registrator;
-        this.registratorState = registrator.getRegistratorState();
-        this.globalCleanAfterwardsAction = new NoOpCleanAfterwardsAction();
-        this.parentServiceOrNull = other;
     }
 
     public OmniscientTopLevelDataSetRegistratorState getRegistratorState()
@@ -130,6 +112,9 @@ public class DataSetRegistrationService implements IRollbackDelegate
         return future;
     }
 
+    /**
+     * Create a new transaction that atomically performs file operations and registers entities.
+     */
     public IDataSetRegistrationTransaction transaction(File dataSetFile,
             IDataSetRegistrationDetailsFactory<DataSetInformation> detailsFactory)
     {
@@ -145,27 +130,18 @@ public class DataSetRegistrationService implements IRollbackDelegate
         // Clone this service for the transaction to keep them independent
         liveTransactionOrNull =
                 new DataSetRegistrationTransaction<DataSetInformation>(registrator.getGlobalState()
-                        .getStoreRootDir(), workingDirectory, stagingDirectory,
-                        this.createSubService(), detailsFactory);
+                        .getStoreRootDir(), workingDirectory, stagingDirectory, this,
+                        detailsFactory);
 
         return liveTransactionOrNull;
     }
 
-    // public <T extends DataSetInformation> DataSetStorageAlgorithm<T> createStorageAlgorithm(
-    // File dataSetFile, DataSetRegistrationDetails<T> dataSetDetails)
-    // {
-    // IDataStoreStrategy strategy =
-    // registratorState.getDataStrategyStore().getDataStoreStrategy(
-    // dataSetDetails.getDataSetInformation(), dataSetFile);
-    // DataSetStorageAlgorithm<T> algorithm =
-    // new DataSetStorageAlgorithm<T>(dataSetFile, dataSetDetails, strategy,
-    // registratorState.getStorageProcessor(), null, null, null, null);
-    // return algorithm;
-    // }
-
+    /**
+     * Commit any scheduled changes.
+     */
     public void commit()
     {
-        // If a transaction is hanging around, commit it before starting a new one
+        // If a transaction is hanging around, commit it
         commitExtantTransaction();
 
         for (DataSetRegistrationAlgorithm registrationAlgorithm : dataSetRegistrations)
@@ -175,30 +151,50 @@ public class DataSetRegistrationService implements IRollbackDelegate
         globalCleanAfterwardsAction.execute();
     }
 
+    /**
+     * Abort any scheduled changes.
+     */
     public void abort()
     {
+        rollbackExtantTransaction();
         dataSetRegistrations.clear();
+    }
 
-        if (null != liveTransactionOrNull)
-        {
-            liveTransactionOrNull.rollback();
-        }
-        if (null != parentServiceOrNull)
-        {
-            if (null != parentServiceOrNull.liveTransactionOrNull)
-            {
-                parentServiceOrNull.liveTransactionOrNull.rollback();
-            }
-        }
+    public void rollback(DataSetRegistrationAlgorithm algorithm, Throwable ex)
+    {
+        registrator.rollback(this, algorithm, ex);
+    }
+
+    public <T extends DataSetInformation> void rollbackTransaction(
+            DataSetRegistrationTransaction<T> transaction,
+            DataSetStorageAlgorithmRunner<T> algorithm, Throwable ex)
+    {
+        registrator.rollbackTransaction(this, transaction, algorithm, ex);
     }
 
     /**
-     * Create a service derived from this one. By default, use the copy constructor. Subclasses
-     * should override.
+     * Create a storage algorithm for storing an individual data set. This is internally used by
+     * transactions. Other clients may find it useful as well.
      */
-    protected DataSetRegistrationService createSubService()
+    public <T extends DataSetInformation> DataSetStorageAlgorithm<T> createStorageAlgorithm(
+            File dataSetFile, DataSetRegistrationDetails<T> dataSetDetails)
+    {
+        IDataStoreStrategy strategy =
+                registratorState.getDataStrategyStore().getDataStoreStrategy(
+                        dataSetDetails.getDataSetInformation(), dataSetFile);
+
+        TopLevelDataSetRegistratorGlobalState globalContext = registratorState.getGlobalState();
+        DataSetStorageAlgorithm<T> algorithm =
+                new DataSetStorageAlgorithm<T>(dataSetFile, dataSetDetails, strategy,
+                        registratorState.getStorageProcessor(),
+                        globalContext.getDataSetValidator(), globalContext.getDssCode(),
+                        registratorState.getFileOperations(), globalContext.getMailClient());
+        return algorithm;
+    }
+
+    public <T extends DataSetInformation> IEntityRegistrationService<T> getEntityRegistrationService()
     {
-        return new DataSetRegistrationService(this);
+        return new DefaultEntityRegistrationService<T>(registrator);
     }
 
     private DataSetRegistrationAlgorithm createRegistrationAlgorithm(File incomingDataSetFile,
@@ -234,12 +230,28 @@ public class DataSetRegistrationService implements IRollbackDelegate
                         details.getDataSetInformation()));
     }
 
-    public void rollback(DataSetRegistrationAlgorithm algorithm, Throwable ex)
+    /**
+     * If a transaction is hanging around, commit it
+     */
+    private void commitExtantTransaction()
     {
-        registrator.rollback(this, algorithm, ex);
+        if (null != liveTransactionOrNull
+                && false == liveTransactionOrNull.isCommittedOrRolledback())
+        {
+            // Commit the existing transaction
+            liveTransactionOrNull.commit();
+        }
+    }
+
+    private void rollbackExtantTransaction()
+    {
+        if (null != liveTransactionOrNull)
+        {
+            liveTransactionOrNull.rollback();
+        }
     }
 
-    protected static class DefaultApplicationServerRegistrator implements
+    private static class DefaultApplicationServerRegistrator implements
             IDataSetInApplicationServerRegistrator
     {
         private final AbstractOmniscientTopLevelDataSetRegistrator registrator;
@@ -260,23 +272,4 @@ public class DataSetRegistrationService implements IRollbackDelegate
         }
     }
 
-    protected static class NoOpCleanAfterwardsAction implements IDelegatedActionWithResult<Boolean>
-    {
-        public Boolean execute()
-        {
-            return true; // do nothing
-        }
-    }
-
-    /**
-     * If a transaction is hanging around, commit it
-     */
-    private void commitExtantTransaction()
-    {
-        if (null != liveTransactionOrNull)
-        {
-            // Commit the existing transaction
-            liveTransactionOrNull.commit();
-        }
-    }
 }
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithm.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithm.java
new file mode 100644
index 00000000000..bcb1f74068b
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithm.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.etlserver.registrator;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.StopWatch;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked;
+import ch.systemsx.cisd.common.Constants;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.filesystem.IFileOperations;
+import ch.systemsx.cisd.common.logging.Log4jSimpleLogger;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.mail.IMailClient;
+import ch.systemsx.cisd.common.types.BooleanOrUnknown;
+import ch.systemsx.cisd.etlserver.BaseDirectoryHolder;
+import ch.systemsx.cisd.etlserver.DataStoreStrategyKey;
+import ch.systemsx.cisd.etlserver.FileRenamer;
+import ch.systemsx.cisd.etlserver.IDataStoreStrategy;
+import ch.systemsx.cisd.etlserver.IStorageProcessor;
+import ch.systemsx.cisd.etlserver.IStorageProcessor.UnstoreDataAction;
+import ch.systemsx.cisd.etlserver.TransferredDataSetHandler;
+import ch.systemsx.cisd.etlserver.validation.IDataSetValidator;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType;
+import ch.systemsx.cisd.openbis.generic.shared.dto.NewExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.StorageFormat;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
+
+/**
+ * DataSetStorageAlgorithm is a state machine that executes steps to store a data set in the store
+ * and transitions between states.
+ * <p>
+ * The states and transitions are as follows: <br>
+ * 
+ * <pre>
+ *                                             /-> (Committed*)
+ * (Initialized) -> (Prepared) -> (Stored) -<
+ *                     \                       \-> (Rolledback) -> (UndoneState*)
+ *                      \ -------------------------------------/
+ * </pre>
+ * 
+ * States marked with a "*" are terminal.
+ * <p>
+ * N.b. Methods invoked on states for which the method is not valid will yield a class cast
+ * exception since the state object will not be castable to the desired class.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class DataSetStorageAlgorithm<T extends DataSetInformation>
+{
+    static private final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            DataSetStorageAlgorithm.class);
+
+    private final File incomingDataSetFile;
+
+    private final DataSetRegistrationDetails<T> registrationDetails;
+
+    private final T dataSetInformation;
+
+    private final IDataStoreStrategy dataStoreStrategy;
+
+    private final IStorageProcessor storageProcessor;
+
+    private final String dataStoreCode;
+
+    private final DataSetType dataSetType;
+
+    private final File storeRoot;
+
+    private final IFileOperations fileOperations;
+
+    // Used by the storage processor
+    private final IMailClient mailClient;
+
+    // State that changes during execution
+    private DataSetStorageAlgorithmState<T> state;
+
+    public DataSetStorageAlgorithm(File incomingDataSetFile,
+            DataSetRegistrationDetails<T> registrationDetails,
+            IDataStoreStrategy dataStoreStrategy, IStorageProcessor storageProcessor,
+            IDataSetValidator dataSetValidator, String dataStoreCode,
+            IFileOperations fileOperations, IMailClient mailClient)
+    {
+        this.incomingDataSetFile = incomingDataSetFile;
+        this.registrationDetails = registrationDetails;
+        this.dataSetInformation = registrationDetails.getDataSetInformation();
+        this.dataStoreStrategy = dataStoreStrategy;
+        this.storageProcessor = storageProcessor;
+        this.dataStoreCode = dataStoreCode;
+        this.fileOperations = fileOperations;
+        this.mailClient = mailClient;
+
+        this.storeRoot = storageProcessor.getStoreRootDirectory();
+        this.dataSetType = registrationDetails.getDataSetType();
+
+        if (dataStoreStrategy.getKey() != DataStoreStrategyKey.IDENTIFIED)
+        {
+            throw new UserFailureException("Data set must be associated with an experiment ");
+        }
+
+        dataSetValidator.assertValidDataSet(dataSetType, incomingDataSetFile);
+
+        state = new InitializedState<T>(this);
+    }
+
+    /**
+     * Prepare registration of a data set.
+     */
+    public void prepare()
+    {
+        InitializedState<T> initializedState = (InitializedState<T>) state;
+        initializedState.prepare();
+
+        state = new PreparedState<T>(initializedState);
+    }
+
+    /**
+     * Run the storage processor.
+     */
+    public void runStorageProcessor() throws Throwable
+    {
+        PreparedState<T> preparedState = (PreparedState<T>) state;
+        preparedState.storeData();
+
+        state = new StoredState<T>(preparedState);
+    }
+
+    /**
+     * Ask the storage processor to rollback. Used by clients of the algorithm.
+     */
+    public void rollbackStorageProcessor(Throwable throwable)
+    {
+        // Rollback may be called on in the stored state or in the prepared state. In the prepared
+        // state, there is nothing to do.
+        if (state instanceof PreparedState)
+        {
+            return;
+        }
+
+        StoredState<T> storedState = (StoredState<T>) state;
+        UnstoreDataAction action = storedState.rollbackStorageProcessor(throwable);
+
+        state = new RolledbackState<T>(storedState, action, throwable);
+    }
+
+    public void executeUndoStoreAction()
+    {
+        // Rollback may be called on in the stored state or in the prepared state. In the prepared
+        // state, there is nothing to do.
+        if (state instanceof PreparedState)
+        {
+            state = new UndoneState<T>((PreparedState<T>) state);
+            return;
+        }
+
+        RolledbackState<T> rolledbackState = (RolledbackState<T>) state;
+        rolledbackState.executeUndoAction();
+
+        state = new UndoneState<T>(rolledbackState);
+    }
+
+    /**
+     * Ask the storage processor to commit. Used by clients of the algorithm.
+     */
+    public void commitStorageProcessor()
+    {
+        StoredState<T> storedState = (StoredState<T>) state;
+        storedState.commitStorageProcessor();
+
+        state = new CommittedState<T>(storedState);
+    }
+
+    public File getIncomingDataSetFile()
+    {
+        return incomingDataSetFile;
+    }
+
+    public T getDataSetInformation()
+    {
+        return dataSetInformation;
+    }
+
+    protected Logger getOperationLog()
+    {
+        return operationLog;
+    }
+
+    public NewExternalData createExternalData()
+    {
+        final NewExternalData data = new NewExternalData();
+        data.setUserId(dataSetInformation.getUploadingUserIdOrNull());
+        data.setUserEMail(dataSetInformation.tryGetUploadingUserEmail());
+        data.setExtractableData(dataSetInformation.getExtractableData());
+        data.setLocatorType(registrationDetails.getLocatorType());
+        data.setDataSetType(registrationDetails.getDataSetType());
+        data.setFileFormatType(registrationDetails.getFileFormatType());
+        data.setMeasured(registrationDetails.isMeasuredData());
+        data.setDataStoreCode(dataStoreCode);
+
+        File dataFile = ((StoredState<T>) state).getDataFile();
+
+        final String relativePath = FileUtilities.getRelativeFile(storeRoot, dataFile);
+        String absolutePath = dataFile.getAbsolutePath();
+        assert relativePath != null : String.format(
+                TransferredDataSetHandler.TARGET_NOT_RELATIVE_TO_STORE_ROOT, absolutePath,
+                storeRoot.getAbsolutePath());
+        final StorageFormat storageFormat = storageProcessor.getStorageFormat();
+        final BooleanOrUnknown isCompleteFlag = dataSetInformation.getIsCompleteFlag();
+
+        data.setComplete(isCompleteFlag);
+        data.setLocation(relativePath);
+        data.setStorageFormat(storageFormat);
+
+        return data;
+    }
+
+    public String getSuccessRegistrationMessage()
+    {
+        final StringBuilder buffer = new StringBuilder();
+        String userID = dataSetInformation.getUploadingUserIdOrNull();
+        String userEMail = dataSetInformation.tryGetUploadingUserEmail();
+        if (userID != null || userEMail != null)
+        {
+            appendNameAndObject(buffer, "User", userID == null ? userEMail : userID);
+        }
+        appendNameAndObject(buffer, "Data Set Code", dataSetInformation.getDataSetCode());
+        appendNameAndObject(buffer, "Data Set Type", dataSetType.getCode());
+        appendNameAndObject(buffer, "Experiment Identifier",
+                dataSetInformation.getExperimentIdentifier());
+        appendNameAndObject(buffer, "Sample Identifier", dataSetInformation.getSampleIdentifier());
+        appendNameAndObject(buffer, "Producer Code", dataSetInformation.getProducerCode());
+        appendNameAndObject(buffer, "Production Date",
+                formatDate(dataSetInformation.getProductionDate()));
+        final List<String> parentDataSetCodes = dataSetInformation.getParentDataSetCodes();
+        if (parentDataSetCodes.isEmpty() == false)
+        {
+            appendNameAndObject(buffer, "Parent Data Sets",
+                    StringUtils.join(parentDataSetCodes, ' '));
+        }
+        appendNameAndObject(buffer, "Is complete", dataSetInformation.getIsCompleteFlag());
+        buffer.setLength(buffer.length() - 1);
+        return buffer.toString();
+    }
+
+    public String getFailureRegistrationMessage()
+    {
+        return "Error when trying to register data set '" + incomingDataSetFile.getName() + "'.";
+    }
+
+    private String formatDate(Date productionDate)
+    {
+        return productionDate == null ? "" : Constants.DATE_FORMAT.get().format(productionDate);
+    }
+
+    private final void appendNameAndObject(final StringBuilder buffer, final String name,
+            final Object object)
+    {
+        if (object != null)
+        {
+            buffer.append(name).append("::").append(object).append(";");
+        }
+    }
+
+    protected DataSetType getDataSetType()
+    {
+        return dataSetType;
+    }
+
+    protected IDataStoreStrategy getDataStoreStrategy()
+    {
+        return dataStoreStrategy;
+    }
+
+    protected File getStoreRoot()
+    {
+        return storeRoot;
+    }
+
+    protected IStorageProcessor getStorageProcessor()
+    {
+        return storageProcessor;
+    }
+
+    protected DataSetRegistrationDetails<T> getRegistrationDetails()
+    {
+        return registrationDetails;
+    }
+
+    private static abstract class DataSetStorageAlgorithmState<T extends DataSetInformation>
+    {
+        protected final DataSetStorageAlgorithm<T> storageAlgorithm;
+
+        protected final File incomingDataSetFile;
+
+        protected DataSetStorageAlgorithmState(DataSetStorageAlgorithm<T> storageAlgorithm)
+        {
+            this.storageAlgorithm = storageAlgorithm;
+            this.incomingDataSetFile = storageAlgorithm.getIncomingDataSetFile();
+        }
+
+        protected Logger getOperationLog()
+        {
+            return storageAlgorithm.getOperationLog();
+        }
+
+        protected IFileOperations getFileOperations()
+        {
+            return storageAlgorithm.fileOperations;
+        }
+
+        protected final File createBaseDirectory(final IDataStoreStrategy strategy,
+                final File baseDir, final DataSetInformation dataSetInfo)
+        {
+            final File baseDirectory =
+                    strategy.getBaseDirectory(baseDir, dataSetInfo,
+                            storageAlgorithm.getDataSetType());
+            baseDirectory.mkdirs();
+            if (getFileOperations().isDirectory(baseDirectory) == false)
+            {
+                throw EnvironmentFailureException.fromTemplate(
+                        "Creating data set base directory '%s' for data set '%s' failed.",
+                        baseDirectory.getAbsolutePath(), incomingDataSetFile);
+            }
+            return baseDirectory;
+        }
+    }
+
+    private static class InitializedState<T extends DataSetInformation> extends
+            DataSetStorageAlgorithmState<T>
+    {
+        protected BaseDirectoryHolder baseDirectoryHolder;
+
+        public InitializedState(DataSetStorageAlgorithm<T> storageAlgorithm)
+        {
+            super(storageAlgorithm);
+        }
+
+        /**
+         * Prepare registration of a data set.
+         */
+        public void prepare()
+        {
+            IDataStoreStrategy dataStoreStrategy = storageAlgorithm.getDataStoreStrategy();
+            final File baseDirectory =
+                    createBaseDirectory(dataStoreStrategy, storageAlgorithm.getStoreRoot(),
+                            storageAlgorithm.getDataSetInformation());
+            baseDirectoryHolder =
+                    new BaseDirectoryHolder(dataStoreStrategy, baseDirectory, incomingDataSetFile);
+        }
+    }
+
+    private static class PreparedState<T extends DataSetInformation> extends
+            DataSetStorageAlgorithmState<T>
+    {
+        protected final BaseDirectoryHolder baseDirectoryHolder;
+
+        protected final IStorageProcessor storageProcessor;
+
+        protected final DataSetInformation dataSetInformation;
+
+        protected File markerFile;
+
+        protected File dataFile;
+
+        public PreparedState(InitializedState<T> oldState)
+        {
+            super(oldState.storageAlgorithm);
+            this.storageProcessor = storageAlgorithm.getStorageProcessor();
+            this.baseDirectoryHolder = oldState.baseDirectoryHolder;
+            this.dataSetInformation = storageAlgorithm.getDataSetInformation();
+        }
+
+        public void storeData()
+        {
+            markerFile = createProcessingMarkerFile();
+            String entityDescription = createEntityDescription();
+            if (getOperationLog().isInfoEnabled())
+            {
+                getOperationLog().info("Start storing data set for " + entityDescription + ".");
+            }
+            final StopWatch watch = new StopWatch();
+            watch.start();
+
+            dataFile =
+                    storageProcessor.storeData(storageAlgorithm.getDataSetInformation(),
+                            storageAlgorithm.getRegistrationDetails(), getMailClient(),
+                            incomingDataSetFile, baseDirectoryHolder.getBaseDirectory());
+            if (getOperationLog().isInfoEnabled())
+            {
+                getOperationLog().info(
+                        "Finished storing data set for " + entityDescription + ", took " + watch);
+            }
+            assert dataFile != null : "The folder that contains the stored data should not be null.";
+        }
+
+        private IMailClient getMailClient()
+        {
+            return storageAlgorithm.mailClient;
+        }
+
+        private final File createProcessingMarkerFile()
+        {
+            final File baseDirectory = baseDirectoryHolder.getBaseDirectory();
+            final File baseParentDirectory = baseDirectory.getParentFile();
+            final String processingDirName = baseDirectory.getName();
+            markerFile =
+                    new File(baseParentDirectory, Constants.PROCESSING_PREFIX + processingDirName);
+            try
+            {
+                getFileOperations().createNewFile(markerFile);
+            } catch (final IOExceptionUnchecked ex)
+            {
+                throw EnvironmentFailureException.fromTemplate(ex,
+                        "Cannot create marker file '%s'.", markerFile.getPath());
+            }
+            return markerFile;
+        }
+
+        private String createEntityDescription()
+        {
+            SampleIdentifier sampleIdentifier = dataSetInformation.getSampleIdentifier();
+            if (sampleIdentifier != null)
+            {
+                return "sample '" + sampleIdentifier + "'";
+            }
+            return "experiment '" + dataSetInformation.getExperimentIdentifier() + "'";
+        }
+    }
+
+    private static class StoredState<T extends DataSetInformation> extends
+            DataSetStorageAlgorithmState<T>
+    {
+        protected final IStorageProcessor storageProcessor;
+
+        protected final BaseDirectoryHolder baseDirectoryHolder;
+
+        protected final File markerFile;
+
+        protected final File dataFile;
+
+        public StoredState(PreparedState<T> oldState)
+        {
+            super(oldState.storageAlgorithm);
+            this.storageProcessor = storageAlgorithm.getStorageProcessor();
+            this.baseDirectoryHolder = oldState.baseDirectoryHolder;
+
+            this.markerFile = oldState.markerFile;
+            this.dataFile = oldState.dataFile;
+        }
+
+        protected File getDataFile()
+        {
+            return dataFile;
+        }
+
+        /**
+         * Ask the storage processor to rollback. Used by clients of the algorithm.
+         */
+        public UnstoreDataAction rollbackStorageProcessor(final Throwable throwable)
+        {
+            UnstoreDataAction action =
+                    storageProcessor.rollback(incomingDataSetFile,
+                            baseDirectoryHolder.getBaseDirectory(), throwable);
+            cleanUp();
+            return action;
+        }
+
+        /**
+         * Ask the storage processor to commit. Used by clients of the algorithm.
+         */
+        public void commitStorageProcessor()
+        {
+            storageProcessor.commit(incomingDataSetFile, baseDirectoryHolder.getBaseDirectory());
+            cleanUp();
+        }
+
+        /**
+         * Cleanup from the processing -- done after a commit or rollback
+         */
+        private void cleanUp()
+        {
+            getFileOperations().delete(markerFile);
+        }
+    }
+
+    private static class CommittedState<T extends DataSetInformation> extends
+            DataSetStorageAlgorithmState<T>
+    {
+
+        CommittedState(StoredState<T> oldState)
+        {
+            super(oldState.storageAlgorithm);
+        }
+
+    }
+
+    private static class RolledbackState<T extends DataSetInformation> extends
+            DataSetStorageAlgorithmState<T>
+    {
+        private final UnstoreDataAction action;
+
+        private final Throwable throwable;
+
+        private final File storeRoot;
+
+        private BaseDirectoryHolder baseDirectoryHolder;
+
+        public RolledbackState(StoredState<T> oldState, UnstoreDataAction action,
+                Throwable throwable)
+        {
+            super(oldState.storageAlgorithm);
+            this.action = action;
+            this.throwable = throwable;
+
+            this.storeRoot = oldState.storageProcessor.getStoreRootDirectory();
+
+            this.baseDirectoryHolder = oldState.baseDirectoryHolder;
+        }
+
+        public void executeUndoAction()
+        {
+            if (action == UnstoreDataAction.MOVE_TO_ERROR)
+            {
+                final File baseDirectory =
+                        createBaseDirectory(TransferredDataSetHandler.ERROR_DATA_STRATEGY,
+                                storeRoot, storageAlgorithm.getDataSetInformation());
+                baseDirectoryHolder =
+                        new BaseDirectoryHolder(TransferredDataSetHandler.ERROR_DATA_STRATEGY,
+                                baseDirectory, incomingDataSetFile);
+                FileRenamer.renameAndLog(incomingDataSetFile, baseDirectoryHolder.getTargetFile());
+                writeThrowable();
+            } else if (action == UnstoreDataAction.DELETE)
+            {
+                FileUtilities.deleteRecursively(incomingDataSetFile, new Log4jSimpleLogger(
+                        getOperationLog()));
+            }
+        }
+
+        private void writeThrowable()
+        {
+            final String fileName = incomingDataSetFile.getName() + ".exception";
+            final File file =
+                    new File(baseDirectoryHolder.getTargetFile().getParentFile(), fileName);
+            FileWriter writer = null;
+            try
+            {
+                writer = new FileWriter(file);
+                throwable.printStackTrace(new PrintWriter(writer));
+            } catch (final IOException e)
+            {
+                getOperationLog().warn(
+                        String.format("Could not write out the exception '%s' in file '%s'.",
+                                fileName, file.getAbsolutePath()), e);
+            } finally
+            {
+                IOUtils.closeQuietly(writer);
+            }
+        }
+    }
+
+    private static class UndoneState<T extends DataSetInformation> extends
+            DataSetStorageAlgorithmState<T>
+    {
+
+        UndoneState(PreparedState<T> oldState)
+        {
+            super(oldState.storageAlgorithm);
+        }
+
+        UndoneState(RolledbackState<T> oldState)
+        {
+            super(oldState.storageAlgorithm);
+        }
+
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithmRunner.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithmRunner.java
new file mode 100644
index 00000000000..7cb63dba030
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DataSetStorageAlgorithmRunner.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.etlserver.registrator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.base.exceptions.InterruptedExceptionUnchecked;
+import ch.systemsx.cisd.common.exceptions.HighLevelException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetRegistrationInformation;
+
+/**
+ * An algorithm that implements the logic running many data set storage algorithms in one logical
+ * transaction.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class DataSetStorageAlgorithmRunner<T extends DataSetInformation>
+{
+
+    public static interface IRollbackDelegate<T extends DataSetInformation>
+    {
+        public void rollback(DataSetStorageAlgorithmRunner<T> algorithm, Throwable ex);
+    }
+
+    /**
+     * Interface for code that is run to register a new data set.
+     * 
+     * @author Chandrasekhar Ramakrishnan
+     */
+    public static interface IDataSetInApplicationServerRegistrator<T extends DataSetInformation>
+    {
+        public void registerDataSetsInApplicationServer(List<DataSetRegistrationInformation<T>> data)
+                throws Throwable;
+    }
+
+    static private final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            DataSetStorageAlgorithmRunner.class);
+
+    public static final String DATA_SET_REGISTRATION_FAILURE_TEMPLATE =
+            "Registration of data set '%s' failed.";
+
+    public static final String DATA_SET_STORAGE_FAILURE_TEMPLATE = "Storing data set '%s' failed.";
+
+    public static final String SUCCESSFULLY_REGISTERED = "Successfully registered data set: [";
+
+    private final ArrayList<DataSetStorageAlgorithm<T>> dataSetStorageAlgorithms;
+
+    private final IDataSetInApplicationServerRegistrator<T> applicationServerRegistrator;
+
+    private final IRollbackDelegate<T> rollbackDelegate;
+
+    public DataSetStorageAlgorithmRunner(IEncapsulatedOpenBISService openBisService,
+            List<DataSetStorageAlgorithm<T>> dataSetStorageAlgorithms,
+            IRollbackDelegate<T> rollbackDelegate)
+    {
+        this(dataSetStorageAlgorithms, rollbackDelegate,
+                new DefaultApplicationServerRegistrator<T>(openBisService));
+    }
+
+    public DataSetStorageAlgorithmRunner(List<DataSetStorageAlgorithm<T>> dataSetStorageAlgorithms,
+            IRollbackDelegate<T> rollbackDelegate,
+            IDataSetInApplicationServerRegistrator<T> applicationServerRegistrator)
+    {
+        this.dataSetStorageAlgorithms =
+                new ArrayList<DataSetStorageAlgorithm<T>>(dataSetStorageAlgorithms);
+        this.rollbackDelegate = rollbackDelegate;
+        this.applicationServerRegistrator = applicationServerRegistrator;
+    }
+
+    /**
+     * Prepare registration of a data set.
+     */
+    public final void prepare()
+    {
+        for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+        {
+            storageAlgorithm.prepare();
+        }
+    }
+
+    /**
+     * Register the data sets.
+     */
+    public List<DataSetInformation> runStorageAlgorithms()
+    {
+        try
+        {
+            // Runs or throws a throwable
+            runStorageProcessors();
+
+        } catch (final HighLevelException ex)
+        {
+            rollbackDuringStorageProcessorRun(ex);
+            return Collections.emptyList();
+        } catch (final Throwable throwable)
+        {
+            rollbackDuringStorageProcessorRun(throwable);
+            return Collections.emptyList();
+        }
+
+        try
+        {
+            // Runs or throw a throwable
+            registerDataSetsInApplicationServer();
+
+        } catch (final HighLevelException ex)
+        {
+            rollbackDuringMetadataRegistration(ex);
+            return Collections.emptyList();
+        } catch (final Throwable throwable)
+        {
+            rollbackDuringMetadataRegistration(throwable);
+            return Collections.emptyList();
+        }
+
+        try
+        {
+            // Should always succeed
+            commitStorageProcessors();
+
+            logSuccessfulRegistration();
+
+            ArrayList<DataSetInformation> dataSetInformationCollection =
+                    new ArrayList<DataSetInformation>();
+            for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+            {
+                dataSetInformationCollection.add(storageAlgorithm.getDataSetInformation());
+
+            }
+
+            return dataSetInformationCollection;
+        } catch (final Throwable throwable)
+        {
+            // Something has gone really wrong
+            rollbackAfterStorageProcessorAndMetadataRegistration(throwable);
+            return Collections.emptyList();
+        }
+    }
+
+    public List<DataSetInformation> prepareAndRunStorageAlgorithms()
+    {
+        prepare();
+        return runStorageAlgorithms();
+    }
+
+    private void rollbackDuringStorageProcessorRun(Throwable ex)
+    {
+        rollbackStorageProcessors(ex);
+        rollbackDelegate.rollback(this, ex);
+    }
+
+    private void rollbackDuringMetadataRegistration(Throwable ex)
+    {
+        rollbackStorageProcessors(ex);
+        rollbackDelegate.rollback(this, ex);
+    }
+
+    private void rollbackAfterStorageProcessorAndMetadataRegistration(Throwable ex)
+    {
+        rollbackStorageProcessors(ex);
+        rollbackDelegate.rollback(this, ex);
+    }
+
+    private void commitStorageProcessors()
+    {
+        for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+        {
+            storageAlgorithm.commitStorageProcessor();
+
+        }
+    }
+
+    private void registerDataSetsInApplicationServer() throws Throwable
+    {
+        ArrayList<DataSetRegistrationInformation<T>> registrationData =
+                new ArrayList<DataSetRegistrationInformation<T>>();
+        for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+        {
+            registrationData.add(new DataSetRegistrationInformation<T>(storageAlgorithm
+                    .getDataSetInformation(), storageAlgorithm.createExternalData()));
+
+        }
+        applicationServerRegistrator.registerDataSetsInApplicationServer(registrationData);
+    }
+
+    private void runStorageProcessors() throws Throwable
+    {
+        for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+        {
+            storageAlgorithm.runStorageProcessor();
+        }
+    }
+
+    private void rollbackStorageProcessors(Throwable ex)
+    {
+        // Errors which are not AssertionErrors leave the system in a state that we don't
+        // know and can't trust. Thus we will not perform any operations any more in this
+        // case.
+        if (ex instanceof Error && ex instanceof AssertionError == false)
+        {
+            return;
+        }
+
+        // Don't rollback when this exception happens
+        boolean stopped = ex instanceof InterruptedExceptionUnchecked;
+
+        for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+        {
+            storageAlgorithm.rollbackStorageProcessor(ex);
+
+            if (stopped == false)
+            {
+                storageAlgorithm.executeUndoStoreAction();
+            }
+        }
+    }
+
+    private static class DefaultApplicationServerRegistrator<T extends DataSetInformation>
+            implements IDataSetInApplicationServerRegistrator<T>
+    {
+        private final IEncapsulatedOpenBISService openBisService;
+
+        DefaultApplicationServerRegistrator(IEncapsulatedOpenBISService openBisService)
+        {
+            this.openBisService = openBisService;
+        }
+
+        public void registerDataSetsInApplicationServer(List<DataSetRegistrationInformation<T>> data)
+                throws Throwable
+        {
+            for (DataSetRegistrationInformation<T> datum : data)
+            {
+                openBisService.registerDataSet(datum.getDataSetInformation(),
+                        datum.getExternalData());
+            }
+        }
+
+    }
+
+    private void logSuccessfulRegistration()
+    {
+        if (getOperationLog().isInfoEnabled())
+        {
+            String msg = getSuccessRegistrationMessage();
+            getOperationLog().info(msg);
+        }
+    }
+
+    private final String getSuccessRegistrationMessage()
+    {
+        final StringBuilder buffer = new StringBuilder();
+
+        for (DataSetStorageAlgorithm<T> storageAlgorithm : dataSetStorageAlgorithms)
+        {
+            buffer.append(SUCCESSFULLY_REGISTERED);
+            buffer.append(storageAlgorithm.getSuccessRegistrationMessage());
+            buffer.append(']');
+        }
+        return buffer.toString();
+    }
+
+    private Logger getOperationLog()
+    {
+        return operationLog;
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DefaultEntityRegistrationService.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DefaultEntityRegistrationService.java
new file mode 100644
index 00000000000..2e8f3f0f75a
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/DefaultEntityRegistrationService.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.etlserver.registrator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.AtomicEntityRegistrationDetails;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.AtomicEntityRegistrationResult;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetRegistrationInformation;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory;
+
+public class DefaultEntityRegistrationService<T extends DataSetInformation> implements
+        IEntityRegistrationService<T>
+{
+    private final AbstractOmniscientTopLevelDataSetRegistrator registrator;
+
+    public DefaultEntityRegistrationService(AbstractOmniscientTopLevelDataSetRegistrator registrator)
+    {
+        this.registrator = registrator;
+    }
+
+    public AtomicEntityRegistrationResult registerEntitiesInApplcationServer(
+            AtomicEntityRegistrationDetails<T> registrationDetails)
+    {
+        // Arrays to hold return values
+        ArrayList<Experiment> experimentsUpdated = new ArrayList<Experiment>();
+        ArrayList<Experiment> experimentsCreated = new ArrayList<Experiment>();
+        ArrayList<Sample> samplesUpdated = new ArrayList<Sample>();
+        ArrayList<Sample> samplesCreated = new ArrayList<Sample>();
+        ArrayList<DataSetInformation> dataSetsCreated = new ArrayList<DataSetInformation>();
+
+        IEncapsulatedOpenBISService openBisService =
+                registrator.getGlobalState().getOpenBisService();
+        List<NewExperiment> experimentRegistrations =
+                registrationDetails.getExperimentRegistrations();
+        for (NewExperiment experiment : experimentRegistrations)
+        {
+            openBisService.registerExperiment(experiment);
+            ExperimentIdentifier experimentIdentifier =
+                    new ExperimentIdentifierFactory(experiment.getIdentifier()).createIdentifier();
+            experimentsCreated.add(openBisService.tryToGetExperiment(experimentIdentifier));
+        }
+
+        List<DataSetRegistrationInformation<T>> dataSetRegistrations =
+                registrationDetails.getDataSetRegistrations();
+        for (DataSetRegistrationInformation<T> dataSetRegistration : dataSetRegistrations)
+        {
+            openBisService.registerDataSet(dataSetRegistration.getDataSetInformation(),
+                    dataSetRegistration.getExternalData());
+            dataSetsCreated.add(dataSetRegistration.getDataSetInformation());
+        }
+
+        return new AtomicEntityRegistrationResult(experimentsUpdated, experimentsCreated,
+                samplesUpdated, samplesCreated, dataSetsCreated);
+
+    }
+
+}
\ No newline at end of file
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/IEntityRegistrationService.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/IEntityRegistrationService.java
new file mode 100644
index 00000000000..9bdef2e8746
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/IEntityRegistrationService.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.etlserver.registrator;
+
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.AtomicEntityRegistrationDetails;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.AtomicEntityRegistrationResult;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+
+/**
+ * The inteface implemented by a service that can register openBIS entities atomically.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public interface IEntityRegistrationService<T extends DataSetInformation>
+{
+    AtomicEntityRegistrationResult registerEntitiesInApplcationServer(
+            AtomicEntityRegistrationDetails<T> registrationDetails);
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetHandler.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetHandler.java
index b591a59b43a..193a465014e 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetHandler.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetHandler.java
@@ -244,7 +244,7 @@ public class JythonTopLevelDataSetHandler extends AbstractOmniscientTopLevelData
             dataSetInfo.setInstanceCode(registratorState.getHomeDatabaseInstance().getCode());
             dataSetInfo.setInstanceUUID(registratorState.getHomeDatabaseInstance().getUuid());
         }
-        
+
         /**
          * Adaptor for the IDataSetRegistrationDetailsFactory interface.
          */
@@ -282,22 +282,9 @@ public class JythonTopLevelDataSetHandler extends AbstractOmniscientTopLevelData
             this.interpreter = interpreter;
         }
 
-        public JythonDataSetRegistrationService(JythonDataSetRegistrationService other)
-        {
-            super(other);
-            interpreter = other.interpreter;
-        }
-
         public PythonInterpreter getInterpreter()
         {
             return interpreter;
         }
-
-        @Override
-        protected JythonDataSetRegistrationService createSubService()
-        {
-            return new JythonDataSetRegistrationService(this);
-        }
-
     }
 }
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/AbstractTransactionState.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/AbstractTransactionState.java
new file mode 100644
index 00000000000..1b8955b63a7
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/AbstractTransactionState.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.etlserver.registrator.api.v1.impl;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.etlserver.registrator.DataSetRegistrationDetails;
+import ch.systemsx.cisd.etlserver.registrator.DataSetRegistrationService;
+import ch.systemsx.cisd.etlserver.registrator.DataSetStorageAlgorithm;
+import ch.systemsx.cisd.etlserver.registrator.DataSetStorageAlgorithmRunner;
+import ch.systemsx.cisd.etlserver.registrator.IDataSetRegistrationDetailsFactory;
+import ch.systemsx.cisd.etlserver.registrator.api.v1.IDataSet;
+import ch.systemsx.cisd.etlserver.registrator.api.v1.IExperiment;
+import ch.systemsx.cisd.etlserver.registrator.api.v1.ISample;
+import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.AtomicEntityRegistrationDetails;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetRegistrationInformation;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewSample;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
+
+/**
+ * Abstract superclass for the states a DataSetRegistrationTransaction can be in.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+abstract class AbstractTransactionState<T extends DataSetInformation>
+{
+    protected final DataSetRegistrationTransaction<T> parent;
+
+    protected AbstractTransactionState(DataSetRegistrationTransaction<T> parent)
+    {
+        this.parent = parent;
+    }
+
+    public abstract boolean isCommitted();
+
+    public abstract boolean isRolledback();
+
+    /**
+     * The state where the transaction is still modifyiable.
+     * 
+     * @author Chandrasekhar Ramakrishnan
+     */
+    static class LiveTransactionState<T extends DataSetInformation> extends
+            AbstractTransactionState<T>
+    {
+        // Keeps track of steps that have been executed and may need to be reverted. Elements are
+        // kept in the order they need to be reverted.
+        private final RollbackStack rollbackStack;
+
+        // The directory to use as "local" for paths
+        private final File workingDirectory;
+
+        // The directory in which new data sets get staged
+        private final File stagingDirectory;
+
+        // The registration service that owns this transaction
+        private final DataSetRegistrationService registrationService;
+
+        // The interface to openBIS
+        private final IEncapsulatedOpenBISService openBisService;
+
+        private final IDataSetRegistrationDetailsFactory<T> registrationDetailsFactory;
+
+        private final ArrayList<DataSet<T>> registeredDataSets = new ArrayList<DataSet<T>>();
+
+        private final List<Experiment> experimentsToBeRegistered = new ArrayList<Experiment>();
+
+        public LiveTransactionState(DataSetRegistrationTransaction<T> parent,
+                RollbackStack rollbackStack, File workingDirectory, File stagingDirectory,
+                DataSetRegistrationService registrationService,
+                IDataSetRegistrationDetailsFactory<T> registrationDetailsFactory)
+        {
+            super(parent);
+            this.rollbackStack = rollbackStack;
+            this.workingDirectory = workingDirectory;
+            this.stagingDirectory = stagingDirectory;
+            this.registrationService = registrationService;
+            this.openBisService =
+                    this.registrationService.getRegistratorState().getGlobalState()
+                            .getOpenBisService();
+            this.registrationDetailsFactory = registrationDetailsFactory;
+        }
+
+        public IDataSet createNewDataSet()
+        {
+            // Create registration details for the new data set
+            DataSetRegistrationDetails<T> registrationDetails =
+                    registrationDetailsFactory.createDataSetRegistrationDetails();
+
+            return createNewDataSet(registrationDetails);
+        }
+
+        public IDataSet createNewDataSet(DataSetRegistrationDetails<T> registrationDetails)
+        {
+            // Request a code, so we can keep the staging file name and the data set code in sync
+            String dataSetCode = registrationDetails.getDataSetInformation().getDataSetCode();
+            if (null == dataSetCode)
+            {
+                dataSetCode = generateDataSetCode(registrationDetails);
+                registrationDetails.getDataSetInformation().setDataSetCode(dataSetCode);
+            }
+
+            // Create a directory for the data set
+            File stagingFolder = new File(stagingDirectory, dataSetCode);
+            MkdirsCommand cmd = new MkdirsCommand(stagingFolder.getAbsolutePath());
+            executeCommand(cmd);
+
+            DataSet<T> dataSet =
+                    registrationDetailsFactory.createDataSet(registrationDetails, stagingFolder);
+            registeredDataSets.add(dataSet);
+            return dataSet;
+        }
+
+        public ISample getSampleForUpdate(String sampleIdentifierString)
+        {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        public ISample createNewSample(String sampleIdentifierString)
+        {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        public IExperiment getExperimentForUpdate(String experimentIdentifierString)
+        {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        public IExperiment createNewExperiment(String experimentIdentifierString)
+        {
+            String permID = openBisService.createDataSetCode();
+            Experiment experiment = new Experiment(experimentIdentifierString, permID);
+            experimentsToBeRegistered.add(experiment);
+            return experiment;
+        }
+
+        public String moveFile(String src, IDataSet dst)
+        {
+            File srcFile = new File(src);
+            return moveFile(src, dst, srcFile.getName());
+        }
+
+        public String moveFile(String src, IDataSet dst, String dstInDataset)
+        {
+            @SuppressWarnings("unchecked")
+            DataSet<T> dataSet = (DataSet<T>) dst;
+
+            // See if this is an absolute path
+            File srcFile = new File(src);
+            if (false == srcFile.exists())
+            {
+                // Try it relative
+                srcFile = new File(workingDirectory, src);
+            }
+
+            File dataSetFolder = dataSet.getDataSetStagingFolder();
+            File dstFile = new File(dataSetFolder, dstInDataset);
+
+            FileUtilities.checkInputFile(srcFile);
+
+            MoveFileCommand cmd =
+                    new MoveFileCommand(srcFile.getParentFile().getAbsolutePath(),
+                            srcFile.getName(), dstFile.getParentFile().getAbsolutePath(),
+                            dstFile.getName());
+            executeCommand(cmd);
+            return dstFile.getAbsolutePath();
+        }
+
+        public String createNewDirectory(IDataSet dst, String dirName)
+        {
+            @SuppressWarnings("unchecked")
+            DataSet<T> dataSet = (DataSet<T>) dst;
+            File dataSetFolder = dataSet.getDataSetStagingFolder();
+            File dstFile = new File(dataSetFolder, dirName);
+            MkdirsCommand cmd = new MkdirsCommand(dstFile.getAbsolutePath());
+            executeCommand(cmd);
+            return dstFile.getAbsolutePath();
+        }
+
+        public String createNewFile(IDataSet dst, String fileName)
+        {
+            return createNewFile(dst, "/", fileName);
+        }
+
+        public String createNewFile(IDataSet dst, String dstInDataset, String fileName)
+        {
+            @SuppressWarnings("unchecked")
+            DataSet<T> dataSet = (DataSet<T>) dst;
+            File dataSetFolder = dataSet.getDataSetStagingFolder();
+            File dstFolder = new File(dataSetFolder, dstInDataset);
+            File dstFile = new File(dstFolder, fileName);
+            NewFileCommand cmd = new NewFileCommand(dstFile.getAbsolutePath());
+            executeCommand(cmd);
+            return dstFile.getAbsolutePath();
+        }
+
+        public void deleteFile(String src)
+        {
+            // TODO Auto-generated method stub
+
+        }
+
+        /**
+         * Commit the transaction
+         */
+        public void commit()
+        {
+            ArrayList<DataSetStorageAlgorithm<T>> algorithms =
+                    new ArrayList<DataSetStorageAlgorithm<T>>(registeredDataSets.size());
+            for (DataSet<T> dataSet : registeredDataSets)
+            {
+                File contents = dataSet.getDataSetContents();
+                DataSetRegistrationDetails<T> details = dataSet.getRegistrationDetails();
+                registrationService.getRegistratorState().getDataStrategyStore()
+                        .getDataStoreStrategy(details.getDataSetInformation(), contents);
+                algorithms.add(registrationService.createStorageAlgorithm(contents, details));
+            }
+
+            DataSetStorageAlgorithmRunner<T> runner =
+                    new DataSetStorageAlgorithmRunner<T>(algorithms, parent, parent);
+            runner.prepareAndRunStorageAlgorithms();
+        }
+
+        /**
+         * Rollback any commands that have been executed. Rollback is done in the reverse order of
+         * execution.
+         */
+        public void rollback()
+        {
+            rollbackStack.rollbackAll();
+            registeredDataSets.clear();
+        }
+
+        /**
+         * Execute the command and add it to the list of commands that have been executed.
+         */
+        private void executeCommand(ITransactionalCommand cmd)
+        {
+            rollbackStack.pushAndExecuteCommand(cmd);
+        }
+
+        /**
+         * Generate a data set code for the registration details. Just calls openBisService to get a
+         * data set code by default.
+         * 
+         * @return A data set code
+         */
+        private String generateDataSetCode(DataSetRegistrationDetails<T> registrationDetails)
+        {
+            return openBisService.createDataSetCode();
+        }
+
+        AtomicEntityRegistrationDetails<T> createRegistrationDetails(
+                List<DataSetRegistrationInformation<T>> dataSetRegistrations)
+        {
+            ArrayList<NewExperiment> experimentRegistrations = new ArrayList<NewExperiment>();
+            ArrayList<ExperimentUpdatesDTO> experimentUpdates =
+                    new ArrayList<ExperimentUpdatesDTO>();
+            ArrayList<SampleUpdatesDTO> sampleUpdates = new ArrayList<SampleUpdatesDTO>();
+            ArrayList<NewSample> sampleRegistrations = new ArrayList<NewSample>();
+
+            AtomicEntityRegistrationDetails<T> registrationDetails =
+                    new AtomicEntityRegistrationDetails<T>(experimentUpdates,
+                            experimentRegistrations, sampleUpdates, sampleRegistrations,
+                            dataSetRegistrations);
+            return registrationDetails;
+        }
+
+        @Override
+        public boolean isCommitted()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isRolledback()
+        {
+            return false;
+        }
+    }
+
+    private static abstract class TerminalTransactionState<T extends DataSetInformation> extends
+            AbstractTransactionState<T>
+    {
+        private final LiveTransactionState<T> liveState;
+
+        protected TerminalTransactionState(LiveTransactionState<T> liveState)
+        {
+            super(liveState.parent);
+            this.liveState = liveState;
+            deleteStagingFolders();
+            this.liveState.rollbackStack.discard();
+        }
+
+        private void deleteStagingFolders()
+        {
+            for (DataSet<T> dataSet : liveState.registeredDataSets)
+            {
+                dataSet.getDataSetStagingFolder().delete();
+            }
+        }
+
+    }
+
+    /**
+     * State where the transaction has been committed.
+     * 
+     * @author Chandrasekhar Ramakrishnan
+     */
+    static class CommitedTransactionState<T extends DataSetInformation> extends
+            TerminalTransactionState<T>
+    {
+
+        public CommitedTransactionState(LiveTransactionState<T> liveState)
+        {
+            super(liveState);
+        }
+
+        @Override
+        public boolean isCommitted()
+        {
+            return true;
+        }
+
+        @Override
+        public boolean isRolledback()
+        {
+            return false;
+        }
+    }
+
+    static class RolledbackTransactionState<T extends DataSetInformation> extends
+            TerminalTransactionState<T>
+    {
+        public RolledbackTransactionState(LiveTransactionState<T> liveState)
+        {
+            super(liveState);
+        }
+
+        @Override
+        public boolean isCommitted()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isRolledback()
+        {
+            return true;
+        }
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransaction.java b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransaction.java
index f712acc6d88..1b1b9259ccb 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransaction.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransaction.java
@@ -18,27 +18,33 @@ package ch.systemsx.cisd.etlserver.registrator.api.v1.impl;
 
 import java.io.File;
 import java.io.FilenameFilter;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 
 import org.apache.commons.lang.time.DateFormatUtils;
 import org.apache.log4j.Logger;
 
-import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.etlserver.registrator.DataSetRegistrationDetails;
 import ch.systemsx.cisd.etlserver.registrator.DataSetRegistrationService;
+import ch.systemsx.cisd.etlserver.registrator.DataSetStorageAlgorithmRunner;
 import ch.systemsx.cisd.etlserver.registrator.IDataSetRegistrationDetailsFactory;
+import ch.systemsx.cisd.etlserver.registrator.IEntityRegistrationService;
 import ch.systemsx.cisd.etlserver.registrator.api.v1.IDataSet;
 import ch.systemsx.cisd.etlserver.registrator.api.v1.IDataSetRegistrationTransaction;
 import ch.systemsx.cisd.etlserver.registrator.api.v1.IExperiment;
 import ch.systemsx.cisd.etlserver.registrator.api.v1.IExperimentImmutable;
 import ch.systemsx.cisd.etlserver.registrator.api.v1.ISample;
 import ch.systemsx.cisd.etlserver.registrator.api.v1.ISampleImmutable;
+import ch.systemsx.cisd.etlserver.registrator.api.v1.impl.AbstractTransactionState.CommitedTransactionState;
+import ch.systemsx.cisd.etlserver.registrator.api.v1.impl.AbstractTransactionState.LiveTransactionState;
+import ch.systemsx.cisd.etlserver.registrator.api.v1.impl.AbstractTransactionState.RolledbackTransactionState;
 import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.AtomicEntityRegistrationDetails;
 import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetRegistrationInformation;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
@@ -53,7 +59,8 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifierFa
  * @author Chandrasekhar Ramakrishnan
  */
 public class DataSetRegistrationTransaction<T extends DataSetInformation> implements
-        IDataSetRegistrationTransaction
+        IDataSetRegistrationTransaction, DataSetStorageAlgorithmRunner.IRollbackDelegate<T>,
+        DataSetStorageAlgorithmRunner.IDataSetInApplicationServerRegistrator<T>
 {
     private static final String ROLLBACK_QUEUE1_FILE_NAME_SUFFIX = "rollBackQueue1";
 
@@ -150,8 +157,8 @@ public class DataSetRegistrationTransaction<T extends DataSetInformation> implem
             IDataSetRegistrationDetailsFactory<T> registrationDetailsFactory)
     {
         state =
-                new LiveTransactionState<T>(rollbackStack, workingDirectory, stagingDirectory,
-                        registrationService, registrationDetailsFactory);
+                new LiveTransactionState<T>(this, rollbackStack, workingDirectory,
+                        stagingDirectory, registrationService, registrationDetailsFactory);
         this.registrationService = registrationService;
         this.openBisService =
                 this.registrationService.getRegistratorState().getGlobalState().getOpenBisService();
@@ -159,12 +166,12 @@ public class DataSetRegistrationTransaction<T extends DataSetInformation> implem
 
     public IDataSet createNewDataSet()
     {
-        return ((LiveTransactionState<T>) state).createNewDataSet();
+        return getStateAsLiveState().createNewDataSet();
     }
 
     public IDataSet createNewDataSet(DataSetRegistrationDetails<T> registrationDetails)
     {
-        return ((LiveTransactionState<T>) state).createNewDataSet(registrationDetails);
+        return getStateAsLiveState().createNewDataSet(registrationDetails);
     }
 
     public ISampleImmutable getSample(String sampleIdentifierString)
@@ -176,12 +183,12 @@ public class DataSetRegistrationTransaction<T extends DataSetInformation> implem
 
     public ISample getSampleForUpdate(String sampleIdentifierString)
     {
-        return ((LiveTransactionState<T>) state).getSampleForUpdate(sampleIdentifierString);
+        return getStateAsLiveState().getSampleForUpdate(sampleIdentifierString);
     }
 
     public ISample createNewSample(String sampleIdentifierString)
     {
-        return ((LiveTransactionState<T>) state).createNewSample(sampleIdentifierString);
+        return getStateAsLiveState().createNewSample(sampleIdentifierString);
     }
 
     public IExperimentImmutable getExperiment(String experimentIdentifierString)
@@ -195,42 +202,42 @@ public class DataSetRegistrationTransaction<T extends DataSetInformation> implem
 
     public IExperiment getExperimentForUpdate(String experimentIdentifierString)
     {
-        return ((LiveTransactionState<T>) state).getExperimentForUpdate(experimentIdentifierString);
+        return getStateAsLiveState().getExperimentForUpdate(experimentIdentifierString);
     }
 
     public IExperiment createNewExperiment(String experimentIdentifierString)
     {
-        return ((LiveTransactionState<T>) state).createNewExperiment(experimentIdentifierString);
+        return getStateAsLiveState().createNewExperiment(experimentIdentifierString);
     }
 
     public String moveFile(String src, IDataSet dst)
     {
-        return ((LiveTransactionState<T>) state).moveFile(src, dst);
+        return getStateAsLiveState().moveFile(src, dst);
     }
 
     public String moveFile(String src, IDataSet dst, String dstInDataset)
     {
-        return ((LiveTransactionState<T>) state).moveFile(src, dst, dstInDataset);
+        return getStateAsLiveState().moveFile(src, dst, dstInDataset);
     }
 
     public String createNewDirectory(IDataSet dst, String dirName)
     {
-        return ((LiveTransactionState<T>) state).createNewDirectory(dst, dirName);
+        return getStateAsLiveState().createNewDirectory(dst, dirName);
     }
 
     public String createNewFile(IDataSet dst, String fileName)
     {
-        return ((LiveTransactionState<T>) state).createNewFile(dst, fileName);
+        return getStateAsLiveState().createNewFile(dst, fileName);
     }
 
     public String createNewFile(IDataSet dst, String dstInDataset, String fileName)
     {
-        return ((LiveTransactionState<T>) state).createNewFile(dst, dstInDataset, fileName);
+        return getStateAsLiveState().createNewFile(dst, dstInDataset, fileName);
     }
 
     public void deleteFile(String src)
     {
-        ((LiveTransactionState<T>) state).deleteFile(src);
+        getStateAsLiveState().deleteFile(src);
     }
 
     /**
@@ -238,7 +245,12 @@ public class DataSetRegistrationTransaction<T extends DataSetInformation> implem
      */
     public void commit()
     {
-        LiveTransactionState<T> liveState = (LiveTransactionState<T>) state;
+        // No need to commit again
+        if (state instanceof CommitedTransactionState)
+        {
+            return;
+        }
+        LiveTransactionState<T> liveState = getStateAsLiveState();
         liveState.commit();
 
         state = new CommitedTransactionState<T>(liveState);
@@ -250,320 +262,78 @@ public class DataSetRegistrationTransaction<T extends DataSetInformation> implem
      */
     public void rollback()
     {
-        LiveTransactionState<T> liveState = (LiveTransactionState<T>) state;
+        // No need to rollback again
+        if (state instanceof RolledbackTransactionState)
+        {
+            return;
+        }
+
+        LiveTransactionState<T> liveState = getStateAsLiveState();
         liveState.rollback();
 
         state = new RolledbackTransactionState<T>(liveState);
     }
 
     /**
-     * Abstract superclass for the states a DataSetRegistrationTransaction can be in.
-     * 
-     * @author Chandrasekhar Ramakrishnan
+     * Delegate method called by the {@link DataSetStorageAlgorithmRunner}.
      */
-    private static abstract class AbstractTransactionState<T extends DataSetInformation>
+    public void rollback(DataSetStorageAlgorithmRunner<T> algorithm, Throwable ex)
     {
-        public abstract boolean isCommitted();
-
-        public abstract boolean isRolledback();
+        rollback();
+        registrationService.rollbackTransaction(this, algorithm, ex);
     }
 
     /**
-     * The state where the transaction is still modifyiable.
-     * 
-     * @author Chandrasekhar Ramakrishnan
+     * Delegate method called by the {@link DataSetStorageAlgorithmRunner}. This implementation asks
+     * the DataSetRegistrationService to register not just the data sets, but perform any creation
+     * or updates of Experiments and Samples as well.
      */
-    private static class LiveTransactionState<T extends DataSetInformation> extends
-            AbstractTransactionState<T>
+    public void registerDataSetsInApplicationServer(
+            List<DataSetRegistrationInformation<T>> dataSetRegistrations) throws Throwable
     {
-        // Keeps track of steps that have been executed and may need to be reverted. Elements are
-        // kept in the order they need to be reverted.
-        private final RollbackStack rollbackStack;
-
-        // The directory to use as "local" for paths
-        private final File workingDirectory;
-
-        // The directory in which new data sets get staged
-        private final File stagingDirectory;
-
-        // The registration service that owns this transaction
-        private final DataSetRegistrationService registrationService;
-
-        // The interface to openBIS
-        private final IEncapsulatedOpenBISService openBisService;
-
-        private final IDataSetRegistrationDetailsFactory<T> registrationDetailsFactory;
-
-        private final ArrayList<DataSet<T>> registeredDataSets = new ArrayList<DataSet<T>>();
-
-        private final List<Experiment> experimentsToBeRegistered = new ArrayList<Experiment>();
-
-        public LiveTransactionState(RollbackStack rollbackStack, File workingDirectory,
-                File stagingDirectory, DataSetRegistrationService registrationService,
-                IDataSetRegistrationDetailsFactory<T> registrationDetailsFactory)
-        {
-            this.rollbackStack = rollbackStack;
-            this.workingDirectory = workingDirectory;
-            this.stagingDirectory = stagingDirectory;
-            this.registrationService = registrationService;
-            this.openBisService =
-                    this.registrationService.getRegistratorState().getGlobalState()
-                            .getOpenBisService();
-            this.registrationDetailsFactory = registrationDetailsFactory;
-        }
-
-        public IDataSet createNewDataSet()
-        {
-            // Create registration details for the new data set
-            DataSetRegistrationDetails<T> registrationDetails =
-                    registrationDetailsFactory.createDataSetRegistrationDetails();
-
-            return createNewDataSet(registrationDetails);
-        }
-
-        public IDataSet createNewDataSet(DataSetRegistrationDetails<T> registrationDetails)
-        {
-            // Request a code, so we can keep the staging file name and the data set code in sync
-            String dataSetCode = registrationDetails.getDataSetInformation().getDataSetCode();
-            if (null == dataSetCode)
-            {
-                dataSetCode = generateDataSetCode(registrationDetails);
-                registrationDetails.getDataSetInformation().setDataSetCode(dataSetCode);
-            }
-
-            // Create a directory for the data set
-            File stagingFolder = new File(stagingDirectory, dataSetCode);
-            MkdirsCommand cmd = new MkdirsCommand(stagingFolder.getAbsolutePath());
-            executeCommand(cmd);
-
-            DataSet<T> dataSet =
-                    registrationDetailsFactory.createDataSet(registrationDetails, stagingFolder);
-            registeredDataSets.add(dataSet);
-            return dataSet;
-        }
-
-        public ISample getSampleForUpdate(String sampleIdentifierString)
-        {
-            // TODO Auto-generated method stub
-            return null;
-        }
-
-        public ISample createNewSample(String sampleIdentifierString)
-        {
-            // TODO Auto-generated method stub
-            return null;
-        }
-
-        public IExperiment getExperimentForUpdate(String experimentIdentifierString)
-        {
-            // TODO Auto-generated method stub
-            return null;
-        }
-
-        public IExperiment createNewExperiment(String experimentIdentifierString)
-        {
-            String permID = openBisService.createDataSetCode();
-            Experiment experiment = new Experiment(experimentIdentifierString, permID);
-            experimentsToBeRegistered.add(experiment);
-            return experiment;
-        }
-
-        public String moveFile(String src, IDataSet dst)
-        {
-            File srcFile = new File(src);
-            return moveFile(src, dst, srcFile.getName());
-        }
-
-        public String moveFile(String src, IDataSet dst, String dstInDataset)
-        {
-            @SuppressWarnings("unchecked")
-            DataSet<T> dataSet = (DataSet<T>) dst;
-
-            // See if this is an absolute path
-            File srcFile = new File(src);
-            if (false == srcFile.exists())
-            {
-                // Try it relative
-                srcFile = new File(workingDirectory, src);
-            }
-
-            File dataSetFolder = dataSet.getDataSetStagingFolder();
-            File dstFile = new File(dataSetFolder, dstInDataset);
-
-            FileUtilities.checkInputFile(srcFile);
-
-            MoveFileCommand cmd =
-                    new MoveFileCommand(srcFile.getParentFile().getAbsolutePath(),
-                            srcFile.getName(), dstFile.getParentFile().getAbsolutePath(),
-                            dstFile.getName());
-            executeCommand(cmd);
-            return dstFile.getAbsolutePath();
-        }
-
-        public String createNewDirectory(IDataSet dst, String dirName)
-        {
-            @SuppressWarnings("unchecked")
-            DataSet<T> dataSet = (DataSet<T>) dst;
-            File dataSetFolder = dataSet.getDataSetStagingFolder();
-            File dstFile = new File(dataSetFolder, dirName);
-            MkdirsCommand cmd = new MkdirsCommand(dstFile.getAbsolutePath());
-            executeCommand(cmd);
-            return dstFile.getAbsolutePath();
-        }
-
-        public String createNewFile(IDataSet dst, String fileName)
-        {
-            return createNewFile(dst, "/", fileName);
-        }
+        AtomicEntityRegistrationDetails<T> registrationDetails =
+                getStateAsLiveState().createRegistrationDetails(dataSetRegistrations);
+        IEntityRegistrationService<T> entityRegistrationService =
+                registrationService.getEntityRegistrationService();
 
-        public String createNewFile(IDataSet dst, String dstInDataset, String fileName)
-        {
-            @SuppressWarnings("unchecked")
-            DataSet<T> dataSet = (DataSet<T>) dst;
-            File dataSetFolder = dataSet.getDataSetStagingFolder();
-            File dstFolder = new File(dataSetFolder, dstInDataset);
-            File dstFile = new File(dstFolder, fileName);
-            NewFileCommand cmd = new NewFileCommand(dstFile.getAbsolutePath());
-            executeCommand(cmd);
-            return dstFile.getAbsolutePath();
-        }
-
-        public void deleteFile(String src)
-        {
-            // TODO Auto-generated method stub
-
-        }
-
-        /**
-         * Commit the transaction
-         */
-        public void commit()
-        {
-            for (DataSet<T> dataSet : registeredDataSets)
-            {
-                registrationService.queueDataSetRegistration(dataSet.getDataSetContents(),
-                        dataSet.getRegistrationDetails());
-            }
-            registrationService.commit();
-
-            // for (DataSet<T> dataSet : registeredDataSets)
-            // {
-            // File contents = dataSet.getDataSetContents();
-            // DataSetRegistrationDetails<T> details = dataSet.getRegistrationDetails();
-            // registrationService.getRegistratorState().getDataStrategyStore()
-            // .getDataStoreStrategy(details.getDataSetInformation(), contents);
-            // DataSetStorageAlgorithm<T> algorithm =
-            // new DataSetStorageAlgorithm<T>(contents, details, null, null, null, null,
-            // null, null);
-            // }
-            // registrationService.commit();
-        }
-
-        /**
-         * Rollback any commands that have been executed. Rollback is done in the reverse order of
-         * execution.
-         */
-        public void rollback()
-        {
-            rollbackStack.rollbackAll();
-            registeredDataSets.clear();
-        }
-
-        /**
-         * Execute the command and add it to the list of commands that have been executed.
-         */
-        private void executeCommand(ITransactionalCommand cmd)
-        {
-            rollbackStack.pushAndExecuteCommand(cmd);
-        }
-
-        /**
-         * Generate a data set code for the registration details. Just calls openBisService to get a
-         * data set code by default.
-         * 
-         * @return A data set code
-         */
-        private String generateDataSetCode(DataSetRegistrationDetails<T> registrationDetails)
-        {
-            return openBisService.createDataSetCode();
-        }
-
-        @Override
-        public boolean isCommitted()
-        {
-            return false;
-        }
-
-        @Override
-        public boolean isRolledback()
-        {
-            return false;
-        }
+        entityRegistrationService.registerEntitiesInApplcationServer(registrationDetails);
     }
 
-    private static abstract class TerminalTransactionState<T extends DataSetInformation> extends
-            AbstractTransactionState<T>
+    public boolean isCommittedOrRolledback()
     {
-        private final LiveTransactionState<T> liveState;
-
-        protected TerminalTransactionState(LiveTransactionState<T> liveState)
-        {
-            this.liveState = liveState;
-            deleteStagingFolders();
-            this.liveState.rollbackStack.discard();
-        }
-
-        private void deleteStagingFolders()
-        {
-            for (DataSet<T> dataSet : liveState.registeredDataSets)
-            {
-                dataSet.getDataSetStagingFolder().delete();
-            }
-        }
-
+        return isCommitted() || isRolledback();
     }
 
-    private static class CommitedTransactionState<T extends DataSetInformation> extends
-            TerminalTransactionState<T>
+    public boolean isCommitted()
     {
-
-        public CommitedTransactionState(LiveTransactionState<T> liveState)
-        {
-            super(liveState);
-        }
-
-        @Override
-        public boolean isCommitted()
-        {
-            return true;
-        }
-
-        @Override
-        public boolean isRolledback()
-        {
-            return false;
-        }
+        return state.isCommitted();
     }
 
-    private static class RolledbackTransactionState<T extends DataSetInformation> extends
-            TerminalTransactionState<T>
+    public boolean isRolledback()
     {
-        public RolledbackTransactionState(LiveTransactionState<T> liveState)
-        {
-            super(liveState);
-        }
+        return state.isRolledback();
+    }
 
-        @Override
-        public boolean isCommitted()
+    /**
+     * Return the state as live state. Throw an EnvironmentFailureException if this is not possible.
+     */
+    private LiveTransactionState<T> getStateAsLiveState()
+    {
+        try
         {
-            return false;
-        }
-
-        @Override
-        public boolean isRolledback()
+            LiveTransactionState<T> liveState = (LiveTransactionState<T>) state;
+            return liveState;
+        } catch (ClassCastException ex)
         {
-            return true;
+            String message;
+            if (state instanceof CommitedTransactionState)
+            {
+                message = "The transaction has already been committed";
+            } else
+            {
+                message = "The transaction has already been rolledback";
+            }
+            throw new EnvironmentFailureException(message, ex);
         }
     }
-
 }
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationDetails.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationDetails.java
new file mode 100644
index 00000000000..a8097a5b7ad
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationDetails.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.dss.generic.shared.dto;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewSample;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
+
+/**
+ * An object that captures the state for performing the registration of one or many openBIS entities
+ * atomically.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class AtomicEntityRegistrationDetails<T extends DataSetInformation> implements
+        Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    private final ArrayList<ExperimentUpdatesDTO> experimentUpdates;
+
+    private final ArrayList<NewExperiment> experimentRegistrations;
+
+    private final ArrayList<SampleUpdatesDTO> sampleUpdates;
+
+    private final ArrayList<NewSample> sampleRegistrations;
+
+    private final ArrayList<DataSetRegistrationInformation<T>> dataSetRegistrations;
+
+    public AtomicEntityRegistrationDetails(List<ExperimentUpdatesDTO> experimentUpdates,
+            List<NewExperiment> experimentRegistrations, List<SampleUpdatesDTO> sampleUpdates,
+            List<NewSample> sampleRegistrations,
+            List<DataSetRegistrationInformation<T>> dataSetRegistrations)
+    {
+        this.experimentUpdates = new ArrayList<ExperimentUpdatesDTO>(experimentUpdates);
+        this.experimentRegistrations = new ArrayList<NewExperiment>(experimentRegistrations);
+        this.sampleUpdates = new ArrayList<SampleUpdatesDTO>(sampleUpdates);
+        this.sampleRegistrations = new ArrayList<NewSample>(sampleRegistrations);
+        this.dataSetRegistrations =
+                new ArrayList<DataSetRegistrationInformation<T>>(dataSetRegistrations);
+    }
+
+    public ArrayList<ExperimentUpdatesDTO> getExperimentUpdates()
+    {
+        return experimentUpdates;
+    }
+
+    public ArrayList<NewExperiment> getExperimentRegistrations()
+    {
+        return experimentRegistrations;
+    }
+
+    public ArrayList<SampleUpdatesDTO> getSampleUpdates()
+    {
+        return sampleUpdates;
+    }
+
+    public ArrayList<NewSample> getSampleRegistrations()
+    {
+        return sampleRegistrations;
+    }
+
+    public ArrayList<DataSetRegistrationInformation<T>> getDataSetRegistrations()
+    {
+        return dataSetRegistrations;
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationResult.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationResult.java
new file mode 100644
index 00000000000..1e0d8c4bb04
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/AtomicEntityRegistrationResult.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.dss.generic.shared.dto;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample;
+
+/**
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class AtomicEntityRegistrationResult implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    private final ArrayList<Experiment> experimentsUpdated;
+
+    private final ArrayList<Experiment> experimentsCreated;
+
+    private final ArrayList<Sample> samplesUpdated;
+
+    private final ArrayList<Sample> samplesCreated;
+
+    private final ArrayList<DataSetInformation> dataSetsCreated;
+
+    public AtomicEntityRegistrationResult(List<Experiment> experimentsUpdated,
+            List<Experiment> experimentsCreated, List<Sample> samplesUpdated,
+            List<Sample> samplesCreated, List<DataSetInformation> dataSetsCreated)
+    {
+        this.experimentsUpdated = new ArrayList<Experiment>(experimentsUpdated);
+        this.experimentsCreated = new ArrayList<Experiment>(experimentsCreated);
+        this.samplesUpdated = new ArrayList<Sample>(samplesUpdated);
+        this.samplesCreated = new ArrayList<Sample>(samplesCreated);
+        this.dataSetsCreated = new ArrayList<DataSetInformation>(dataSetsCreated);
+    }
+
+    public ArrayList<Experiment> getExperimentsUpdated()
+    {
+        return experimentsUpdated;
+    }
+
+    public ArrayList<Experiment> getExperimentsCreated()
+    {
+        return experimentsCreated;
+    }
+
+    public ArrayList<Sample> getSamplesUpdated()
+    {
+        return samplesUpdated;
+    }
+
+    public ArrayList<Sample> getSamplesCreated()
+    {
+        return samplesCreated;
+    }
+
+    public ArrayList<DataSetInformation> getDataSetsCreated()
+    {
+        return dataSetsCreated;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/DataSetRegistrationInformation.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/DataSetRegistrationInformation.java
new file mode 100644
index 00000000000..3f0c9e0f3da
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/dto/DataSetRegistrationInformation.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2011 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.dss.generic.shared.dto;
+
+import ch.systemsx.cisd.openbis.generic.shared.dto.NewExternalData;
+
+public class DataSetRegistrationInformation<T extends DataSetInformation>
+{
+    private final T dataSetInformation;
+
+    private final NewExternalData externalData;
+
+    public DataSetRegistrationInformation(T dataSetInformation, NewExternalData externalData)
+    {
+        this.dataSetInformation = dataSetInformation;
+        this.externalData = externalData;
+    }
+
+    public T getDataSetInformation()
+    {
+        return dataSetInformation;
+    }
+
+    public NewExternalData getExternalData()
+    {
+        return externalData;
+    }
+}
\ No newline at end of file
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetRegistratorTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetRegistratorTest.java
index a9ef583ccae..111b0479d01 100644
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetRegistratorTest.java
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/JythonTopLevelDataSetRegistratorTest.java
@@ -635,7 +635,7 @@ public class JythonTopLevelDataSetRegistratorTest extends AbstractFileSystemTest
                     registrator.setEmail("email@email.com");
                     experiment.setRegistrator(registrator);
 
-                    exactly(2).of(openBisService).tryToGetExperiment(
+                    exactly(3).of(openBisService).tryToGetExperiment(
                             new ExperimentIdentifierFactory("/SPACE/PROJECT/EXP-CODE")
                                     .createIdentifier());
                     will(returnValue(experiment));
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransactionTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransactionTest.java
index d1f4e047c91..ec532bd3c9c 100644
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransactionTest.java
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/etlserver/registrator/api/v1/impl/DataSetRegistrationTransactionTest.java
@@ -34,7 +34,6 @@ import ch.systemsx.cisd.base.tests.AbstractFileSystemTestCase;
 import ch.systemsx.cisd.common.filesystem.FileUtilities;
 import ch.systemsx.cisd.common.filesystem.QueueingPathRemoverService;
 import ch.systemsx.cisd.common.logging.BufferedAppender;
-import ch.systemsx.cisd.common.mail.From;
 import ch.systemsx.cisd.common.mail.IMailClient;
 import ch.systemsx.cisd.common.utilities.ExtendedProperties;
 import ch.systemsx.cisd.common.utilities.IDelegatedActionWithResult;
@@ -132,7 +131,6 @@ public class DataSetRegistrationTransactionTest extends AbstractFileSystemTestCa
     {
         setUpOpenBisExpectations(true);
         setUpDataSetValidatorExpectations();
-        setUpMailClientExpectations();
         createTransaction();
 
         IDataSet newDataSet = tr.createNewDataSet();
@@ -214,9 +212,7 @@ public class DataSetRegistrationTransactionTest extends AbstractFileSystemTestCa
         context.assertIsSatisfied();
     }
 
-    // The second invocation of rollback will cause a class-cast exception.
-    @Test(expectedExceptions =
-        { ClassCastException.class })
+    @Test
     public void testDoubleRollbackNormal()
     {
         setUpOpenBisExpectations(false);
@@ -371,7 +367,7 @@ public class DataSetRegistrationTransactionTest extends AbstractFileSystemTestCa
                         project.setSpace(space);
                         experiment.setProject(project);
 
-                        exactly(2).of(openBisService).tryToGetExperiment(
+                        exactly(3).of(openBisService).tryToGetExperiment(
                                 new ExperimentIdentifierFactory(EXPERIMENT_IDENTIFIER)
                                         .createIdentifier());
                         will(returnValue(experiment));
@@ -397,18 +393,6 @@ public class DataSetRegistrationTransactionTest extends AbstractFileSystemTestCa
             });
     }
 
-    private void setUpMailClientExpectations()
-    {
-        context.checking(new Expectations()
-            {
-                {
-                    exactly(1).of(mailClient).sendMessage(with(any(String.class)),
-                            with(any(String.class)), with(aNull(String.class)),
-                            with(aNull(From.class)), with(any(String[].class)));
-                }
-            });
-    }
-
     public static final class MockStorageProcessor implements IStorageProcessor
     {
         static MockStorageProcessor instance;
-- 
GitLab