From bce73c9dfb271b7b060ee25974ef09fbd9c42771 Mon Sep 17 00:00:00 2001
From: cramakri <cramakri>
Date: Wed, 9 Feb 2011 09:39:07 +0000
Subject: [PATCH] LMS-2033 Added atomic entity operation to IETLLIMSService

SVN: 19829
---
 .../openbis/generic/server/ETLService.java    |  88 ++++++-
 .../generic/server/ETLServiceLogger.java      |   9 +
 .../generic/shared/IETLLIMSService.java       |  25 +-
 .../predicate/AtomicOperationsPredicate.java  | 232 ++++++++++++++++++
 .../dto/AtomicEntityOperationDetails.java     | 100 ++++++++
 .../dto/AtomicEntityOperationResult.java      |  78 ++++++
 .../dto/AtomicEntityOperationDetailsTest.java |  69 ++++++
 7 files changed, 598 insertions(+), 3 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/AtomicOperationsPredicate.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetails.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationResult.java
 create mode 100644 openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
index 8e1d24f2cae..e02873c6b1c 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
@@ -53,6 +53,8 @@ import ch.systemsx.cisd.openbis.generic.shared.IDataStoreService;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ArchiverDataSetCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationDetails;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetArchivingStatus;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetTypeWithVocabularyTerms;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataStoreServiceKind;
@@ -103,7 +105,9 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.SimpleDataSetInformationDTO;
 import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyTermPE;
 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;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifierFactory;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SpaceIdentifier;
 import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityKind;
 import ch.systemsx.cisd.openbis.generic.shared.translator.DataSetTypeTranslator;
@@ -485,7 +489,7 @@ public class ETLService extends AbstractCommonServer<IETLService> implements IET
         Collections.sort(datasets);
         return datasets;
     }
-    
+
     public List<ExternalData> listDataSetsByCode(String sessionToken, List<String> dataSetCodes)
     {
         final Session session = getSession(sessionToken);
@@ -929,4 +933,86 @@ public class ETLService extends AbstractCommonServer<IETLService> implements IET
         return samplePE;
     }
 
+    public AtomicEntityOperationResult performEntityOperations(String sessionToken,
+            AtomicEntityOperationDetails operationDetails)
+    {
+
+        ArrayList<Experiment> experimentsCreated =
+                createExperiments(sessionToken, operationDetails);
+
+        ArrayList<Sample> samplesUpdated = updateSamples(sessionToken, operationDetails);
+
+        ArrayList<Sample> samplesCreated = createSamples(sessionToken, operationDetails);
+
+        ArrayList<ExternalData> dataSetsCreated = createDataSets(sessionToken, operationDetails);
+
+        return new AtomicEntityOperationResult(experimentsCreated, samplesUpdated, samplesCreated,
+                dataSetsCreated);
+    }
+
+    private ArrayList<Sample> createSamples(String sessionToken,
+            AtomicEntityOperationDetails operationDetails)
+    {
+        ArrayList<Sample> samplesCreated = new ArrayList<Sample>();
+        List<NewSample> newSamples = operationDetails.getSampleRegistrations();
+        for (NewSample newSample : newSamples)
+        {
+            registerSample(sessionToken, newSample, null);
+            SampleIdentifier sampleIdentifier =
+                    new SampleIdentifierFactory(newSample.getIdentifier()).createIdentifier();
+            samplesCreated.add(tryGetSampleWithExperiment(sessionToken, sampleIdentifier));
+        }
+        return samplesCreated;
+    }
+
+    private ArrayList<Sample> updateSamples(String sessionToken,
+            AtomicEntityOperationDetails operationDetails)
+    {
+        ArrayList<Sample> samplesUpdated = new ArrayList<Sample>();
+        List<SampleUpdatesDTO> sampleUpdates = operationDetails.getSampleUpdates();
+        for (SampleUpdatesDTO sampleUpdate : sampleUpdates)
+        {
+            updateSample(sessionToken, sampleUpdate);
+            SampleIdentifier sampleIdentifier = sampleUpdate.getSampleIdentifier();
+            samplesUpdated.add(tryGetSampleWithExperiment(sessionToken, sampleIdentifier));
+        }
+        return samplesUpdated;
+    }
+
+    private ArrayList<ExternalData> createDataSets(String sessionToken,
+            AtomicEntityOperationDetails operationDetails)
+    {
+        ArrayList<ExternalData> dataSetsCreated = new ArrayList<ExternalData>();
+        ArrayList<NewExternalData> dataSetRegistrations =
+                operationDetails.getDataSetRegistrations();
+        for (NewExternalData dataSet : dataSetRegistrations)
+        {
+            SampleIdentifier sampleIdentifier = dataSet.getSampleIdentifierOrNull();
+            if (sampleIdentifier != null)
+            {
+                registerDataSet(sessionToken, sampleIdentifier, dataSet);
+            } else
+            {
+                ExperimentIdentifier experimentIdentifier = dataSet.getExperimentIdentifierOrNull();
+                registerDataSet(sessionToken, experimentIdentifier, dataSet);
+            }
+            dataSetsCreated.add(tryGetDataSet(sessionToken, dataSet.getCode()));
+        }
+        return dataSetsCreated;
+    }
+
+    private ArrayList<Experiment> createExperiments(String sessionToken,
+            AtomicEntityOperationDetails operationDetails)
+    {
+        ArrayList<Experiment> experimentsCreated = new ArrayList<Experiment>();
+        List<NewExperiment> experimentRegistrations = operationDetails.getExperimentRegistrations();
+        for (NewExperiment experiment : experimentRegistrations)
+        {
+            registerExperiment(sessionToken, experiment);
+            ExperimentIdentifier experimentIdentifier =
+                    new ExperimentIdentifierFactory(experiment.getIdentifier()).createIdentifier();
+            experimentsCreated.add(tryToGetExperiment(sessionToken, experimentIdentifier));
+        }
+        return experimentsCreated;
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
index 23be394552a..4fa5f065b4e 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
@@ -25,6 +25,8 @@ import ch.systemsx.cisd.common.spring.IInvocationLoggerContext;
 import ch.systemsx.cisd.openbis.generic.shared.AbstractServerLogger;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ArchiverDataSetCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationDetails;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetArchivingStatus;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetTypeWithVocabularyTerms;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseInstance;
@@ -390,4 +392,11 @@ public class ETLServiceLogger extends AbstractServerLogger implements IETLServic
                 updates.getSampleIdentifier(), externalData);
         return null;
     }
+
+    public AtomicEntityOperationResult performEntityOperations(String sessionToken,
+            AtomicEntityOperationDetails operationDetails)
+    {
+        logAccess(sessionToken, "performEntityOperations", "%s", operationDetails);
+        return null;
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
index 0c7a98577cf..26ed83379e6 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
@@ -27,6 +27,7 @@ import ch.systemsx.cisd.openbis.generic.shared.authorization.annotation.Authoriz
 import ch.systemsx.cisd.openbis.generic.shared.authorization.annotation.ReturnValueFilter;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.annotation.RolesAllowed;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.AbstractTechIdPredicate.ExperimentTechIdPredicate;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.AtomicOperationsPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.DataSetCodeCollectionPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.DataSetCodePredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.ListSampleCriteriaPredicate;
@@ -41,10 +42,12 @@ import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.SpaceIden
 import ch.systemsx.cisd.openbis.generic.shared.authorization.validator.SampleValidator;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ArchiverDataSetCriteria;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationDetails;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetArchivingStatus;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetTypeWithVocabularyTerms;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseInstance;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseModificationKind.ObjectKind;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationResult;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DeletedDataSet;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentType;
@@ -199,7 +202,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider
             String sessionToken,
             @AuthorizationGuard(guardClass = DataSetCodeCollectionPredicate.class) List<String> dataSetCodes)
             throws UserFailureException;
-    
+
     /**
      * Lists samples using given configuration.
      * 
@@ -227,7 +230,7 @@ public interface IETLLIMSService extends IServer, ISessionProvider
             final String sessionToken,
             @AuthorizationGuard(guardClass = SampleOwnerIdentifierPredicate.class) final SampleIdentifier sampleIdentifier)
             throws UserFailureException;
-    
+
     /**
      * Registers/updates various entities in one transaction.
      */
@@ -550,4 +553,22 @@ public interface IETLLIMSService extends IServer, ISessionProvider
             @AuthorizationGuard(guardClass = SampleUpdatesPredicate.class) SampleUpdatesDTO updates,
             NewExternalData externalData);
 
+    /**
+     * Updates a sample and registers a data set connected to that sample in one transaction.
+     * 
+     * @param sessionToken The user authentication token. Must not be <code>null</code>.
+     * @param operationDetails A DTO containing information about the entities to change / register.
+     * @throws UserFailureException if given data set code could not be found in the persistence
+     *             layer.
+     */
+    @Transactional
+    @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
+    @DatabaseUpdateModification(value =
+        { ObjectKind.SAMPLE, ObjectKind.EXPERIMENT })
+    @DatabaseCreateOrDeleteModification(value =
+        { ObjectKind.SAMPLE, ObjectKind.EXPERIMENT, ObjectKind.DATA_SET })
+    public AtomicEntityOperationResult performEntityOperations(
+            String sessionToken,
+            @AuthorizationGuard(guardClass = AtomicOperationsPredicate.class) AtomicEntityOperationDetails operationDetails);
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/AtomicOperationsPredicate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/AtomicOperationsPredicate.java
new file mode 100644
index 00000000000..3f5413b33a8
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/AtomicOperationsPredicate.java
@@ -0,0 +1,232 @@
+/*
+ * 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.generic.shared.authorization.predicate;
+
+import java.util.List;
+
+import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.IAuthorizationDataProvider;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.RoleWithIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AtomicEntityOperationDetails;
+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.NewExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
+
+/**
+ * The predicate for the {@link AtomicEntityOperationDetails}.
+ * 
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class AtomicOperationsPredicate extends AbstractPredicate<AtomicEntityOperationDetails>
+{
+    private final NewExperimentPredicate newExperimentPredicate;
+
+    private final ExperimentUpdatesPredicate experimentUpdatesPredicate;
+
+    private final NewSamplePredicate newSamplePredicate;
+
+    private final SampleUpdatesPredicate sampleUpdatesPredicate;
+
+    private final SampleOwnerIdentifierPredicate sampleOwnerIdentifierPredicate;
+
+    private final SpaceIdentifierPredicate experimentOwnerIdentifierPredicate;
+
+    public AtomicOperationsPredicate()
+    {
+        newExperimentPredicate = new NewExperimentPredicate();
+        experimentUpdatesPredicate = new ExperimentUpdatesPredicate();
+        newSamplePredicate = new NewSamplePredicate();
+        sampleUpdatesPredicate = new SampleUpdatesPredicate();
+        sampleOwnerIdentifierPredicate = new SampleOwnerIdentifierPredicate();
+        experimentOwnerIdentifierPredicate = new SpaceIdentifierPredicate();
+    }
+
+    public void init(IAuthorizationDataProvider provider)
+    {
+        newExperimentPredicate.init(provider);
+        experimentUpdatesPredicate.init(provider);
+        newSamplePredicate.init(provider);
+        sampleUpdatesPredicate.init(provider);
+        sampleOwnerIdentifierPredicate.init(provider);
+        experimentOwnerIdentifierPredicate.init(provider);
+    }
+
+    @Override
+    public String getCandidateDescription()
+    {
+        return "atomic entity operations";
+    }
+
+    @Override
+    protected Status doEvaluation(PersonPE person, List<RoleWithIdentifier> allowedRoles,
+            AtomicEntityOperationDetails value)
+    {
+        AtomicOperationsPredicateEvaluator evaluator =
+                new AtomicOperationsPredicateEvaluator(this, person, allowedRoles, value);
+        return evaluator.evaluate();
+    }
+
+    /**
+     * A class to evaluate all the sub-predicates.
+     * 
+     * @author Chandrasekhar Ramakrishnan
+     */
+    private static class AtomicOperationsPredicateEvaluator
+    {
+        private final AtomicOperationsPredicate predicate;
+
+        private final PersonPE person;
+
+        private final List<RoleWithIdentifier> allowedRoles;
+
+        private final AtomicEntityOperationDetails value;
+
+        private Status result = Status.OK;
+
+        public AtomicOperationsPredicateEvaluator(AtomicOperationsPredicate predicate,
+                PersonPE person, List<RoleWithIdentifier> allowedRoles,
+                AtomicEntityOperationDetails value)
+        {
+            this.predicate = predicate;
+            this.person = person;
+            this.allowedRoles = allowedRoles;
+            this.value = value;
+        }
+
+        public Status evaluate()
+        {
+            // Evaluate all the predicates, stopping if we find an operation that is not allowed
+            if (result.equals(Status.OK))
+            {
+                result = evaluateExperimentUpdatePredicate();
+            }
+            if (result.equals(Status.OK))
+            {
+                result = evaluateNewExperimentPredicate();
+            }
+            if (result.equals(Status.OK))
+            {
+                result = evaluateSampleUpdatePredicate();
+            }
+            if (result.equals(Status.OK))
+            {
+                result = evaluateNewSamplePredicate();
+            }
+            if (result.equals(Status.OK))
+            {
+                result = evaluateDataSetsPredicate();
+            }
+
+            return result;
+        }
+
+        private Status evaluateExperimentUpdatePredicate()
+        {
+            for (ExperimentUpdatesDTO experimentUpdates : value.getExperimentUpdates())
+            {
+                Status status;
+
+                status =
+                        predicate.experimentUpdatesPredicate.doEvaluation(person, allowedRoles,
+                                experimentUpdates);
+                if (status.equals(Status.OK) == false)
+                {
+                    return status;
+                }
+            }
+            return Status.OK;
+        }
+
+        private Status evaluateNewExperimentPredicate()
+        {
+            for (NewExperiment newExperiment : value.getExperimentRegistrations())
+            {
+                Status status;
+
+                status =
+                        predicate.newExperimentPredicate.doEvaluation(person, allowedRoles,
+                                newExperiment);
+                if (status.equals(Status.OK) == false)
+                {
+                    return status;
+                }
+            }
+            return Status.OK;
+        }
+
+        private Status evaluateSampleUpdatePredicate()
+        {
+            for (SampleUpdatesDTO sampleUpdates : value.getSampleUpdates())
+            {
+                Status status =
+                        predicate.sampleUpdatesPredicate.doEvaluation(person, allowedRoles,
+                                sampleUpdates);
+                if (status.equals(Status.OK) == false)
+                {
+                    return status;
+                }
+            }
+            return Status.OK;
+        }
+
+        private Status evaluateNewSamplePredicate()
+        {
+            for (NewSample newSample : value.getSampleRegistrations())
+            {
+                Status status =
+                        predicate.newSamplePredicate.doEvaluation(person, allowedRoles, newSample);
+                if (status.equals(Status.OK) == false)
+                {
+                    return status;
+                }
+            }
+            return Status.OK;
+        }
+
+        private Status evaluateDataSetsPredicate()
+        {
+            for (NewExternalData newExternalData : value.getDataSetRegistrations())
+            {
+                Status status;
+                SampleIdentifier sampleIdentifier = newExternalData.getSampleIdentifierOrNull();
+                if (null != sampleIdentifier)
+                {
+                    status =
+                            predicate.sampleOwnerIdentifierPredicate.doEvaluation(person,
+                                    allowedRoles, sampleIdentifier);
+                } else
+                {
+                    ExperimentIdentifier experimentIdentifier =
+                            newExternalData.getExperimentIdentifierOrNull();
+                    status =
+                            predicate.experimentOwnerIdentifierPredicate.doEvaluation(person,
+                                    allowedRoles, experimentIdentifier);
+                }
+                if (status.equals(Status.OK) == false)
+                {
+                    return status;
+                }
+            }
+            return Status.OK;
+        }
+    }
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetails.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetails.java
new file mode 100644
index 00000000000..e879766d701
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetails.java
@@ -0,0 +1,100 @@
+/*
+ * 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.generic.shared.basic.dto;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.NewExternalData;
+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 AtomicEntityOperationDetails implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    // This is always an empty list, since it is not currently possible to update experiments from
+    // the DSS
+    private final ArrayList<ExperimentUpdatesDTO> experimentUpdates;
+
+    private final ArrayList<NewExperiment> experimentRegistrations;
+
+    private final ArrayList<SampleUpdatesDTO> sampleUpdates;
+
+    private final ArrayList<NewSample> sampleRegistrations;
+
+    private final ArrayList<NewExternalData> dataSetRegistrations;
+
+    public AtomicEntityOperationDetails(List<NewExperiment> experimentRegistrations,
+            List<SampleUpdatesDTO> sampleUpdates, List<NewSample> sampleRegistrations,
+            List<NewExternalData> dataSetRegistrations)
+    {
+        this.experimentUpdates = new ArrayList<ExperimentUpdatesDTO>();
+        this.experimentRegistrations = new ArrayList<NewExperiment>(experimentRegistrations);
+        this.sampleUpdates = new ArrayList<SampleUpdatesDTO>(sampleUpdates);
+        this.sampleRegistrations = new ArrayList<NewSample>(sampleRegistrations);
+        this.dataSetRegistrations = new ArrayList<NewExternalData>(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<NewExternalData> getDataSetRegistrations()
+    {
+        return dataSetRegistrations;
+    }
+
+    @Override
+    public String toString()
+    {
+        ToStringBuilder sb = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
+        sb.append("experimentUpdates", experimentUpdates);
+        sb.append("experimentRegistrations", experimentRegistrations);
+        sb.append("sampleUpdates", sampleUpdates);
+        sb.append("sampleRegistrations", sampleRegistrations);
+        sb.append("dataSetRegistrations", dataSetRegistrations);
+        return sb.toString();
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationResult.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationResult.java
new file mode 100644
index 00000000000..7a108148702
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationResult.java
@@ -0,0 +1,78 @@
+/*
+ * 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.generic.shared.basic.dto;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class AtomicEntityOperationResult implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    // This is currently always an empty list since there is no way to update experiments from the
+    // DSS.
+    private final ArrayList<Experiment> experimentsUpdated;
+
+    private final ArrayList<Experiment> experimentsCreated;
+
+    private final ArrayList<Sample> samplesUpdated;
+
+    private final ArrayList<Sample> samplesCreated;
+
+    private final ArrayList<ExternalData> dataSetsCreated;
+
+    public AtomicEntityOperationResult(List<Experiment> experimentsCreated,
+            List<Sample> samplesUpdated, List<Sample> samplesCreated,
+            List<ExternalData> dataSetsCreated)
+    {
+        this.experimentsUpdated = new ArrayList<Experiment>();
+        this.experimentsCreated = new ArrayList<Experiment>(experimentsCreated);
+        this.samplesUpdated = new ArrayList<Sample>(samplesUpdated);
+        this.samplesCreated = new ArrayList<Sample>(samplesCreated);
+        this.dataSetsCreated = new ArrayList<ExternalData>(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<ExternalData> getDataSetsCreated()
+    {
+        return dataSetsCreated;
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java
new file mode 100644
index 00000000000..ac24dd67d73
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AtomicEntityOperationDetailsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.generic.shared.basic.dto;
+
+import java.util.ArrayList;
+
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.openbis.generic.shared.dto.NewExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifierFactory;
+
+/**
+ * @author Chandrasekhar Ramakrishnan
+ */
+public class AtomicEntityOperationDetailsTest extends AssertJUnit
+{
+
+    @Test
+    public void testToString()
+    {
+        ArrayList<NewExperiment> experimentRegistrations = new ArrayList<NewExperiment>();
+        experimentRegistrations.add(new NewExperiment("/SPACE/PROJECT/EXP-ID1", "EXP-TYPE"));
+        experimentRegistrations.add(new NewExperiment("/SPACE/PROJECT/EXP-ID2", "EXP-TYPE"));
+
+        ArrayList<SampleUpdatesDTO> sampleUpdates = new ArrayList<SampleUpdatesDTO>();
+
+        ArrayList<NewSample> sampleRegistrations = new ArrayList<NewSample>();
+        sampleRegistrations.add(new NewSample("/SPACE/SAMPLE-ID1", new SampleType(), null, null,
+                "/SPACE/PROJECT/EXP-ID1", new IEntityProperty[0], new ArrayList<NewAttachment>()));
+        sampleRegistrations.add(new NewSample("/SPACE/SAMPLE-ID2", new SampleType(), null, null,
+                "/SPACE/PROJECT/EXP-ID1", new IEntityProperty[0], new ArrayList<NewAttachment>()));
+
+        ArrayList<NewExternalData> dataSetRegistrations = new ArrayList<NewExternalData>();
+        NewExternalData newExternalData = new NewExternalData();
+        newExternalData.setCode("DATA-SET-CODE");
+        newExternalData.setSampleIdentifierOrNull(new SampleIdentifierFactory("/SPACE/SAMPLE-ID1")
+                .createIdentifier());
+        dataSetRegistrations.add(newExternalData);
+
+        AtomicEntityOperationDetails details =
+                new AtomicEntityOperationDetails(experimentRegistrations, sampleUpdates,
+                        sampleRegistrations, dataSetRegistrations);
+
+        assertEquals(
+                "AtomicEntityOperationDetails[experimentUpdates=[]"
+                        + ",experimentRegistrations=[/SPACE/PROJECT/EXP-ID1, /SPACE/PROJECT/EXP-ID2]"
+                        + ",sampleUpdates=[]"
+                        + ",sampleRegistrations=[/SPACE/SAMPLE-ID1, /SPACE/SAMPLE-ID2]"
+                        + ",dataSetRegistrations=[NewExternalData[code=DATA-SET-CODE,type=<null>,fileFormat=<null>,properties=[]]]]",
+                details.toString());
+
+    }
+}
-- 
GitLab