From 90fb28f1a2929b1eac6ea724000c43ee4dccca0f Mon Sep 17 00:00:00 2001
From: pkupczyk <pkupczyk>
Date: Fri, 27 Mar 2015 10:00:55 +0000
Subject: [PATCH] SSDM-1677 : V3 AS API - finish up projects and materials

SVN: 33753
---
 .../server/api/v3/ApplicationServerApi.java   |  23 ++
 .../api/v3/ApplicationServerApiLogger.java    |   7 +
 .../project/IUpdateProjectExecutor.java       |  28 ++
 .../project/IUpdateProjectLeaderExecutor.java |  29 ++
 .../project/IUpdateProjectSpaceExecutor.java  |  29 ++
 .../project/UpdateProjectExecutor.java        | 148 ++++++++
 .../project/UpdateProjectLeaderExecutor.java  |  82 +++++
 .../project/UpdateProjectSpaceExecutor.java   |  93 +++++
 .../entity/person/PersonTranslator.java       |   2 +
 .../systemtest/api/v3/AbstractTest.java       |  24 ++
 .../systemtest/api/v3/MapProjectTest.java     | 310 ++++++++++++++++
 .../systemtest/api/v3/UpdateProjectTest.java  | 342 ++++++++++++++++++
 .../shared/api/v3/IApplicationServerApi.java  |  11 +-
 .../api/v3/dto/entity/person/Person.java      |  17 +
 .../v3/dto/entity/project/ProjectUpdate.java  | 117 ++++++
 .../api/v3/dto/entity/space/SpaceUpdate.java  |   1 +
 .../api/v3/dto/generators/Generator.java      |   2 +
 17 files changed, 1261 insertions(+), 4 deletions(-)
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectLeaderExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectSpaceExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectLeaderExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectSpaceExecutor.java
 create mode 100644 openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/MapProjectTest.java
 create mode 100644 openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/UpdateProjectTest.java
 create mode 100644 openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/project/ProjectUpdate.java

diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
index 4f649fbe2d6..bfc052c454a 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApi.java
@@ -45,6 +45,7 @@ import ch.ethz.sis.openbis.generic.server.api.v3.executor.material.IMapMaterialB
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.material.IUpdateMaterialExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.ICreateProjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.IMapProjectByIdExecutor;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.project.IUpdateProjectExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.sample.ICreateSampleExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.sample.IDeleteSampleExecutor;
 import ch.ethz.sis.openbis.generic.server.api.v3.executor.sample.IMapSampleByIdExecutor;
@@ -83,6 +84,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.MaterialCre
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.MaterialUpdate;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.Project;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectCreation;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.Sample;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.SampleCreation;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.SampleUpdate;
@@ -163,6 +165,9 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Autowired
     private IUpdateSpaceExecutor updateSpaceExecutor;
 
+    @Autowired
+    private IUpdateProjectExecutor updateProjectExecutor;
+
     @Autowired
     private IUpdateExperimentExecutor updateExperimentExecutor;
 
@@ -392,6 +397,24 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
         }
     }
 
+    @Override
+    @Transactional
+    @RolesAllowed({ RoleWithHierarchy.SPACE_POWER_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @Capability("WRITE_PROJECT")
+    public void updateProjects(String sessionToken, List<ProjectUpdate> projectUpdates)
+    {
+        Session session = getSession(sessionToken);
+        OperationContext context = new OperationContext(session);
+
+        try
+        {
+            updateProjectExecutor.update(context, projectUpdates);
+        } catch (Throwable t)
+        {
+            throw ExceptionUtils.create(context, t);
+        }
+    }
+
     @Override
     @Transactional
     @RolesAllowed(
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
index 4b848d0a088..0c63b8d938b 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/ApplicationServerApiLogger.java
@@ -36,6 +36,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.MaterialCre
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.MaterialUpdate;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.Project;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectCreation;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.Sample;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.SampleCreation;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.SampleUpdate;
@@ -156,6 +157,12 @@ public class ApplicationServerApiLogger extends AbstractServerLogger implements
         logAccess(sessionToken, "update-spaces", "SPACE_UPDATES(%s)", spaceUpdates);
     }
 
+    @Override
+    public void updateProjects(String sessionToken, List<ProjectUpdate> projectUpdates)
+    {
+        logAccess(sessionToken, "update-projects", "PROJECT_UPDATES(%s)", projectUpdates);
+    }
+
     @Override
     public void updateExperiments(String sessionToken, List<ExperimentUpdate> experimentUpdates)
     {
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectExecutor.java
new file mode 100644
index 00000000000..6d4bb6ff9c5
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectExecutor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.entity.IUpdateEntityExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+
+/**
+ * @author pkupczyk
+ */
+public interface IUpdateProjectExecutor extends IUpdateEntityExecutor<ProjectUpdate>
+{
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectLeaderExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectLeaderExecutor.java
new file mode 100644
index 00000000000..ab91122843c
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectLeaderExecutor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.entity.IUpdateEntityRelationsExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+
+/**
+ * @author pkupczyk
+ */
+public interface IUpdateProjectLeaderExecutor extends IUpdateEntityRelationsExecutor<ProjectUpdate, ProjectPE>
+{
+
+}
\ No newline at end of file
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectSpaceExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectSpaceExecutor.java
new file mode 100644
index 00000000000..055aacd440d
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/IUpdateProjectSpaceExecutor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.entity.IUpdateEntityRelationsExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+
+/**
+ * @author pkupczyk
+ */
+public interface IUpdateProjectSpaceExecutor extends IUpdateEntityRelationsExecutor<ProjectUpdate, ProjectPE>
+{
+
+}
\ No newline at end of file
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectExecutor.java
new file mode 100644
index 00000000000..143b5a70dcf
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectExecutor.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2014 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.attachment.IUpdateAttachmentForEntityExecutor;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.entity.AbstractUpdateEntityExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.IProjectId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.exceptions.UnauthorizedObjectAccessException;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.server.authorization.validator.ProjectByIdentiferValidator;
+import ch.systemsx.cisd.openbis.generic.server.business.bo.DataAccessExceptionTranslator;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+import ch.systemsx.cisd.openbis.generic.shared.util.RelationshipUtils;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class UpdateProjectExecutor extends AbstractUpdateEntityExecutor<ProjectUpdate, ProjectPE, IProjectId> implements
+        IUpdateProjectExecutor
+{
+
+    @Autowired
+    private IDAOFactory daoFactory;
+
+    @Autowired
+    private IMapProjectByIdExecutor mapProjectByIdExecutor;
+
+    @Autowired
+    private IUpdateProjectSpaceExecutor updateProjectSpaceExecutor;
+
+    @Autowired
+    private IUpdateProjectLeaderExecutor updateProjectLeaderExecutor;
+
+    @Autowired
+    private IUpdateAttachmentForEntityExecutor updateAttachmentForEntityExecutor;
+
+    @Override
+    protected IProjectId getId(ProjectUpdate update)
+    {
+        return update.getProjectId();
+    }
+
+    @Override
+    protected void checkData(IOperationContext context, ProjectUpdate update)
+    {
+        if (update.getProjectId() == null)
+        {
+            throw new UserFailureException("Project id cannot be null.");
+        }
+    }
+
+    @Override
+    protected void checkAccess(IOperationContext context, IProjectId id, ProjectPE entity)
+    {
+        if (false == new ProjectByIdentiferValidator().doValidation(context.getSession().tryGetPerson(), entity))
+        {
+            throw new UnauthorizedObjectAccessException(id);
+        }
+    }
+
+    @Override
+    protected void checkBusinessRules(IOperationContext context, Collection<ProjectPE> entities)
+    {
+        // nothing to do
+    }
+
+    @Override
+    protected void updateBatch(IOperationContext context, Map<ProjectUpdate, ProjectPE> entitiesMap)
+    {
+        updateProjectSpaceExecutor.update(context, entitiesMap);
+        updateProjectLeaderExecutor.update(context, entitiesMap);
+
+        for (Map.Entry<ProjectUpdate, ProjectPE> entry : entitiesMap.entrySet())
+        {
+            ProjectUpdate update = entry.getKey();
+            ProjectPE project = entry.getValue();
+
+            RelationshipUtils.updateModificationDateAndModifier(project, context.getSession().tryGetPerson());
+
+            if (update.getDescription() != null && update.getDescription().isModified())
+            {
+                project.setDescription(update.getDescription().getValue());
+            }
+
+            updateAttachmentForEntityExecutor.update(context, project, update.getAttachments());
+        }
+    }
+
+    @Override
+    protected void updateAll(IOperationContext context, Map<ProjectUpdate, ProjectPE> entitiesMap)
+    {
+        // nothing to do
+    }
+
+    @Override
+    protected Map<IProjectId, ProjectPE> map(IOperationContext context, Collection<IProjectId> ids)
+    {
+        return mapProjectByIdExecutor.map(context, ids);
+    }
+
+    @Override
+    protected List<ProjectPE> list(IOperationContext context, Collection<Long> ids)
+    {
+        return daoFactory.getProjectDAO().listByIDs(ids);
+    }
+
+    @Override
+    protected void save(IOperationContext context, List<ProjectPE> entities, boolean clearCache)
+    {
+        for (ProjectPE entity : entities)
+        {
+            daoFactory.getProjectDAO().validateAndSaveUpdatedEntity(entity);
+        }
+    }
+
+    @Override
+    protected void handleException(DataAccessException e)
+    {
+        DataAccessExceptionTranslator.throwException(e, "project", null);
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectLeaderExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectLeaderExecutor.java
new file mode 100644
index 00000000000..1bd4187a800
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectLeaderExecutor.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 ETH Zuerich, Scientific IT Services
+ *
+ * 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.entity.AbstractUpdateEntityFieldUpdateValueRelationExecutor;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.person.IMapPersonByIdExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.FieldUpdateValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.IPersonId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.PersonPermId;
+import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class UpdateProjectLeaderExecutor extends AbstractUpdateEntityFieldUpdateValueRelationExecutor<ProjectUpdate, ProjectPE, IPersonId, PersonPE>
+        implements IUpdateProjectLeaderExecutor
+{
+
+    @Autowired
+    private IMapPersonByIdExecutor mapPersonByIdExecutor;
+
+    @Override
+    protected IPersonId getRelatedId(PersonPE related)
+    {
+        return new PersonPermId(related.getUserId());
+    }
+
+    @Override
+    protected PersonPE getCurrentlyRelated(ProjectPE entity)
+    {
+        return entity.getProjectLeader();
+    }
+
+    @Override
+    protected FieldUpdateValue<IPersonId> getRelatedUpdate(ProjectUpdate update)
+    {
+        return update.getLeaderId();
+    }
+
+    @Override
+    protected Map<IPersonId, PersonPE> map(IOperationContext context, List<IPersonId> relatedIds)
+    {
+        return mapPersonByIdExecutor.map(context, relatedIds);
+    }
+
+    @Override
+    protected void check(IOperationContext context, ProjectPE entity, IPersonId relatedId, PersonPE related)
+    {
+        // nothing to do
+    }
+
+    @Override
+    protected void update(IOperationContext context, ProjectPE entity, PersonPE related)
+    {
+        entity.setProjectLeader(related);
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectSpaceExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectSpaceExecutor.java
new file mode 100644
index 00000000000..978cd083ce1
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/executor/project/UpdateProjectSpaceExecutor.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2014 ETH Zuerich, Scientific IT Services
+ *
+ * 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.ethz.sis.openbis.generic.server.api.v3.executor.project;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.entity.AbstractUpdateEntityFieldUpdateValueRelationExecutor;
+import ch.ethz.sis.openbis.generic.server.api.v3.executor.space.IMapSpaceByIdExecutor;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.FieldUpdateValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.ISpaceId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.exceptions.UnauthorizedObjectAccessException;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.server.authorization.validator.SimpleSpaceValidator;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class UpdateProjectSpaceExecutor extends AbstractUpdateEntityFieldUpdateValueRelationExecutor<ProjectUpdate, ProjectPE, ISpaceId, SpacePE>
+        implements IUpdateProjectSpaceExecutor
+{
+
+    @Autowired
+    private IMapSpaceByIdExecutor mapSpaceByIdExecutor;
+
+    @Override
+    protected ISpaceId getRelatedId(SpacePE related)
+    {
+        return new SpacePermId(related.getCode());
+    }
+
+    @Override
+    protected SpacePE getCurrentlyRelated(ProjectPE entity)
+    {
+        return entity.getSpace();
+    }
+
+    @Override
+    protected FieldUpdateValue<ISpaceId> getRelatedUpdate(ProjectUpdate update)
+    {
+        return update.getSpaceId();
+    }
+
+    @Override
+    protected Map<ISpaceId, SpacePE> map(IOperationContext context, List<ISpaceId> relatedIds)
+    {
+        return mapSpaceByIdExecutor.map(context, relatedIds);
+    }
+
+    @Override
+    protected void check(IOperationContext context, ProjectPE entity, ISpaceId relatedId, SpacePE related)
+    {
+        if (false == new SimpleSpaceValidator().doValidation(context.getSession().tryGetPerson(), related))
+        {
+            throw new UnauthorizedObjectAccessException(relatedId);
+        }
+    }
+
+    @Override
+    protected void update(IOperationContext context, ProjectPE entity, SpacePE related)
+    {
+        if (related == null)
+        {
+            throw new UserFailureException("Space id cannot be null");
+        } else
+        {
+            relationshipService.assignProjectToSpace(context.getSession(), entity, related);
+        }
+    }
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/translator/entity/person/PersonTranslator.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/translator/entity/person/PersonTranslator.java
index bdb64236be7..60c40f32e6c 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/translator/entity/person/PersonTranslator.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/api/v3/translator/entity/person/PersonTranslator.java
@@ -22,6 +22,7 @@ import ch.ethz.sis.openbis.generic.server.api.v3.translator.TranslationContext;
 import ch.ethz.sis.openbis.generic.server.api.v3.translator.entity.space.SpaceTranslator;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.person.Person;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.person.PersonFetchOptions;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.PersonPermId;
 import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
 
 /**
@@ -39,6 +40,7 @@ public class PersonTranslator extends AbstractCachingTranslator<PersonPE, Person
     {
         Person result = new Person();
 
+        result.setPermId(new PersonPermId(person.getUserId()));
         result.setUserId(person.getUserId());
         result.setFirstName(person.getFirstName());
         result.setLastName(person.getLastName());
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/AbstractTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/AbstractTest.java
index 5b559ab93e6..1ddee1d890b 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/AbstractTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/AbstractTest.java
@@ -181,6 +181,18 @@ public class AbstractTest extends SystemTestCase
             });
     }
 
+    protected void assertExperimentsNotFetched(final Project project)
+    {
+        assertNotFetched(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    project.getExperiments();
+                }
+            });
+    }
+
     protected void assertTagsNotFetched(final ITagsHolder entity)
     {
         assertNotFetched(new IDelegatedAction()
@@ -361,6 +373,18 @@ public class AbstractTest extends SystemTestCase
             });
     }
 
+    protected void assertLeaderNotFetched(final Project entity)
+    {
+        assertNotFetched(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    entity.getLeader();
+                }
+            });
+    }
+
     protected void assertPreviousAttachmentNotFetched(final Attachment att)
     {
         assertNotFetched(new IDelegatedAction()
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/MapProjectTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/MapProjectTest.java
new file mode 100644
index 00000000000..c932720f8a5
--- /dev/null
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/MapProjectTest.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2014 ETH Zuerich, Scientific IT Services
+ *
+ * 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.ethz.sis.openbis.systemtest.api.v3;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.Project;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectCreation;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.project.ProjectFetchOptions;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.IProjectId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectIdentifier;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectPermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
+
+/**
+ * @author pkupczyk
+ */
+public class MapProjectTest extends AbstractTest
+{
+
+    @Test
+    public void testMapByPermId()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        ProjectPermId permId1 = new ProjectPermId("20120814110011738-103");
+        ProjectPermId permId2 = new ProjectPermId("20120814110011738-105");
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(permId1, permId2), new ProjectFetchOptions());
+
+        assertEquals(2, map.size());
+
+        Iterator<Project> iter = map.values().iterator();
+        assertEquals(iter.next().getPermId(), permId1);
+        assertEquals(iter.next().getPermId(), permId2);
+
+        assertEquals(map.get(permId1).getPermId(), permId1);
+        assertEquals(map.get(permId2).getPermId(), permId2);
+
+        v3api.logout(sessionToken);
+    }
+
+    @Test
+    public void testMapByIdentifier()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        ProjectIdentifier identifier1 = new ProjectIdentifier("/CISD/NEMO");
+        ProjectIdentifier identifier2 = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(identifier1, identifier2), new ProjectFetchOptions());
+
+        assertEquals(2, map.size());
+
+        Iterator<Project> iter = map.values().iterator();
+        assertEquals(iter.next().getIdentifier(), identifier1);
+        assertEquals(iter.next().getIdentifier(), identifier2);
+
+        assertEquals(map.get(identifier1).getIdentifier(), identifier1);
+        assertEquals(map.get(identifier2).getIdentifier(), identifier2);
+
+        v3api.logout(sessionToken);
+    }
+
+    @Test
+    public void testMapByIdsNonexistent()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        ProjectPermId permId1 = new ProjectPermId("IDONTEXIST");
+        ProjectIdentifier identifier1 = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        ProjectPermId permId2 = new ProjectPermId("20120814110011738-103");
+        ProjectIdentifier identifier2 = new ProjectIdentifier("/IDONT/EXIST");
+
+        Map<IProjectId, Project> map =
+                v3api.mapProjects(sessionToken, Arrays.asList(permId1, identifier1, permId2, identifier2), new ProjectFetchOptions());
+
+        assertEquals(2, map.size());
+
+        Iterator<Project> iter = map.values().iterator();
+        assertEquals(iter.next().getIdentifier(), identifier1);
+        assertEquals(iter.next().getPermId(), permId2);
+
+        assertEquals(map.get(identifier1).getIdentifier(), identifier1);
+        assertEquals(map.get(permId2).getPermId(), permId2);
+
+        v3api.logout(sessionToken);
+    }
+
+    @Test
+    public void testMapByIdsDuplicated()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        ProjectPermId permId1 = new ProjectPermId("20120814110011738-103");
+        ProjectIdentifier identifier = new ProjectIdentifier("/CISD/NEMO");
+        ProjectPermId permId2 = new ProjectPermId("20120814110011738-103");
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(permId1, identifier, permId2), new ProjectFetchOptions());
+
+        assertEquals(2, map.size());
+
+        assertEquals(map.get(permId1).getPermId(), permId1);
+        assertEquals(map.get(identifier).getIdentifier(), identifier);
+
+        v3api.logout(sessionToken);
+    }
+
+    @Test
+    public void testMapByIdsUnauthorized()
+    {
+        ProjectPermId permId = new ProjectPermId("20120814110011738-101");
+        ProjectIdentifier identifier1 = new ProjectIdentifier("/CISD/NEMO");
+        ProjectIdentifier identifier2 = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+
+        List<? extends IProjectId> ids = Arrays.asList(permId, identifier1, identifier2);
+
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, ids, new ProjectFetchOptions());
+
+        assertEquals(map.size(), 3);
+        v3api.logout(sessionToken);
+
+        sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+        map = v3api.mapProjects(sessionToken, ids, new ProjectFetchOptions());
+
+        assertEquals(map.size(), 1);
+
+        assertEquals(map.get(identifier2).getIdentifier(), identifier2);
+
+        v3api.logout(sessionToken);
+    }
+
+    @Test
+    public void testMapByIdsWithFetchOptionsEmpty()
+    {
+        ProjectIdentifier identifier = new ProjectIdentifier("/CISD/NEMO");
+        ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(identifier), fetchOptions);
+
+        Project project = map.get(identifier);
+
+        assertExperimentsNotFetched(project);
+        assertSpaceNotFetched(project);
+        assertRegistratorNotFetched(project);
+        assertModifierNotFetched(project);
+        assertLeaderNotFetched(project);
+        assertAttachmentsNotFetched(project);
+    }
+
+    @Test
+    public void testMapByIdsWithExperiments()
+    {
+        ProjectIdentifier identifier = new ProjectIdentifier("/CISD/DEFAULT");
+
+        ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+        fetchOptions.withExperiments();
+
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(identifier), fetchOptions);
+
+        Project project = map.get(identifier);
+        List<Experiment> experiments = project.getExperiments();
+
+        assertExperimentIdentifiers(experiments, "/CISD/DEFAULT/EXP-REUSE", "/CISD/DEFAULT/EXP-WELLS", "/CISD/DEFAULT/EXP-Y");
+
+        assertSpaceNotFetched(project);
+        assertRegistratorNotFetched(project);
+        assertModifierNotFetched(project);
+        assertLeaderNotFetched(project);
+        assertAttachmentsNotFetched(project);
+    }
+
+    @Test
+    public void testMapByIdsWithSpace()
+    {
+        ProjectIdentifier identifier = new ProjectIdentifier("/CISD/DEFAULT");
+
+        ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+        fetchOptions.withSpace();
+
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(identifier), fetchOptions);
+
+        Project project = map.get(identifier);
+
+        Assert.assertEquals(project.getSpace().getCode(), "CISD");
+
+        assertExperimentsNotFetched(project);
+        assertRegistratorNotFetched(project);
+        assertModifierNotFetched(project);
+        assertLeaderNotFetched(project);
+        assertAttachmentsNotFetched(project);
+    }
+
+    @Test
+    public void testMapByIdsWithRegistrator()
+    {
+        String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        ProjectCreation creation = new ProjectCreation();
+        creation.setCode("TEST_REGISTRATOR");
+        creation.setSpaceId(new SpacePermId("TEST-SPACE"));
+
+        List<ProjectPermId> permIds = v3api.createProjects(sessionToken, Arrays.asList(creation));
+
+        ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+        fetchOptions.withRegistrator();
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, permIds, fetchOptions);
+        Project project = map.values().iterator().next();
+
+        Assert.assertEquals(project.getRegistrator().getUserId(), TEST_SPACE_USER);
+
+        assertExperimentsNotFetched(project);
+        assertSpaceNotFetched(project);
+        assertModifierNotFetched(project);
+        assertLeaderNotFetched(project);
+        assertAttachmentsNotFetched(project);
+    }
+
+    @Test
+    public void testMapByIdsWithModifier()
+    {
+        String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        ProjectIdentifier projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+        fetchOptions.withModifier();
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(projectId), fetchOptions);
+        Project project = map.get(projectId);
+
+        Assert.assertEquals(project.getModifier().getUserId(), TEST_USER);
+
+        ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+
+        v3api.updateProjects(sessionToken, Arrays.asList(update));
+
+        map = v3api.mapProjects(sessionToken, Arrays.asList(projectId), fetchOptions);
+        project = map.get(projectId);
+
+        Assert.assertEquals(project.getModifier().getUserId(), TEST_SPACE_USER);
+
+        assertExperimentsNotFetched(project);
+        assertSpaceNotFetched(project);
+        assertRegistratorNotFetched(project);
+        assertLeaderNotFetched(project);
+        assertAttachmentsNotFetched(project);
+    }
+
+    @Test
+    public void testMapByIdsWithLeader()
+    {
+        String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        ProjectIdentifier projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+        fetchOptions.withModifier();
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(projectId), fetchOptions);
+        Project project = map.get(projectId);
+
+        Assert.assertEquals(project.getModifier().getUserId(), TEST_USER);
+
+        ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+
+        v3api.updateProjects(sessionToken, Arrays.asList(update));
+
+        map = v3api.mapProjects(sessionToken, Arrays.asList(projectId), fetchOptions);
+        project = map.get(projectId);
+
+        Assert.assertEquals(project.getModifier().getUserId(), TEST_SPACE_USER);
+
+        assertExperimentsNotFetched(project);
+        assertSpaceNotFetched(project);
+        assertRegistratorNotFetched(project);
+        assertLeaderNotFetched(project);
+        assertAttachmentsNotFetched(project);
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/UpdateProjectTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/UpdateProjectTest.java
new file mode 100644
index 00000000000..b303add001b
--- /dev/null
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/api/v3/UpdateProjectTest.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2015 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.ethz.sis.openbis.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.attachment.AttachmentCreation;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.Project;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectCreation;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.Sample;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.project.ProjectFetchOptions;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.IPersonId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.PersonPermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.IProjectId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectIdentifier;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectPermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.ISpaceId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.SpacePermId;
+import ch.systemsx.cisd.common.action.IDelegatedAction;
+
+/**
+ * @author pkupczyk
+ */
+public class UpdateProjectTest extends AbstractTest
+{
+
+    @Test
+    public void testUpdateWithProjectNull()
+    {
+        final String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        final ProjectUpdate update = new ProjectUpdate();
+
+        assertUserFailureException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, "Project id cannot be null");
+    }
+
+    @Test
+    public void testUpdateWithProjectUnauthorized()
+    {
+        final String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/CISD/NEMO");
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+
+        assertUnauthorizedObjectAccessException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, projectId);
+    }
+
+    @Test
+    public void testUpdateWithProjectNonexistent()
+    {
+        final String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectPermId("IDONTEXIST");
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, projectId);
+    }
+
+    @Test
+    public void testUpdateWithSpaceUnauthorized()
+    {
+        final String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        final ISpaceId spaceId = new SpacePermId("CISD");
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+        update.setSpaceId(spaceId);
+
+        assertUnauthorizedObjectAccessException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, spaceId);
+    }
+
+    @Test
+    public void testUpdateWithSpaceNull()
+    {
+        final String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+        update.setSpaceId(null);
+
+        assertUserFailureException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, "Space id cannot be null");
+    }
+
+    @Test
+    public void testUpdateWithSpaceNonexistent()
+    {
+        final String sessionToken = v3api.login(TEST_SPACE_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        final ISpaceId spaceId = new SpacePermId("IDONTEXIST");
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+        update.setSpaceId(spaceId);
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, spaceId);
+    }
+
+    @Test
+    public void testUpdateWithSpaceWhenSamplesExist()
+    {
+        final String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        ProjectFetchOptions projectFetchOptions = new ProjectFetchOptions();
+        projectFetchOptions.withSpace();
+        projectFetchOptions.withExperiments().withSamples().withSpace();
+
+        Map<IProjectId, Project> projectMap = v3api.mapProjects(sessionToken, Arrays.asList(projectId), projectFetchOptions);
+
+        for (Project project : projectMap.values())
+        {
+            Assert.assertEquals(project.getSpace().getCode(), "TEST-SPACE");
+
+            for (Experiment experiment : project.getExperiments())
+            {
+                for (Sample sample : experiment.getSamples())
+                {
+                    Assert.assertEquals(sample.getSpace().getCode(), "TEST-SPACE");
+                }
+            }
+
+        }
+
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+        update.setSpaceId(new SpacePermId("CISD"));
+
+        v3api.updateProjects(sessionToken, Arrays.asList(update));
+
+        projectMap = v3api.mapProjects(sessionToken, Arrays.asList(projectId), projectFetchOptions);
+
+        for (Project project : projectMap.values())
+        {
+            Assert.assertEquals(project.getSpace().getCode(), "CISD");
+
+            for (Experiment experiment : project.getExperiments())
+            {
+                for (Sample sample : experiment.getSamples())
+                {
+                    Assert.assertEquals(sample.getSpace().getCode(), "CISD");
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testUpdateWithLeaderNonexistent()
+    {
+        final String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        final IPersonId leaderId = new PersonPermId("IDONTEXIST");
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+        update.setLeaderId(leaderId);
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateProjects(sessionToken, Arrays.asList(update));
+                }
+            }, leaderId);
+    }
+
+    @Test
+    public void testUpdateWithLeader()
+    {
+        final String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        final IProjectId projectId = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+        final ProjectFetchOptions projectFetchOptions = new ProjectFetchOptions();
+        projectFetchOptions.withLeader();
+
+        Map<IProjectId, Project> projectMap = v3api.mapProjects(sessionToken, Arrays.asList(projectId), projectFetchOptions);
+        Project project = projectMap.get(projectId);
+
+        Assert.assertNull(project.getLeader());
+
+        final IPersonId leaderId = new PersonPermId(TEST_SPACE_USER);
+        final ProjectUpdate update = new ProjectUpdate();
+        update.setProjectId(projectId);
+        update.setLeaderId(leaderId);
+
+        v3api.updateProjects(sessionToken, Arrays.asList(update));
+        projectMap = v3api.mapProjects(sessionToken, Arrays.asList(projectId), projectFetchOptions);
+        project = projectMap.get(projectId);
+
+        Assert.assertEquals(project.getLeader().getPermId(), leaderId);
+
+        update.setLeaderId(null);
+
+        v3api.updateProjects(sessionToken, Arrays.asList(update));
+        projectMap = v3api.mapProjects(sessionToken, Arrays.asList(projectId), projectFetchOptions);
+        project = projectMap.get(projectId);
+
+        Assert.assertNull(project.getLeader());
+    }
+
+    @Test
+    public void testUpdateWithAttachments()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        ProjectCreation projectCreation = new ProjectCreation();
+        projectCreation.setCode("PROJECT_ATTACHMENTS_TEST");
+        projectCreation.setSpaceId(new SpacePermId("CISD"));
+
+        ProjectFetchOptions projectFetchOptions = new ProjectFetchOptions();
+        projectFetchOptions.withAttachments().withContent();
+
+        List<ProjectPermId> projectPermIds = v3api.createProjects(sessionToken, Arrays.asList(projectCreation));
+
+        Map<IProjectId, Project> projectMap = v3api.mapProjects(sessionToken, projectPermIds, projectFetchOptions);
+        Project project = projectMap.values().iterator().next();
+
+        assertAttachments(project.getAttachments());
+
+        AttachmentCreation attachmentCreation = new AttachmentCreation();
+        attachmentCreation.setFileName("test_file");
+        attachmentCreation.setTitle("test_title");
+        attachmentCreation.setDescription("test_description");
+        attachmentCreation.setContent(new String("test_content").getBytes());
+
+        ProjectUpdate projectUpdate = new ProjectUpdate();
+        projectUpdate.setProjectId(projectPermIds.get(0));
+        projectUpdate.getAttachments().add(attachmentCreation);
+
+        v3api.updateProjects(sessionToken, Arrays.asList(projectUpdate));
+
+        projectMap = v3api.mapProjects(sessionToken, projectPermIds, projectFetchOptions);
+        project = projectMap.values().iterator().next();
+
+        assertAttachments(project.getAttachments(), attachmentCreation);
+    }
+
+    public void testUpdateWithDescription()
+    {
+        final String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        final IProjectId projectId1 = new ProjectIdentifier("/CISD/NEMO");
+        final IProjectId projectId2 = new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT");
+
+        Map<IProjectId, Project> map = v3api.mapProjects(sessionToken, Arrays.asList(projectId1, projectId2), new ProjectFetchOptions());
+
+        Project project1 = map.get(projectId1);
+        Project project2 = map.get(projectId2);
+
+        Assert.assertEquals(project1.getCode(), "NEMO");
+        Assert.assertEquals(project1.getDescription(), "nemo description");
+        Assert.assertEquals(project2.getCode(), "TEST-PROJECT");
+        Assert.assertNull(project2.getDescription());
+
+        ProjectUpdate update1 = new ProjectUpdate();
+        update1.setProjectId(projectId1);
+        update1.setDescription("a new description 1");
+
+        ProjectUpdate update2 = new ProjectUpdate();
+        update2.setProjectId(projectId2);
+        update2.setDescription("a new description 2");
+
+        v3api.updateProjects(sessionToken, Arrays.asList(update1, update2));
+
+        map = v3api.mapProjects(sessionToken, Arrays.asList(projectId1, projectId2), new ProjectFetchOptions());
+        project1 = map.get(projectId1);
+        project2 = map.get(projectId2);
+
+        Assert.assertEquals(project1.getCode(), "NEMO");
+        Assert.assertEquals(project1.getDescription(), update1.getDescription());
+        Assert.assertEquals(project2.getCode(), "TEST-PROJECT");
+        Assert.assertEquals(project2.getDescription(), update2.getDescription());
+    }
+
+}
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
index d9d2e4552bb..ad7e9c0775a 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/IApplicationServerApi.java
@@ -35,6 +35,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.MaterialCre
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.material.MaterialUpdate;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.Project;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectCreation;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project.ProjectUpdate;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.Sample;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.SampleCreation;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.sample.SampleUpdate;
@@ -115,10 +116,10 @@ public interface IApplicationServerApi extends IRpcService
 
     public List<MaterialPermId> createMaterials(String sessionToken, List<MaterialCreation> newMaterials);
 
-    public void updateMaterials(String sessionToken, List<MaterialUpdate> materialUpdates);
-
     public void updateSpaces(String sessionToken, List<SpaceUpdate> spaceUpdates);
 
+    public void updateProjects(String sessionToken, List<ProjectUpdate> projectUpdates);
+
     // REPLACES:
     // - ServiceForDataStoreServer.updateExperiment()
 
@@ -135,6 +136,8 @@ public interface IApplicationServerApi extends IRpcService
 
     public void updateDataSets(String sessionToken, List<DataSetUpdate> dataSetUpdates);
 
+    public void updateMaterials(String sessionToken, List<MaterialUpdate> materialUpdates);
+
     public Map<ISpaceId, Space> mapSpaces(String sessionToken, List<? extends ISpaceId> spaceIds,
             SpaceFetchOptions fetchOptions);
 
@@ -192,8 +195,6 @@ public interface IApplicationServerApi extends IRpcService
 
     public void deleteSpaces(String sessionToken, List<? extends ISpaceId> spaceIds, SpaceDeletionOptions deletionOptions);
 
-    public void deleteMaterials(String sessionToken, List<? extends IMaterialId> materialIds, MaterialDeletionOptions deletionOptions);
-
     // REPLACES:
     // - IGeneralInformationChangingService.deleteExperiments(List<Long>, String, DeletionType)
     public IDeletionId deleteExperiments(String sessionToken, List<? extends IExperimentId> experimentIds, ExperimentDeletionOptions deletionOptions);
@@ -204,6 +205,8 @@ public interface IApplicationServerApi extends IRpcService
 
     public IDeletionId deleteDataSets(String sessionToken, List<? extends IDataSetId> dataSetIds, DataSetDeletionOptions deletionOptions);
 
+    public void deleteMaterials(String sessionToken, List<? extends IMaterialId> materialIds, MaterialDeletionOptions deletionOptions);
+
     // REPLACES:
     // - IGeneralInformationService.listDeletions(EnumSet<DeletionFetchOption>)
     public List<Deletion> listDeletions(String sessionToken, DeletionFetchOptions fetchOptions);
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/person/Person.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/person/Person.java
index 8601176a72c..7abda7cec68 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/person/Person.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/person/Person.java
@@ -21,6 +21,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.interfaces.ISpaceHol
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.person.Person;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.space.Space;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.fetchoptions.person.PersonFetchOptions;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.PersonPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.exceptions.NotFetchedException;
 import ch.systemsx.cisd.base.annotation.JsonObject;
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -39,6 +40,9 @@ public class Person implements Serializable, ISpaceHolder, IRegistratorHolder, I
     @JsonProperty
     private PersonFetchOptions fetchOptions;
 
+    @JsonProperty
+    private PersonPermId permId;
+
     @JsonProperty
     private String userId;
 
@@ -76,6 +80,19 @@ public class Person implements Serializable, ISpaceHolder, IRegistratorHolder, I
         this.fetchOptions = fetchOptions;
     }
 
+    // Method automatically generated with {@link ch.ethz.sis.openbis.generic.shared.api.v3.dto.generators.DtoGenerator}
+    @JsonIgnore
+    public PersonPermId getPermId()
+    {
+        return permId;
+    }
+
+    // Method automatically generated with {@link ch.ethz.sis.openbis.generic.shared.api.v3.dto.generators.DtoGenerator}
+    public void setPermId(PersonPermId permId)
+    {
+        this.permId = permId;
+    }
+
     // Method automatically generated with {@link ch.ethz.sis.openbis.generic.shared.api.v3.dto.generators.DtoGenerator}
     @JsonIgnore
     public String getUserId()
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/project/ProjectUpdate.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/project/ProjectUpdate.java
new file mode 100644
index 00000000000..0d20472fb31
--- /dev/null
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/project/ProjectUpdate.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2013 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.ethz.sis.openbis.generic.shared.api.v3.dto.entity.project;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.AttachmentListUpdateValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.FieldUpdateValue;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.entity.ListUpdateValue.ListUpdateAction;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.IPersonId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.IProjectId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.space.ISpaceId;
+import ch.systemsx.cisd.base.annotation.JsonObject;
+
+/**
+ * @author pkupczyk
+ */
+@JsonObject("dto.entity.project.ProjectUpdate")
+public class ProjectUpdate implements Serializable
+{
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty
+    private IProjectId projectId;
+
+    @JsonProperty
+    private FieldUpdateValue<ISpaceId> spaceId = new FieldUpdateValue<ISpaceId>();
+
+    @JsonProperty
+    private FieldUpdateValue<String> description = new FieldUpdateValue<String>();
+
+    @JsonProperty
+    private FieldUpdateValue<IPersonId> leaderId = new FieldUpdateValue<IPersonId>();
+
+    @JsonProperty
+    private AttachmentListUpdateValue attachments = new AttachmentListUpdateValue();
+
+    @JsonIgnore
+    public IProjectId getProjectId()
+    {
+        return projectId;
+    }
+
+    @JsonIgnore
+    public void setProjectId(IProjectId projectId)
+    {
+        this.projectId = projectId;
+    }
+
+    @JsonIgnore
+    public void setSpaceId(ISpaceId spaceId)
+    {
+        this.spaceId.setValue(spaceId);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<ISpaceId> getSpaceId()
+    {
+        return spaceId;
+    }
+
+    @JsonIgnore
+    public void setDescription(String description)
+    {
+        this.description.setValue(description);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<String> getDescription()
+    {
+        return description;
+    }
+
+    @JsonIgnore
+    public void setLeaderId(IPersonId leaderId)
+    {
+        this.leaderId.setValue(leaderId);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<IPersonId> getLeaderId()
+    {
+        return leaderId;
+    }
+
+    @JsonIgnore
+    public AttachmentListUpdateValue getAttachments()
+    {
+        return attachments;
+    }
+
+    @JsonIgnore
+    public void setAttachmentsActions(List<ListUpdateAction<Object>> actions)
+    {
+        attachments.setActions(actions);
+    }
+
+}
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/space/SpaceUpdate.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/space/SpaceUpdate.java
index 0a4669f6bb9..0fcb8b4b58d 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/space/SpaceUpdate.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/entity/space/SpaceUpdate.java
@@ -37,6 +37,7 @@ public class SpaceUpdate implements Serializable
     @JsonProperty
     private ISpaceId spaceId;
 
+    @JsonProperty
     private FieldUpdateValue<String> description = new FieldUpdateValue<String>();
 
     @JsonIgnore
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/generators/Generator.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/generators/Generator.java
index 21eb8c3c50e..aa5d31846e6 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/generators/Generator.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/shared/api/v3/dto/generators/Generator.java
@@ -47,6 +47,7 @@ import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.entitytype.EntityTypePer
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.experiment.ExperimentIdentifier;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.experiment.ExperimentPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.material.MaterialPermId;
+import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.person.PersonPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectIdentifier;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.project.ProjectPermId;
 import ch.ethz.sis.openbis.generic.shared.api.v3.dto.id.sample.SampleIdentifier;
@@ -308,6 +309,7 @@ public class Generator extends AbstractGenerator
     {
         DtoGenerator gen = new DtoGenerator("person", "Person", PersonFetchOptions.class);
 
+        gen.addSimpleField(PersonPermId.class, "permId");
         gen.addStringField("userId");
         gen.addStringField("firstName");
         gen.addStringField("lastName");
-- 
GitLab