From 0ece420e663f5f8c6a43aadf84722687d0bc090f Mon Sep 17 00:00:00 2001
From: felmer <felmer>
Date: Wed, 31 Oct 2012 09:24:30 +0000
Subject: [PATCH] SP-357, BIS-242: Project: Using new version column as
 Hibernate version column. Code added to update modifier and modification
 date. ProjectOptimisticLockingTest introduced to test this, including of
 adding several experiments in two threads "at the same time".

SVN: 27430
---
 .../web/client/ICommonClientService.java      |   3 +-
 .../web/client/ICommonClientServiceAsync.java |   3 +-
 .../ui/project/ProjectEditForm.java           |  12 +-
 .../web/server/CommonClientService.java       |  14 +-
 .../openbis/generic/server/CommonServer.java  |   4 +-
 .../generic/server/CommonServerLogger.java    |   4 +-
 .../server/business/RelationshipService.java  |  16 ++
 .../server/business/bo/ExperimentBO.java      |  25 +-
 .../server/business/bo/ExperimentTable.java   |   1 +
 .../generic/server/business/bo/ProjectBO.java |   2 +-
 .../server/dataaccess/db/ProjectDAO.java      |   2 +
 .../openbis/generic/shared/ICommonServer.java |   2 +-
 .../basic/dto/AbstractProjectUpdates.java     |   7 +-
 ...deWithRegistrationAndModificationDate.java |  12 +
 .../openbis/generic/shared/dto/ProjectPE.java |  19 +-
 .../shared/translator/ProjectTranslator.java  |   1 +
 .../server/business/bo/ExperimentBOTest.java  |  48 +++-
 .../server/util/TimeIntervalChecker.java      |  65 +++++
 .../generic/shared/CommonTestUtils.java       |   3 +
 .../AtomicEntityOperationDetailsBuilder.java  | 103 +++++++
 .../systemtest/PersistentSystemTestCase.java  | 262 ++++++++++++++++++
 .../base/builder/ProjectUpdateBuilder.java    |   2 +-
 .../OptimisticLockingTestCase.java            | 197 +++++++++++++
 .../ProjectOptimisticLockingTest.java         | 262 ++++++++++++++++++
 24 files changed, 1007 insertions(+), 62 deletions(-)
 create mode 100644 openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/util/TimeIntervalChecker.java
 create mode 100644 openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/dto/builders/AtomicEntityOperationDetailsBuilder.java
 create mode 100644 openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/PersistentSystemTestCase.java
 create mode 100644 openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/OptimisticLockingTestCase.java
 create mode 100644 openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/ProjectOptimisticLockingTest.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java
index a966d924ac0..dec028453e7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java
@@ -16,7 +16,6 @@
 
 package ch.systemsx.cisd.openbis.generic.client.web.client;
 
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -664,7 +663,7 @@ public interface ICommonClientService extends IClientService
     /**
      * Updates project.
      */
-    public Date updateProject(ProjectUpdates updates) throws UserFailureException;
+    public int updateProject(ProjectUpdates updates) throws UserFailureException;
 
     /** Deletes/Trashes the specified data sets. */
     public void deleteDataSets(
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java
index 6a6f409f265..f7038879ee2 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java
@@ -16,7 +16,6 @@
 
 package ch.systemsx.cisd.openbis.generic.client.web.client;
 
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -696,7 +695,7 @@ public interface ICommonClientServiceAsync extends IClientServiceAsync
     /**
      * @see ICommonClientService#updateProject(ProjectUpdates)
      */
-    public void updateProject(ProjectUpdates updates, AsyncCallback<Date> projectEditCallback);
+    public void updateProject(ProjectUpdates updates, AsyncCallback<Integer> projectEditCallback);
 
     /**
      * @see ICommonClientService#deleteEntityTypes(EntityKind, List)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/project/ProjectEditForm.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/project/ProjectEditForm.java
index eb3bf270383..e3ee1c7aeef 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/project/ProjectEditForm.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/project/ProjectEditForm.java
@@ -16,8 +16,6 @@
 
 package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.project;
 
-import java.util.Date;
-
 import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAsync;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.AbstractAsyncCallback;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
@@ -63,7 +61,7 @@ public class ProjectEditForm extends AbstractProjectEditRegisterForm
         updates.setAttachmentSessionKey(sessionKey);
         updates.setDescription(projectDescriptionField.getValue());
         updates.setTechId(projectId);
-        updates.setVersion(originalProject.getModificationDate());
+        updates.setVersion(originalProject.getVersion());
         Space space = spaceField.tryGetSelected();
         updates.setGroupCode(space == null ? null : space.getCode());
 
@@ -71,7 +69,7 @@ public class ProjectEditForm extends AbstractProjectEditRegisterForm
     }
 
     private final class ProjectEditCallback extends
-            AbstractRegistrationForm.AbstractRegistrationCallback<Date>
+            AbstractRegistrationForm.AbstractRegistrationCallback<Integer>
     {
 
         ProjectEditCallback(final IViewContext<?> viewContext)
@@ -80,15 +78,15 @@ public class ProjectEditForm extends AbstractProjectEditRegisterForm
         }
 
         @Override
-        protected void process(final Date result)
+        protected void process(final Integer result)
         {
-            originalProject.setModificationDate(result);
+            originalProject.setVersion(result);
             updateOriginalValues();
             super.process(result);
         }
 
         @Override
-        protected String createSuccessfullRegistrationInfo(Date result)
+        protected String createSuccessfullRegistrationInfo(Integer result)
         {
             return "Project <b>" + originalProject.getCode() + "</b> successfully updated.";
         }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
index a27db3d39ab..7297eedad01 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
@@ -21,11 +21,11 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.http.HttpSession;
 
@@ -42,9 +42,9 @@ import ch.systemsx.cisd.common.parser.IPropertyMapper;
 import ch.systemsx.cisd.common.parser.ParserException;
 import ch.systemsx.cisd.common.reflection.BeanUtils;
 import ch.systemsx.cisd.common.servlet.IRequestContextProvider;
-import ch.systemsx.cisd.openbis.common.spring.IUncheckedMultipartFile;
 import ch.systemsx.cisd.common.string.ReflectingStringUnescaper;
 import ch.systemsx.cisd.common.string.UnicodeUtils;
+import ch.systemsx.cisd.openbis.common.spring.IUncheckedMultipartFile;
 import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientService;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ArchivingResult;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DataSetUploadParameters;
@@ -1638,11 +1638,11 @@ public final class CommonClientService extends AbstractClientService implements
     }
 
     @Override
-    public Date updateProject(final ProjectUpdates updates)
+    public int updateProject(final ProjectUpdates updates)
             throws ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException
     {
         final String sessionToken = getSessionToken();
-        final Date modificationDate = new Date();
+        final AtomicInteger version = new AtomicInteger();
         new AttachmentRegistrationHelper()
             {
                 @Override
@@ -1650,11 +1650,11 @@ public final class CommonClientService extends AbstractClientService implements
                 {
                     ProjectUpdatesDTO updatesDTO = translate(updates);
                     updatesDTO.setAttachments(attachments);
-                    Date date = commonServer.updateProject(sessionToken, updatesDTO);
-                    modificationDate.setTime(date.getTime());
+                    int versionNumber = commonServer.updateProject(sessionToken, updatesDTO);
+                    version.set(versionNumber);
                 }
             }.process(updates.getAttachmentSessionKey(), getHttpSession(), updates.getAttachments());
-        return modificationDate;
+        return version.get();
     }
 
     private static ProjectUpdatesDTO translate(ProjectUpdates updates)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
index 57b89f1b15a..f904bfd8703 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
@@ -2192,7 +2192,7 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
     @Override
     @RolesAllowed(RoleWithHierarchy.SPACE_POWER_USER)
     @Capability("WRITE_PROJECT")
-    public Date updateProject(String sessionToken,
+    public int updateProject(String sessionToken,
             @AuthorizationGuard(guardClass = ProjectUpdatesPredicate.class)
             ProjectUpdatesDTO updates)
     {
@@ -2200,7 +2200,7 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         final IProjectBO bo = businessObjectFactory.createProjectBO(session);
         bo.update(updates);
         bo.save();
-        return bo.getProject().getModificationDate();
+        return bo.getProject().getVersion();
     }
 
     private void deleteEntityTypes(String sessionToken, EntityKind entityKind, List<String> codes)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java
index a729604eda6..0faff1e3851 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java
@@ -957,11 +957,11 @@ final class CommonServerLogger extends AbstractServerLogger implements ICommonSe
     }
 
     @Override
-    public Date updateProject(String sessionToken, ProjectUpdatesDTO updates)
+    public int updateProject(String sessionToken, ProjectUpdatesDTO updates)
     {
         logTracking(sessionToken, "edit_project", "PROJECT_ID(%s) ATTACHMENTS_ADDED(%s)",
                 updates.getTechId(), updates.getAttachments().size());
-        return null;
+        return 0;
     }
 
     @Override
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/RelationshipService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/RelationshipService.java
index 23ef8dffcc6..c2780d326da 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/RelationshipService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/RelationshipService.java
@@ -16,6 +16,8 @@
 
 package ch.systemsx.cisd.openbis.generic.server.business;
 
+import java.util.Date;
+
 import javax.annotation.Resource;
 
 import ch.systemsx.cisd.common.exception.UserFailureException;
@@ -60,7 +62,21 @@ public class RelationshipService implements IRelationshipService
             ProjectPE project)
     {
         SampleUtils.setSamplesSpace(experiment, project.getSpace());
+        PersonPE modifier = experiment.getModifier();
+        ProjectPE previousProject = experiment.getProject();
+        setModifierAndModificationDate(previousProject, modifier);
         experiment.setProject(project);
+        setModifierAndModificationDate(project, modifier);
+    }
+
+    private void setModifierAndModificationDate(ProjectPE projectOrNull, PersonPE modifier)
+    {
+        if (projectOrNull == null)
+        {
+            return;
+        }
+        projectOrNull.setModifier(modifier);
+        projectOrNull.setModificationDate(new Date());
     }
 
     @Override
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBO.java
index 3c9780d7a3f..47cedb4968b 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBO.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBO.java
@@ -19,6 +19,7 @@ package ch.systemsx.cisd.openbis.generic.server.business.bo;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -129,8 +130,8 @@ public final class ExperimentBO extends AbstractBusinessObject implements IExper
         }
     }
 
-    private static final String PROPERTY_TYPES =
-            "experimentType.experimentTypePropertyTypesInternal";
+    @Private
+    static final String PROPERTY_TYPES = "experimentType.experimentTypePropertyTypesInternal";
 
     @Override
     public void loadDataByTechId(TechId experimentId)
@@ -368,6 +369,8 @@ public final class ExperimentBO extends AbstractBusinessObject implements IExper
         {
             throw UserFailureException.fromTemplate(ERR_PROJECT_NOT_FOUND, newExperiment);
         }
+        project.setModificationDate(new Date());
+        project.setModifier(findPerson());
         experiment.setProject(project);
     }
 
@@ -453,6 +456,7 @@ public final class ExperimentBO extends AbstractBusinessObject implements IExper
         {
             throwModifiedEntityException("Experiment");
         }
+        experiment.setModifier(findPerson());
         updateProperties(updates.getProperties());
 
         ProjectPE project = findProject(updates.getProjectIdentifier());
@@ -594,23 +598,6 @@ public final class ExperimentBO extends AbstractBusinessObject implements IExper
         return new HashSet<String>(Arrays.asList(objects));
     }
 
-    @Private
-    void updateProject(ProjectIdentifier newProjectIdentifier)
-    {
-        ProjectPE project = findProject(newProjectIdentifier);
-        ProjectPE previousProject = experiment.getProject();
-        if (project.equals(previousProject))
-        {
-            return; // nothing to change
-        }
-        // if the group has changes, move all samples to that group
-        if (project.getSpace().equals(previousProject.getSpace()) == false)
-        {
-            SampleUtils.setSamplesSpace(experiment, project.getSpace());
-        }
-        experiment.setProject(project);
-    }
-
     private ProjectPE findProject(ProjectIdentifier newProjectIdentifier)
     {
         ProjectPE project =
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentTable.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentTable.java
index fcaea658d75..1f3623aa627 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentTable.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentTable.java
@@ -245,6 +245,7 @@ public final class ExperimentTable extends AbstractBusinessObject implements IEx
                     "No experiment could be found with given identifier '%s'.",
                     updates.getOldExperimentIdentifier());
         }
+        experiment.setModifier(findPerson());
         ExperimentBatchUpdateDetails details = updates.getDetails();
         batchUpdateProperties(experiment, updates.getProperties(), details.getPropertiesToUpdate());
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ProjectBO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ProjectBO.java
index 69ef4e3b317..aa26f27ec14 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ProjectBO.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ProjectBO.java
@@ -288,7 +288,7 @@ public final class ProjectBO extends AbstractBusinessObject implements IProjectB
     public void update(ProjectUpdatesDTO updates)
     {
         loadDataByTechId(updates.getTechId());
-        if (updates.getVersion().equals(project.getModificationDate()) == false)
+        if (updates.getVersion() != project.getVersion())
         {
             throwModifiedEntityException("Project");
         }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/ProjectDAO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/ProjectDAO.java
index b9947b9c42d..9fb79de8ec7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/ProjectDAO.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/db/ProjectDAO.java
@@ -16,6 +16,7 @@
 
 package ch.systemsx.cisd.openbis.generic.server.dataaccess.db;
 
+import java.util.Date;
 import java.util.List;
 
 import org.apache.commons.lang.StringUtils;
@@ -125,6 +126,7 @@ public class ProjectDAO extends AbstractGenericEntityDAO<ProjectPE> implements I
 
         project.setCode(CodeConverter.tryToDatabase(project.getCode()));
         project.setModifier(modifier);
+        project.setModificationDate(new Date());
         final HibernateTemplate template = getHibernateTemplate();
         template.saveOrUpdate(project);
         template.flush();
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java
index e8da4b03115..aea34723bfe 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java
@@ -878,7 +878,7 @@ public interface ICommonServer extends IServer
      */
     @Transactional
     @DatabaseUpdateModification(value = ObjectKind.PROJECT)
-    public Date updateProject(String sessionToken, ProjectUpdatesDTO updates);
+    public int updateProject(String sessionToken, ProjectUpdatesDTO updates);
 
     /**
      * Deletes specified data set types.
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AbstractProjectUpdates.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AbstractProjectUpdates.java
index 0151561be24..bad71532d12 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AbstractProjectUpdates.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/AbstractProjectUpdates.java
@@ -17,7 +17,6 @@
 package ch.systemsx.cisd.openbis.generic.shared.basic.dto;
 
 import java.io.Serializable;
-import java.util.Date;
 
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 
@@ -31,7 +30,7 @@ public class AbstractProjectUpdates implements Serializable
 
     private static final long serialVersionUID = ServiceVersionHolder.VERSION;
 
-    private Date version;
+    private int version;
 
     private TechId id;
 
@@ -52,12 +51,12 @@ public class AbstractProjectUpdates implements Serializable
         this.groupCodeOrNull = groupCode;
     }
 
-    public Date getVersion()
+    public int getVersion()
     {
         return version;
     }
 
-    public void setVersion(Date version)
+    public void setVersion(int version)
     {
         this.version = version;
     }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/CodeWithRegistrationAndModificationDate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/CodeWithRegistrationAndModificationDate.java
index 6b2f25944ea..bfe5767ef4d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/CodeWithRegistrationAndModificationDate.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/basic/dto/CodeWithRegistrationAndModificationDate.java
@@ -30,6 +30,8 @@ public class CodeWithRegistrationAndModificationDate<T extends CodeWithRegistrat
 
     private Date modificationDate;
 
+    private int version;
+
     private Person modifier;
 
     public Date getModificationDate()
@@ -52,4 +54,14 @@ public class CodeWithRegistrationAndModificationDate<T extends CodeWithRegistrat
     {
         this.modifier = modifier;
     }
+
+    public int getVersion()
+    {
+        return version;
+    }
+
+    public void setVersion(int version)
+    {
+        this.version = version;
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java
index 946d0825c9d..73b3a47a521 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ProjectPE.java
@@ -47,6 +47,7 @@ import org.hibernate.annotations.Fetch;
 import org.hibernate.annotations.FetchMode;
 import org.hibernate.annotations.Generated;
 import org.hibernate.annotations.GenerationTime;
+import org.hibernate.annotations.OptimisticLock;
 import org.hibernate.search.annotations.ContainedIn;
 import org.hibernate.search.annotations.Field;
 import org.hibernate.search.annotations.Index;
@@ -110,6 +111,8 @@ public final class ProjectPE extends AttachmentHolderPE implements Comparable<Pr
 
     private Date modificationDate;
 
+    private int version;
+
     @Column(name = ColumnNames.REGISTRATION_TIMESTAMP_COLUMN, nullable = false, insertable = false, updatable = false)
     @Generated(GenerationTime.INSERT)
     public Date getRegistrationDate()
@@ -134,6 +137,7 @@ public final class ProjectPE extends AttachmentHolderPE implements Comparable<Pr
         this.registrator = registrator;
     }
 
+    @OptimisticLock(excluded = true)
     @ManyToOne(fetch = FetchType.EAGER)
     @JoinColumn(name = ColumnNames.PERSON_MODIFIER_COLUMN)
     public PersonPE getModifier()
@@ -168,6 +172,7 @@ public final class ProjectPE extends AttachmentHolderPE implements Comparable<Pr
         return space;
     }
 
+    @OptimisticLock(excluded = true)
     @OneToMany(fetch = FetchType.LAZY, mappedBy = "projectInternal")
     @ContainedIn
     private List<ExperimentPE> getExperimentsInternal()
@@ -374,7 +379,7 @@ public final class ProjectPE extends AttachmentHolderPE implements Comparable<Pr
         return projectIdentifier.toString();
     }
 
-    @Version
+    @OptimisticLock(excluded = true)
     @Column(name = ColumnNames.MODIFICATION_TIMESTAMP_COLUMN, nullable = false)
     public Date getModificationDate()
     {
@@ -386,4 +391,16 @@ public final class ProjectPE extends AttachmentHolderPE implements Comparable<Pr
         this.modificationDate = versionDate;
     }
 
+    @Version
+    @Column(name = ColumnNames.VERSION_COLUMN, nullable = false)
+    public int getVersion()
+    {
+        return version;
+    }
+
+    public void setVersion(int version)
+    {
+        this.version = version;
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java
index a3143301c5f..d96a023420c 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/translator/ProjectTranslator.java
@@ -61,6 +61,7 @@ public final class ProjectTranslator
         result.setPermId(project.getPermId());
         result.setModifier(PersonTranslator.translate(project.getModifier()));
         result.setModificationDate(project.getModificationDate());
+        result.setVersion(project.getVersion());
         result.setCode(project.getCode());
         result.setDescription(project.getDescription());
         result.setSpace(SpaceTranslator.translate(project.getSpace()));
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBOTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBOTest.java
index 6c862130bae..c8795a3f949 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBOTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/business/bo/ExperimentBOTest.java
@@ -38,6 +38,7 @@ import ch.systemsx.cisd.openbis.generic.server.business.ManagerTestTool;
 import ch.systemsx.cisd.openbis.generic.shared.CommonTestUtils;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
 import ch.systemsx.cisd.openbis.generic.shared.dto.AttachmentPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE;
@@ -47,6 +48,7 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPropertyPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentTypePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentTypePropertyTypePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentUpdatesDTO;
 import ch.systemsx.cisd.openbis.generic.shared.dto.IAuthSession;
 import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
@@ -448,29 +450,49 @@ public final class ExperimentBOTest extends AbstractBOTest
     {
 
         ExperimentIdentifier identifier = CommonTestUtils.createExperimentIdentifier();
-        ExperimentPE exp = CommonTestUtils.createExperiment(identifier);
-
-        SpacePE group = CommonTestUtils.createSpace(identifier);
+        ExperimentTypePE experimentType = CommonTestUtils.createExperimentType();
+        final ExperimentPE exp = CommonTestUtils.createExperiment(identifier);
+        exp.setExperimentType(experimentType);
+        SpacePE space = CommonTestUtils.createSpace(identifier);
         SamplePE assignedSample = createSampleWithCode("assignedSample");
-        assignedSample.setSpace(group);
+        assignedSample.setSpace(space);
         exp.setSamples(Arrays.asList(assignedSample));
-
-        prepareLoadExperimentByIdentifier(identifier, exp);
-        ExperimentBO expBO = loadExperiment(identifier, exp);
-
+        // prepareLoadExperimentByIdentifier(identifier, exp);
         final ProjectIdentifier newProjectIdentifier =
-                new ProjectIdentifier(identifier.getDatabaseInstanceCode(), "anotherGroup",
+                new ProjectIdentifier(identifier.getDatabaseInstanceCode(), "anotherSpace",
                         "anotherProject");
         final ProjectPE newProject = CommonTestUtils.createProject(newProjectIdentifier);
+        ExperimentUpdatesDTO updates = new ExperimentUpdatesDTO();
+        updates.setExperimentId(new TechId(exp));
+        updates.setVersion(exp.getModificationDate());
+        updates.setProjectIdentifier(newProjectIdentifier);
+        updates.setProperties(Collections.<IEntityProperty> emptyList());
+        updates.setAttachments(Collections.<NewAttachment> emptyList());
+        prepareAnyDaoCreation();
+        context.checking(new Expectations()
+            {
+                {
+                    one(experimentDAO).tryGetByTechId(new TechId(exp.getId()),
+                            ExperimentBO.PROPERTY_TYPES);
+                    will(returnValue(exp));
+
+                    one(entityTypeDAO).listEntityTypes();
+                    will(returnValue(Arrays.asList(exp.getExperimentType())));
+
+                    allowing(entityPropertyTypeDAO)
+                            .listEntityPropertyTypes(exp.getExperimentType());
+                    one(relationshipService).assignExperimentToProject(
+                            ManagerTestTool.EXAMPLE_SESSION, exp, newProject);
+                }
+            });
         prepareTryFindProject(newProjectIdentifier, newProject);
 
+        ExperimentBO expBO = createExperimentBO();
+        expBO.update(updates);
+
         assertFalse(newProject.equals(exp.getProject()));
         assertFalse(newProject.getSpace().equals(assignedSample.getSpace()));
 
-        expBO.updateProject(newProjectIdentifier);
-
-        assertEquals(newProject, exp.getProject());
-        assertEquals(newProject.getSpace(), assignedSample.getSpace());
     }
 
     @Test
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/util/TimeIntervalChecker.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/util/TimeIntervalChecker.java
new file mode 100644
index 00000000000..8c26dfcb61f
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/util/TimeIntervalChecker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012 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.server.util;
+
+import java.util.Date;
+
+import org.testng.AssertJUnit;
+
+/**
+ * Helper class to check that a time stamp is between now and a time in the past when an instance of
+ * this class has been created.
+ * <p>
+ * This class is useful in tests where some productive code creates a time stamp which should be
+ * checked.
+ * 
+ * @author Franz-Josef Elmer
+ */
+public class TimeIntervalChecker extends AssertJUnit
+{
+    private Date notBeforeDate;
+
+    /**
+     * Creates an instance for now.
+     */
+    public TimeIntervalChecker()
+    {
+        this(0);
+    }
+
+    /**
+     * Creates an instance for now minus specified shift in seconds.
+     */
+    public TimeIntervalChecker(long shiftInSeconds)
+    {
+        notBeforeDate = new Date(System.currentTimeMillis() - shiftInSeconds * 1000);
+    }
+
+    /**
+     * Asserts that the specified date is after the time stamp of creation of this instance and
+     * before now.
+     */
+    public void assertDateInInterval(Date date)
+    {
+        assertTrue("Actual date [" + date + "] is before notBeforeDate [" + notBeforeDate + "].",
+                notBeforeDate.getTime() <= date.getTime());
+        Date now = new Date();
+        assertTrue("Actual date [" + date + "] is after now [" + now + "].",
+                now.getTime() >= date.getTime());
+
+    }
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/CommonTestUtils.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/CommonTestUtils.java
index eb411e6add9..dca8068f337 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/CommonTestUtils.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/CommonTestUtils.java
@@ -19,6 +19,7 @@ package ch.systemsx.cisd.openbis.generic.shared;
 import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.List;
 
 import org.apache.commons.io.FileUtils;
@@ -345,10 +346,12 @@ public class CommonTestUtils
         final ExperimentPE exp = new ExperimentPE();
         final ExperimentTypePE expType = new ExperimentTypePE();
         expType.setCode("expType");
+        exp.setId(42L);
         exp.setExperimentType(expType);
         exp.setCode(ei.getExperimentCode());
         exp.setProject(createProject(new ProjectIdentifier(ei.getDatabaseInstanceCode(), ei
                 .getSpaceCode(), ei.getProjectCode())));
+        exp.setModificationDate(new Date(4711L));
         return exp;
     }
 
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/dto/builders/AtomicEntityOperationDetailsBuilder.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/dto/builders/AtomicEntityOperationDetailsBuilder.java
new file mode 100644
index 00000000000..88d60832781
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/dto/builders/AtomicEntityOperationDetailsBuilder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2012 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.dto.builders;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewMaterial;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewProject;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewSample;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewSpace;
+import ch.systemsx.cisd.openbis.generic.shared.dto.AtomicEntityOperationDetails;
+import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetBatchUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentUpdatesDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MaterialUpdateDTO;
+import ch.systemsx.cisd.openbis.generic.shared.dto.NewExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class AtomicEntityOperationDetailsBuilder
+{
+    private final List<ExperimentUpdatesDTO> experimentUpdates =
+            new ArrayList<ExperimentUpdatesDTO>();
+
+    private final List<NewSpace> spaceRegistrations = new ArrayList<NewSpace>();
+
+    private final List<NewProject> projectRegistrations = new ArrayList<NewProject>();
+
+    private final List<NewExperiment> experimentRegistrations = new ArrayList<NewExperiment>();
+
+    private final List<SampleUpdatesDTO> sampleUpdates = new ArrayList<SampleUpdatesDTO>();
+
+    private final List<NewSample> sampleRegistrations = new ArrayList<NewSample>();
+
+    private final Map<String /* material type */, List<NewMaterial>> materialRegistrations =
+            new HashMap<String, List<NewMaterial>>();
+
+    private final List<MaterialUpdateDTO> materialUpdates = new ArrayList<MaterialUpdateDTO>();
+
+    private final List<? extends NewExternalData> dataSetRegistrations =
+            new ArrayList<NewExternalData>();
+
+    private final List<DataSetBatchUpdatesDTO> dataSetUpdates =
+            new ArrayList<DataSetBatchUpdatesDTO>();
+
+    private TechId registrationIdOrNull;
+
+    private String userIdOrNull;
+
+    private Integer batchSizeOrNull;
+
+    public AtomicEntityOperationDetailsBuilder user(String userId)
+    {
+        userIdOrNull = userId;
+        return this;
+    }
+
+    public AtomicEntityOperationDetailsBuilder batchSize(int size)
+    {
+        batchSizeOrNull = size;
+        return this;
+    }
+
+    public AtomicEntityOperationDetailsBuilder addNewExperiment(NewExperiment newExperiment)
+    {
+        experimentRegistrations.add(newExperiment);
+        return this;
+    }
+
+    public AtomicEntityOperationDetailsBuilder addNewSample(NewSample newSample)
+    {
+        sampleRegistrations.add(newSample);
+        return this;
+    }
+
+    public AtomicEntityOperationDetails getDetails()
+    {
+        return new AtomicEntityOperationDetails(registrationIdOrNull, userIdOrNull,
+                spaceRegistrations, projectRegistrations, experimentRegistrations,
+                experimentUpdates, sampleUpdates, sampleRegistrations, materialRegistrations,
+                materialUpdates, dataSetRegistrations, dataSetUpdates, batchSizeOrNull);
+    }
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/PersistentSystemTestCase.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/PersistentSystemTestCase.java
new file mode 100644
index 00000000000..e22470fe2bf
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/PersistentSystemTestCase.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2009 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.systemtest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpSession;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+import org.testng.AssertJUnit;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeSuite;
+
+import ch.systemsx.cisd.common.servlet.SpringRequestContextProvider;
+import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientService;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.SessionContext;
+import ch.systemsx.cisd.openbis.generic.client.web.server.UploadedFilesBean;
+import ch.systemsx.cisd.openbis.generic.server.ICommonServerForInternalUse;
+import ch.systemsx.cisd.openbis.generic.server.util.TestInitializer;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DisplaySettings;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Grantee;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewSample;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy.RoleCode;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SampleType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.builders.PropertyBuilder;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SpaceIdentifier;
+import ch.systemsx.cisd.openbis.plugin.generic.client.web.client.IGenericClientService;
+import ch.systemsx.cisd.openbis.plugin.generic.shared.IGenericServer;
+
+/**
+ * Abstract super class of head-less system tests which makes database changes persistent. Test
+ * classes extending this test case class are responsible for cleaning up the database.
+ * 
+ * @author Franz-Josef Elmer
+ */
+@ContextConfiguration(locations = "classpath:applicationContext.xml")
+public abstract class PersistentSystemTestCase extends AbstractTestNGSpringContextTests
+{
+    protected static final String SESSION_KEY = "session-key";
+
+    protected ICommonServerForInternalUse commonServer;
+
+    protected IGenericServer genericServer;
+
+    protected ICommonClientService commonClientService;
+
+    protected IGenericClientService genericClientService;
+
+    protected IETLLIMSService etlService;
+
+    protected MockHttpServletRequest request;
+
+    protected String systemSessionToken;
+
+    @BeforeSuite
+    public void beforeSuite()
+    {
+        TestInitializer.init();
+    }
+
+    @BeforeClass
+    public void loginAsSystem()
+    {
+        systemSessionToken = commonServer.tryToAuthenticateAsSystem().getSessionToken();
+    }
+
+    /**
+     * Sets a {@link MockHttpServletRequest} for the specified context provider
+     */
+    @Autowired
+    public final void setRequestContextProvider(final SpringRequestContextProvider contextProvider)
+    {
+        request = new MockHttpServletRequest();
+        contextProvider.setRequest(request);
+    }
+
+    /**
+     * Sets <code>commonServer</code>.
+     * <p>
+     * Will be automatically dependency injected by type.
+     * </p>
+     */
+    @Autowired
+    public final void setCommonServer(final ICommonServerForInternalUse commonServer)
+    {
+        this.commonServer = commonServer;
+    }
+
+    /**
+     * Sets <code>genericServer</code>.
+     * <p>
+     * Will be automatically dependency injected by type.
+     * </p>
+     */
+    @Autowired
+    public final void setGenericServer(final IGenericServer genericServer)
+    {
+        this.genericServer = genericServer;
+    }
+
+    /**
+     * Sets <code>commonClientService</code>.
+     * <p>
+     * Will be automatically dependency injected by type.
+     * </p>
+     */
+    @Autowired
+    public final void setCommonClientService(final ICommonClientService commonClientService)
+    {
+        this.commonClientService = commonClientService;
+    }
+
+    /**
+     * Sets <code>genericClientService</code>.
+     * <p>
+     * Will be automatically dependency injected by type.
+     * </p>
+     */
+    @Autowired
+    public final void setGenericClientService(final IGenericClientService genericClientService)
+    {
+        this.genericClientService = genericClientService;
+    }
+
+    @Autowired
+    public void setETLService(IETLLIMSService etlService)
+    {
+        this.etlService = etlService;
+
+    }
+
+    protected SessionContext logIntoCommonClientService()
+    {
+        SessionContext context = commonClientService.tryToLogin("test", "a");
+        AssertJUnit.assertNotNull(context);
+        return context;
+    }
+
+    protected void logOutFromCommonClientService()
+    {
+        commonClientService.logout(new DisplaySettings(), false);
+    }
+
+    protected void sleep(long millis)
+    {
+        try
+        {
+            Thread.sleep(millis);
+        } catch (InterruptedException ex)
+        {
+            ex.printStackTrace();
+        }
+    }
+
+    public final class NewSampleBuilder
+    {
+        private NewSample sample = new NewSample();
+
+        private List<IEntityProperty> propertis = new ArrayList<IEntityProperty>();
+
+        public NewSampleBuilder(String identifier)
+        {
+            sample.setIdentifier(identifier);
+        }
+
+        public NewSampleBuilder type(String type)
+        {
+            SampleType sampleType = new SampleType();
+            sampleType.setCode(type);
+            sample.setSampleType(sampleType);
+            return this;
+        }
+
+        public NewSampleBuilder experiment(String identifier)
+        {
+            sample.setExperimentIdentifier(identifier);
+            return this;
+        }
+
+        public NewSampleBuilder parents(String... parentIdentifiers)
+        {
+            sample.setParentsOrNull(parentIdentifiers);
+            return this;
+        }
+
+        public NewSampleBuilder property(String key, String value)
+        {
+            propertis.add(new PropertyBuilder(key).value(value).getProperty());
+            return this;
+        }
+
+        public void register()
+        {
+            sample.setProperties(propertis.toArray(new IEntityProperty[propertis.size()]));
+            genericClientService.registerSample(SESSION_KEY, sample);
+        }
+    }
+
+    /**
+     * Register a person with specified user ID.
+     * 
+     * @return userID
+     */
+    protected String registerPerson(String userID)
+    {
+        commonServer.registerPerson(systemSessionToken, userID);
+        return userID;
+    }
+
+    protected void assignInstanceRole(String userID, RoleCode roleCode)
+    {
+        commonServer.registerInstanceRole(systemSessionToken, roleCode,
+                Grantee.createPerson(userID));
+    }
+
+    protected void assignSpaceRole(String userID, RoleCode roleCode, SpaceIdentifier spaceIdentifier)
+    {
+        commonServer.registerSpaceRole(systemSessionToken, roleCode, spaceIdentifier,
+                Grantee.createPerson(userID));
+    }
+
+    /**
+     * Authenticates as specified user.
+     * 
+     * @return session token
+     */
+    protected String authenticateAs(String user)
+    {
+        return commonServer.tryToAuthenticate(user, "password").getSessionToken();
+    }
+
+    protected void uploadFile(String fileName, String fileContent)
+    {
+        UploadedFilesBean bean = new UploadedFilesBean();
+        bean.addMultipartFile(new MockMultipartFile(fileName, fileName, null, fileContent
+                .getBytes()));
+        HttpSession session = request.getSession();
+        session.setAttribute(SESSION_KEY, bean);
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/base/builder/ProjectUpdateBuilder.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/base/builder/ProjectUpdateBuilder.java
index ef7b365cd66..b7bf969f2bc 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/base/builder/ProjectUpdateBuilder.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/base/builder/ProjectUpdateBuilder.java
@@ -39,7 +39,7 @@ public class ProjectUpdateBuilder extends UpdateBuilder<ProjectUpdatesDTO>
         updates.setAttachments(new ArrayList<NewAttachment>());
         updates.setDescription(project.getDescription());
         updates.setTechId(new TechId(project.getId()));
-        updates.setVersion(project.getModificationDate());
+        updates.setVersion(project.getVersion());
     }
 
     public ProjectUpdateBuilder toSpace(Space space)
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/OptimisticLockingTestCase.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/OptimisticLockingTestCase.java
new file mode 100644
index 00000000000..5f5bfa2df58
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/OptimisticLockingTestCase.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2012 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.systemtest.optimistic_locking;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.ICodeHolder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Deletion;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DeletionType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Grantee;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Person;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Project;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy.RoleCode;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Space;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.builders.ExperimentTypeBuilder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.builders.PropertyBuilder;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.DatabaseInstanceIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifierFactory;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SpaceIdentifierFactory;
+import ch.systemsx.cisd.openbis.systemtest.PersistentSystemTestCase;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class OptimisticLockingTestCase extends PersistentSystemTestCase
+{
+    protected static final String USER_ID = "optimist";
+
+    protected static final String SPACE_1 = "OPTIMISTIC_LOCKING_1";
+
+    protected static final String SPACE_2 = "OPTIMISTIC_LOCKING_2";
+
+    protected static final String EXPERIMENT_TYPE_CODE = "SIRNA_HCS";
+
+    protected Space space1;
+
+    protected Space space2;
+
+    protected Project project1;
+
+    protected Project project2;
+
+    @BeforeMethod
+    public void createSpacesAndProjects()
+    {
+        space1 = findOrCreateSpace(SPACE_1);
+        space2 = findOrCreateSpace(SPACE_2);
+        project1 = findOrCreateProject("/" + SPACE_1 + "/P1");
+        project2 = findOrCreateProject("/" + SPACE_2 + "/P2");
+        createInstanceAdmin(USER_ID);
+    }
+
+    @AfterMethod
+    public void deleteSpaces()
+    {
+        deleteSpace(space1);
+        deleteSpace(space2);
+    }
+
+    private void deleteSpace(Space space)
+    {
+        List<Experiment> experiments =
+                commonServer.listExperiments(systemSessionToken,
+                        new ExperimentTypeBuilder().code(EXPERIMENT_TYPE_CODE).getExperimentType(),
+                        new SpaceIdentifierFactory(space.getIdentifier()).createIdentifier());
+        commonServer.deleteExperiments(systemSessionToken, TechId.createList(experiments),
+                "cleanup", DeletionType.TRASH);
+        List<Deletion> deletions = commonServer.listDeletions(systemSessionToken, false);
+        commonServer.deletePermanently(systemSessionToken, TechId.createList(deletions));
+        List<Project> projects = commonServer.listProjects(systemSessionToken);
+        List<TechId> projectIds = new ArrayList<TechId>();
+        for (Project project : projects)
+        {
+            if (project.getSpace().getCode().equals(space.getCode()))
+            {
+                projectIds.add(new TechId(project));
+            }
+        }
+        commonServer.deleteProjects(systemSessionToken, projectIds, "cleanup");
+        commonServer.deleteSpaces(systemSessionToken, Arrays.asList(new TechId(space.getId())),
+                "cleanup");
+    }
+
+    private void createInstanceAdmin(String userId)
+    {
+        List<Person> persons = commonServer.listPersons(systemSessionToken);
+        for (Person person : persons)
+        {
+            if (person.getUserId().equals(userId))
+            {
+                return;
+            }
+        }
+        commonServer.registerPerson(systemSessionToken, userId);
+        commonServer.registerInstanceRole(systemSessionToken, RoleCode.ADMIN,
+                Grantee.createPerson(userId));
+    }
+
+    protected Project findOrCreateProject(String projectIdentifier)
+    {
+        Project project = tryToFindProject(projectIdentifier);
+        if (project != null)
+        {
+            return project;
+        }
+        commonServer.registerProject(systemSessionToken, new ProjectIdentifierFactory(
+                projectIdentifier).createIdentifier(), "A test project", null, Collections
+                .<NewAttachment> emptyList());
+        return tryToFindProject(projectIdentifier);
+    }
+
+    protected Project tryToFindProject(String projectIdentifier)
+    {
+        List<Project> projects = commonServer.listProjects(systemSessionToken);
+        for (Project project : projects)
+        {
+            if (project.getIdentifier().equals(projectIdentifier))
+            {
+                return project;
+            }
+        }
+        return null;
+    }
+
+    protected Space findOrCreateSpace(String spaceCode)
+    {
+        Space space = tryToFindSpace(spaceCode);
+        if (space != null)
+        {
+            return space;
+        }
+        commonServer.registerSpace(systemSessionToken, spaceCode, "A test space");
+        return tryToFindSpace(spaceCode);
+    }
+
+    protected Space tryToFindSpace(String spaceCode)
+    {
+        DatabaseInstanceIdentifier identifier = new DatabaseInstanceIdentifier(null);
+        List<Space> spaces = commonServer.listSpaces(systemSessionToken, identifier);
+        for (Space space : spaces)
+        {
+            if (space.getCode().equals(spaceCode))
+            {
+                return space;
+            }
+        }
+        return null;
+    }
+
+    protected NewExperiment experiment(int number)
+    {
+        NewExperiment experiment =
+                new NewExperiment(project1.getIdentifier() + "/OLT-E" + number,
+                        EXPERIMENT_TYPE_CODE);
+        experiment.setAttachments(Collections.<NewAttachment> emptyList());
+        experiment.setProperties(new IEntityProperty[]
+            { new PropertyBuilder("DESCRIPTION").value("hello " + number).getProperty() });
+        return experiment;
+    }
+
+    protected List<String> extractCodes(List<? extends ICodeHolder> codeHolders)
+    {
+        List<String> result = new ArrayList<String>();
+        for (ICodeHolder codeHolder : codeHolders)
+        {
+            result.add(codeHolder.getCode());
+        }
+        Collections.sort(result);
+        return result;
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/ProjectOptimisticLockingTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/ProjectOptimisticLockingTest.java
new file mode 100644
index 00000000000..7739d790a0b
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/optimistic_locking/ProjectOptimisticLockingTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2012 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.systemtest.optimistic_locking;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.fail;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.concurrent.MessageChannel;
+import ch.systemsx.cisd.common.concurrent.MessageChannelBuilder;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.openbis.common.conversation.context.ServiceConversationsThreadContext;
+import ch.systemsx.cisd.openbis.common.conversation.progress.IServiceConversationProgressListener;
+import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.server.util.TimeIntervalChecker;
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IEntityProperty;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewExperiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Project;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ProjectUpdates;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.builders.ExperimentTypeBuilder;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.builders.PropertyBuilder;
+import ch.systemsx.cisd.openbis.generic.shared.dto.AtomicEntityOperationDetails;
+import ch.systemsx.cisd.openbis.generic.shared.dto.builders.AtomicEntityOperationDetailsBuilder;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifierFactory;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class ProjectOptimisticLockingTest extends OptimisticLockingTestCase
+{
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            ProjectOptimisticLockingTest.class);
+
+    private static final String REGISTERED = "registered";
+
+    private static final String FIRST_REGISTERED = "First registered";
+
+    private static final String CREATE_EXPERIMENTS_PHASE = "createExperiments";
+
+    @Test
+    public void testCreateProject()
+    {
+        Project project = new Project();
+        project.setCode("POLT-1");
+        String identifier = "/" + space1.getCode() + "/POLT-1";
+        project.setIdentifier(identifier);
+        project.setDescription("ProjectOptimisticLockingTest test");
+        project.setSpace(space1);
+        logIntoCommonClientService();
+        TimeIntervalChecker timeIntervalChecker = new TimeIntervalChecker(1);
+
+        commonClientService.registerProject(SESSION_KEY, project);
+
+        Project p =
+                commonServer
+                        .getProjectInfo(systemSessionToken, createProjectIdentifier(identifier));
+        assertEquals(project.getDescription(), p.getDescription());
+        assertEquals("test", p.getRegistrator().getUserId());
+        assertEquals("test", p.getModifier().getUserId());
+        timeIntervalChecker.assertDateInInterval(p.getRegistrationDate());
+        timeIntervalChecker.assertDateInInterval(p.getModificationDate());
+    }
+
+    private ProjectIdentifier createProjectIdentifier(String identifier)
+    {
+        return new ProjectIdentifierFactory(identifier).createIdentifier();
+    }
+
+    @Test
+    public void testUpdateProjectAndCheckModificationDateAndModifier()
+    {
+        ProjectIdentifier projectIdentifier = createProjectIdentifier(project1.getIdentifier());
+        Project p = commonServer.getProjectInfo(systemSessionToken, projectIdentifier);
+        ProjectUpdates updates = new ProjectUpdates();
+        updates.setVersion(p.getVersion());
+        updates.setTechId(new TechId(p));
+        updates.setDescription(p.getDescription() + " 2");
+        updates.setAttachments(Collections.<NewAttachment> emptyList());
+        updates.setAttachmentSessionKey(SESSION_KEY);
+        logIntoCommonClientService();
+        TimeIntervalChecker timeIntervalChecker = new TimeIntervalChecker(1);
+
+        commonClientService.updateProject(updates);
+
+        p = commonServer.getProjectInfo(systemSessionToken, projectIdentifier);
+        assertEquals(project1.getDescription() + " 2", p.getDescription());
+        assertEquals("system", p.getRegistrator().getUserId());
+        assertEquals("test", p.getModifier().getUserId());
+        timeIntervalChecker.assertDateInInterval(p.getModificationDate());
+
+    }
+
+    @Test
+    public void testUpdateProjectWithOldVersion()
+    {
+        ProjectIdentifier projectIdentifier = createProjectIdentifier(project1.getIdentifier());
+        Project currentProject = commonServer.getProjectInfo(systemSessionToken, projectIdentifier);
+        ProjectUpdates updates = new ProjectUpdates();
+        updates.setVersion(currentProject.getVersion());
+        updates.setTechId(new TechId(currentProject));
+        updates.setDescription(currentProject.getDescription() + " 1");
+        updates.setAttachments(Collections.<NewAttachment> emptyList());
+        updates.setAttachmentSessionKey(SESSION_KEY);
+        logIntoCommonClientService();
+        commonClientService.updateProject(updates);
+
+        try
+        {
+            commonClientService.updateProject(updates);
+            fail("UserFailureException expected");
+        } catch (UserFailureException ex)
+        {
+            assertEquals("Project has been modified in the meantime. Reopen tab to be able "
+                    + "to continue with refreshed data.", ex.getMessage());
+        }
+    }
+
+    @Test
+    public void testRegisterExperimentAndCheckModificationDateAndModifierOfProject()
+    {
+        String sessionToken = logIntoCommonClientService().getSessionID();
+        NewExperiment experiment =
+                new NewExperiment(project1.getIdentifier() + "/POLT-1", EXPERIMENT_TYPE_CODE);
+        experiment.setAttachments(Collections.<NewAttachment> emptyList());
+        experiment.setProperties(new IEntityProperty[]
+            { new PropertyBuilder("DESCRIPTION").value("hello").getProperty() });
+        assertEquals("system", project1.getModifier().getUserId());
+        TimeIntervalChecker timeIntervalChecker = new TimeIntervalChecker(1);
+
+        genericServer.registerExperiment(sessionToken, experiment,
+                Collections.<NewAttachment> emptyList());
+
+        checkModifierAndModificationDateOfProject1(timeIntervalChecker);
+    }
+
+    @Test
+    public void testRegisterExperiments()
+    {
+        assertEquals("system", project1.getModifier().getUserId());
+        AtomicEntityOperationDetailsBuilder builder = new AtomicEntityOperationDetailsBuilder();
+        builder.user(USER_ID);
+        builder.addNewExperiment(experiment(1)).addNewExperiment(experiment(2));
+        TimeIntervalChecker timeIntervalChecker = new TimeIntervalChecker(1);
+
+        etlService.performEntityOperations(systemSessionToken, builder.getDetails());
+
+        List<Experiment> experiments =
+                commonServer.listExperiments(systemSessionToken,
+                        new ExperimentTypeBuilder().code(EXPERIMENT_TYPE_CODE).getExperimentType(),
+                        createProjectIdentifier(project1.getIdentifier()));
+        assertEquals("[OLT-E1, OLT-E2]", extractCodes(experiments).toString());
+        checkModifierAndModificationDateOfProject1(timeIntervalChecker, USER_ID);
+    }
+
+    @Test
+    public void testRegisterExperiments2()
+    {
+        assertEquals("system", project1.getModifier().getUserId());
+        final StringBuilder stringBuilder = new StringBuilder();
+        final MessageChannel messageChannelMain =
+                new MessageChannelBuilder(10000).name("main").logger(operationLog).getChannel();
+        final MessageChannel messageChannelSecond =
+                new MessageChannelBuilder(10000).name("second").logger(operationLog).getChannel();
+        final IServiceConversationProgressListener listener =
+                new IServiceConversationProgressListener()
+                    {
+                        @Override
+                        public void update(String phaseName, int totalItemsToProcess,
+                                int numItemsProcessed)
+                        {
+                            stringBuilder.append(phaseName).append(" ").append(numItemsProcessed)
+                                    .append("/").append(totalItemsToProcess).append("\n");
+                            if (phaseName.equals(CREATE_EXPERIMENTS_PHASE)
+                                    && numItemsProcessed == 1 && totalItemsToProcess == 2)
+                            {
+                                messageChannelMain.send(FIRST_REGISTERED);
+                            }
+                        }
+
+                        @Override
+                        public void close()
+                        {
+                        }
+                    };
+        TimeIntervalChecker timeIntervalChecker = new TimeIntervalChecker(1);
+
+        new Thread(new Runnable()
+            {
+                @Override
+                public void run()
+                {
+                    NewExperiment experiment3 = experiment(3);
+                    String sessionToken =
+                            genericServer.tryToAuthenticate("test", "a").getSessionToken();
+                    messageChannelMain.assertNextMessage(FIRST_REGISTERED);
+                    genericServer.registerExperiment(sessionToken, experiment3,
+                            Collections.<NewAttachment> emptyList());
+                    messageChannelSecond.send(REGISTERED);
+                }
+            }).start();
+
+        ServiceConversationsThreadContext.setProgressListener(listener);
+        AtomicEntityOperationDetailsBuilder builder = new AtomicEntityOperationDetailsBuilder();
+        builder.user(USER_ID);
+        builder.addNewExperiment(experiment(1)).addNewExperiment(experiment(2));
+        AtomicEntityOperationDetails details = builder.getDetails();
+
+        etlService.performEntityOperations(systemSessionToken, details);
+
+        messageChannelSecond.assertNextMessage(REGISTERED);
+
+        List<Experiment> experiments =
+                commonServer.listExperiments(systemSessionToken,
+                        new ExperimentTypeBuilder().code(EXPERIMENT_TYPE_CODE).getExperimentType(),
+                        createProjectIdentifier(project1.getIdentifier()));
+        assertEquals("[OLT-E1, OLT-E2, OLT-E3]", extractCodes(experiments).toString());
+        checkModifierAndModificationDateOfProject1(timeIntervalChecker);
+        assertEquals("authorize 1/2\n" + "authorize 2/2\n" + "createExperiments 1/2\n"
+                + "createExperiments 2/2\n", stringBuilder.toString());
+    }
+
+    private void checkModifierAndModificationDateOfProject1(TimeIntervalChecker timeIntervalChecker)
+    {
+        checkModifierAndModificationDateOfProject1(timeIntervalChecker, "test");
+    }
+
+    private void checkModifierAndModificationDateOfProject1(
+            TimeIntervalChecker timeIntervalChecker, String modifier)
+    {
+        ProjectIdentifier projectIdentifier = createProjectIdentifier(project1.getIdentifier());
+        Project p = commonServer.getProjectInfo(systemSessionToken, projectIdentifier);
+        assertEquals("system", p.getRegistrator().getUserId());
+        assertEquals(project1.getRegistrationDate(), p.getRegistrationDate());
+        assertEquals(modifier, p.getModifier().getUserId());
+        timeIntervalChecker.assertDateInInterval(p.getModificationDate());
+    }
+
+}
-- 
GitLab