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 a966d924ac0287955ad18ff755dc5c15512d3dba..dec028453e76e0892af7fc5a0dfa91d637c13a44 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 6a6f409f265aadb67b47e750b2ed5d1915747531..f7038879ee2574894f84f2dc4baad6c065a2562c 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 eb3bf270383cda342e2effd91d8717764801c45c..e3ee1c7aeef41aef17efbf9eacfeb5a6e4e9913c 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 a27db3d39ab51ffc81afbcb10d809a5b9a674de6..7297eedad01a9a63c404cca6d94c949074821492 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 57b89f1b15aad07a9ef32180665db47eddb87d81..f904bfd8703d390e1970051b3c7430c53a1632e2 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 a729604eda6ece4364898b12279e3f632962052b..0faff1e3851f15c6d358ef6664adb970df3bf112 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 23ef8dffcc6bbe082c64e29279d52336c7865fd0..c2780d326da90041f328637c24159d48706d1e9e 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 3c9780d7a3fad25e833f79e1d074e8587f4d91a0..47cedb4968b43d87b274647ea5626984fa86dc19 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 fcaea658d759870691857fca5f3867c489abf0d8..1f3623aa62778cd6b962d9920090f6b7162a42fc 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 69ef4e3b31755757ab5b04c836c8512f6d33ae1f..aa26f27ec146cc9fc0ab05c566bfe39f307f391a 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 b9947b9c42da9abef35105928b40dbf4e6ff5428..9fb79de8ec70ea23fe57901553aca41076f3a10e 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 e8da4b03115824ca0648dbbb0b0d07d9b83a03d4..aea34723bfe0f25f9272af93af18364787f0028d 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 0151561be241d11877df73080c2f6ed5ac5905c0..bad71532d1279941a27c92b2f5f7bbebfb7a16a8 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 6b2f25944eacc620c3468d436335f9ffe8edfd18..bfe5767ef4d54d4a0eb48c78a72404cfe04b6de6 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 946d0825c9d922ee236de2041fca4ff686300f87..73b3a47a521d06d886b7980c5a3f1f39308ac30c 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 a3143301c5f699a67b09875ec0cb121e6bbdd832..d96a023420c526710aabe5d53fe640ff0ac4efa0 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 6c862130bae367aa4d198c09f916bb179703eea8..c8795a3f949c4f0088fc538eb3d55d25d8621337 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 0000000000000000000000000000000000000000..8c26dfcb61f26101720fff65624853de0062f948
--- /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 eb411e6add9c5cff5b5cf6238c8815f14a372cf1..dca8068f337b4ec050e76ac951b770a66bd68d10 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 0000000000000000000000000000000000000000..88d60832781c987c51cea55d74fcb25c95609acb
--- /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 0000000000000000000000000000000000000000..e22470fe2bf03b53b7aacee181c343b580b13bd1
--- /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 ef7b365cd667e686cbe1d817218524a9340b659a..b7bf969f2bcbc5097a58d0a8aec727d10c4f9313 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 0000000000000000000000000000000000000000..5f5bfa2df58428af3a3aaf6501a1e3f3d9cbacd5
--- /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 0000000000000000000000000000000000000000..7739d790a0b80063e7b9cd590e3a1c42b0bb654e
--- /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());
+    }
+
+}