From 7692e93432f6e0ff9d2bb744f9847fee8b037ec4 Mon Sep 17 00:00:00 2001
From: pkupczyk <pkupczyk>
Date: Tue, 29 Mar 2016 15:19:13 +0000
Subject: [PATCH] SSDM-3395 : V3 AS API - controlled vocabulary terms -
 updateVocabularyTerms

SVN: 36033
---
 .../server/asapi/v3/ApplicationServerApi.java |  14 +
 .../asapi/v3/ApplicationServerApiLogger.java  |   7 +
 .../IUpdateVocabularyTermMethodExecutor.java  |  27 ++
 .../UpdateVocabularyTermMethodExecutor.java   |  44 +++
 ...teVocabularyTermAuthorizationExecutor.java |  31 ++
 .../IUpdateVocabularyTermExecutor.java        |  28 ++
 ...teVocabularyTermAuthorizationExecutor.java |  49 +++
 .../UpdateVocabularyTermExecutor.java         | 273 +++++++++++++++
 .../asapi/v3/AbstractVocabularyTermTest.java  | 202 +++++++++++
 .../asapi/v3/CreateVocabularyTermTest.java    | 179 ++--------
 .../asapi/v3/DeleteVocabularyTermTest.java    |  94 +----
 .../asapi/v3/SearchVocabularyTermTest.java    |  38 +-
 .../asapi/v3/UpdateVocabularyTermTest.java    | 326 ++++++++++++++++++
 .../asapi/v3/IApplicationServerApi.java       |   3 +
 .../update/VocabularyTermUpdate.java          | 111 ++++++
 .../generic/sharedapi/v3/dictionary.txt       |   6 +-
 16 files changed, 1163 insertions(+), 269 deletions(-)
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/IUpdateVocabularyTermMethodExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/UpdateVocabularyTermMethodExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermAuthorizationExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermAuthorizationExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermExecutor.java
 create mode 100644 openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractVocabularyTermTest.java
 create mode 100644 openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateVocabularyTermTest.java
 create mode 100644 openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/vocabulary/update/VocabularyTermUpdate.java

diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
index 028b39ebf45..762bc7ea6c6 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
@@ -95,6 +95,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.Vocabula
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.IConfirmDeletionMethodExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.ICreateDataSetMethodExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.ICreateExperimentMethodExecutor;
@@ -135,6 +136,7 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.IUpdateMateri
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.IUpdateProjectMethodExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.IUpdateSampleMethodExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.IUpdateSpaceMethodExecutor;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.method.IUpdateVocabularyTermMethodExecutor;
 import ch.systemsx.cisd.openbis.common.spring.IInvocationLoggerContext;
 import ch.systemsx.cisd.openbis.generic.server.AbstractServer;
 import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.Capability;
@@ -202,6 +204,9 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Autowired
     private IUpdateMaterialMethodExecutor updateMaterialExecutor;
 
+    @Autowired
+    private IUpdateVocabularyTermMethodExecutor updateVocabularyTermExecutor;
+
     @Autowired
     private IMapSpaceMethodExecutor mapSpaceExecutor;
 
@@ -453,6 +458,15 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
         updateDataSetExecutor.update(sessionToken, updates);
     }
 
+    @Override
+    @Transactional
+    // @RolesAllowed and @Capability are checked later depending whether an official or unofficial term is updated
+    @DatabaseUpdateModification(value = ObjectKind.VOCABULARY_TERM)
+    public void updateVocabularyTerms(String sessionToken, List<VocabularyTermUpdate> vocabularyTermUpdates)
+    {
+        updateVocabularyTermExecutor.update(sessionToken, vocabularyTermUpdates);
+    }
+
     @Override
     @Transactional(readOnly = true)
     @RolesAllowed({ RoleWithHierarchy.SPACE_OBSERVER, RoleWithHierarchy.SPACE_ETL_SERVER })
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java
index 61ce60de171..2ed497f426d 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java
@@ -91,6 +91,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.Vocabula
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
 import ch.systemsx.cisd.authentication.ISessionManager;
 import ch.systemsx.cisd.openbis.common.spring.IInvocationLoggerContext;
 import ch.systemsx.cisd.openbis.generic.shared.AbstractServerLogger;
@@ -223,6 +224,12 @@ public class ApplicationServerApiLogger extends AbstractServerLogger implements
         logAccess(sessionToken, "update-materials", "MATERIAL_UPDATES(%s)", abbreviate(materialUpdates));
     }
 
+    @Override
+    public void updateVocabularyTerms(String sessionToken, List<VocabularyTermUpdate> vocabularyTermUpdates)
+    {
+        logAccess(sessionToken, "update-vocabulary-terms", "VOCABULARY_TERM_UPDATES(%s)", abbreviate(vocabularyTermUpdates));
+    }
+
     @Override
     public Map<ISpaceId, Space> mapSpaces(String sessionToken, List<? extends ISpaceId> spaceIds, SpaceFetchOptions fetchOptions)
     {
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/IUpdateVocabularyTermMethodExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/IUpdateVocabularyTermMethodExecutor.java
new file mode 100644
index 00000000000..bc9658e0044
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/IUpdateVocabularyTermMethodExecutor.java
@@ -0,0 +1,27 @@
+/*
+ * 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.asapi.v3.executor.method;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
+
+/**
+ * @author pkupczyk
+ */
+public interface IUpdateVocabularyTermMethodExecutor extends IUpdateMethodExecutor<VocabularyTermUpdate>
+{
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/UpdateVocabularyTermMethodExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/UpdateVocabularyTermMethodExecutor.java
new file mode 100644
index 00000000000..15b3cafa7ef
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/method/UpdateVocabularyTermMethodExecutor.java
@@ -0,0 +1,44 @@
+/*
+
+ * 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.asapi.v3.executor.method;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.IUpdateEntityExecutor;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.vocabulary.IUpdateVocabularyTermExecutor;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class UpdateVocabularyTermMethodExecutor extends AbstractUpdateMethodExecutor<VocabularyTermUpdate>
+        implements IUpdateVocabularyTermMethodExecutor
+{
+
+    @Autowired
+    private IUpdateVocabularyTermExecutor updateExecutor;
+
+    @Override
+    protected IUpdateEntityExecutor<VocabularyTermUpdate> getUpdateExecutor()
+    {
+        return updateExecutor;
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermAuthorizationExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermAuthorizationExecutor.java
new file mode 100644
index 00000000000..3257564974a
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermAuthorizationExecutor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.asapi.v3.executor.vocabulary;
+
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+
+/**
+ * @author pkupczyk
+ */
+public interface IUpdateVocabularyTermAuthorizationExecutor
+{
+
+    public void checkUpdateOfficialTerm(IOperationContext context);
+
+    public void checkUpdateUnofficialTerm(IOperationContext context);
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermExecutor.java
new file mode 100644
index 00000000000..65c739fabe1
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/IUpdateVocabularyTermExecutor.java
@@ -0,0 +1,28 @@
+/*
+ * 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.asapi.v3.executor.vocabulary;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.IUpdateEntityExecutor;
+
+/**
+ * @author pkupczyk
+ */
+public interface IUpdateVocabularyTermExecutor extends IUpdateEntityExecutor<VocabularyTermUpdate>
+{
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermAuthorizationExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermAuthorizationExecutor.java
new file mode 100644
index 00000000000..2ec33b41a14
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermAuthorizationExecutor.java
@@ -0,0 +1,49 @@
+/*
+ * 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.asapi.v3.executor.vocabulary;
+
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.Capability;
+import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.RolesAllowed;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class UpdateVocabularyTermAuthorizationExecutor implements IUpdateVocabularyTermAuthorizationExecutor
+{
+
+    @Override
+    @RolesAllowed({ RoleWithHierarchy.SPACE_POWER_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @Capability("UPDATE_OFFICIAL_VOCABULARY_TERM")
+    public void checkUpdateOfficialTerm(IOperationContext context)
+    {
+        // do nothing - authorization is handled by the annotations
+    }
+
+    @Override
+    @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
+    @Capability("UPDATE_UNOFFICIAL_VOCABULARY_TERM")
+    public void checkUpdateUnofficialTerm(IOperationContext context)
+    {
+        // do nothing - authorization is handled by the annotations
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermExecutor.java
new file mode 100644
index 00000000000..43b3982e1cf
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/vocabulary/UpdateVocabularyTermExecutor.java
@@ -0,0 +1,273 @@
+/*
+ * 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.asapi.v3.executor.vocabulary;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Resource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
+import ch.ethz.sis.openbis.generic.asapi.v3.exceptions.ObjectNotFoundException;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.server.ComponentNames;
+import ch.systemsx.cisd.openbis.generic.server.business.bo.ICommonBusinessObjectFactory;
+import ch.systemsx.cisd.openbis.generic.server.business.bo.IVocabularyTermBO;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.IVocabularyTermUpdates;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy.RoleCode;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.VocabularyTerm;
+import ch.systemsx.cisd.openbis.generic.shared.dto.RoleAssignmentPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyTermPE;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class UpdateVocabularyTermExecutor implements IUpdateVocabularyTermExecutor
+{
+
+    @Resource(name = ComponentNames.COMMON_BUSINESS_OBJECT_FACTORY)
+    private ICommonBusinessObjectFactory businessObjectFactory;
+
+    @Autowired
+    private IMapVocabularyTermByIdExecutor mapVocabularyTermByIdExecutor;
+
+    @Autowired
+    private IUpdateVocabularyTermAuthorizationExecutor authorizationExecutor;
+
+    @Override
+    public void update(IOperationContext context, List<VocabularyTermUpdate> updates)
+    {
+        checkData(context, updates);
+
+        final Map<IVocabularyTermId, VocabularyTermPE> terms = getTermsMap(context, updates);
+        final Map<IVocabularyTermId, VocabularyTermPE> previousTerms = getPreviousTermsMap(context, updates);
+
+        checkAccess(context, updates, terms);
+
+        IVocabularyTermBO termBO = businessObjectFactory.createVocabularyTermBO(context.getSession());
+
+        for (final VocabularyTermUpdate update : updates)
+        {
+            final VocabularyTermPE termPE = terms.get(update.getVocabularyTermId());
+
+            if (update.getDescription().isModified() || update.getLabel().isModified() || update.getPreviousTermId().isModified())
+            {
+                termBO.update(new IVocabularyTermUpdates()
+                    {
+                        @Override
+                        public Long getId()
+                        {
+                            return termPE.getId();
+                        }
+
+                        @Override
+                        public String getCode()
+                        {
+                            return termPE.getCode();
+                        }
+
+                        @Override
+                        public String getLabel()
+                        {
+                            return update.getLabel().isModified() ? update.getLabel().getValue() : termPE.getLabel();
+                        }
+
+                        @Override
+                        public String getDescription()
+                        {
+                            return update.getDescription().isModified() ? update.getDescription().getValue() : termPE.getDescription();
+                        }
+
+                        @Override
+                        public Long getOrdinal()
+                        {
+                            if (update.getPreviousTermId().isModified())
+                            {
+                                if (update.getPreviousTermId().getValue() == null)
+                                {
+                                    Long minOrdinal = termPE.getOrdinal();
+
+                                    for (VocabularyTermPE otherTermPE : termPE.getVocabulary().getTerms())
+                                    {
+                                        if (minOrdinal > otherTermPE.getOrdinal())
+                                        {
+                                            minOrdinal = otherTermPE.getOrdinal();
+                                        }
+                                    }
+
+                                    return minOrdinal;
+                                } else
+                                {
+                                    VocabularyTermPE previousTermPE = previousTerms.get(update.getPreviousTermId().getValue());
+
+                                    if (false == previousTermPE.getVocabulary().equals(termPE.getVocabulary()))
+                                    {
+                                        throw new UserFailureException("Position of term " + update.getVocabularyTermId()
+                                                + " could not be found as the specified previous term " + update.getPreviousTermId().getValue()
+                                                + " is in a different vocabulary (" + previousTermPE.getVocabulary().getCode() + ").");
+                                    }
+
+                                    return previousTermPE.getOrdinal() + 1;
+                                }
+                            } else
+                            {
+                                return termPE.getOrdinal();
+                            }
+                        }
+
+                        @Override
+                        public Date getModificationDate()
+                        {
+                            return termPE.getModificationDate();
+                        }
+
+                    });
+            }
+
+            if (update.isOfficial().isModified())
+            {
+                if (termPE.isOfficial() && Boolean.FALSE.equals(update.isOfficial().getValue()))
+                {
+                    throw new UserFailureException(
+                            "Offical vocabulary term " + update.getVocabularyTermId() + " cannot be updated to be unofficial.");
+                }
+                VocabularyTerm term = new VocabularyTerm();
+                term.setId(termPE.getId());
+                termBO.makeOfficial(Arrays.asList(term));
+            }
+        }
+    }
+
+    private void checkData(IOperationContext context, Collection<VocabularyTermUpdate> updates)
+    {
+        for (VocabularyTermUpdate update : updates)
+        {
+            if (update.getVocabularyTermId() == null)
+            {
+                throw new UserFailureException("Vocabulary term id cannot be null");
+            }
+        }
+    }
+
+    private void checkAccess(IOperationContext context, Collection<VocabularyTermUpdate> updates, Map<IVocabularyTermId, VocabularyTermPE> terms)
+    {
+        boolean allowedToChangeInternallyManaged = isAllowedToChangeInternallyManaged(context);
+        boolean hasOfficial = false;
+        boolean hasUnofficial = false;
+
+        for (VocabularyTermUpdate update : updates)
+        {
+            VocabularyTermPE term = terms.get(update.getVocabularyTermId());
+
+            if (term.getVocabulary().isManagedInternally() && false == allowedToChangeInternallyManaged)
+            {
+                throw new UserFailureException("Not allowed to update terms of an internally managed vocabulary");
+            }
+
+            if (term.isOfficial() || (update.isOfficial().isModified() && Boolean.TRUE.equals(update.isOfficial().getValue())))
+            {
+                hasOfficial = true;
+            } else
+            {
+                hasUnofficial = true;
+            }
+        }
+
+        if (hasOfficial)
+        {
+            authorizationExecutor.checkUpdateOfficialTerm(context);
+        }
+        if (hasUnofficial)
+        {
+            authorizationExecutor.checkUpdateUnofficialTerm(context);
+        }
+    }
+
+    private Map<IVocabularyTermId, VocabularyTermPE> getTermsMap(IOperationContext context, List<VocabularyTermUpdate> updates)
+    {
+        Collection<IVocabularyTermId> ids = new HashSet<IVocabularyTermId>();
+
+        for (VocabularyTermUpdate update : updates)
+        {
+            ids.add(update.getVocabularyTermId());
+        }
+
+        Map<IVocabularyTermId, VocabularyTermPE> termsMap = mapVocabularyTermByIdExecutor.map(context, ids);
+
+        for (IVocabularyTermId id : ids)
+        {
+            if (termsMap.get(id) == null)
+            {
+                throw new ObjectNotFoundException(id);
+            }
+        }
+
+        return termsMap;
+    }
+
+    private Map<IVocabularyTermId, VocabularyTermPE> getPreviousTermsMap(IOperationContext context, List<VocabularyTermUpdate> updates)
+    {
+        Collection<IVocabularyTermId> ids = new HashSet<IVocabularyTermId>();
+
+        for (VocabularyTermUpdate update : updates)
+        {
+            if (update.getPreviousTermId().getValue() != null)
+            {
+                ids.add(update.getPreviousTermId().getValue());
+            }
+        }
+
+        Map<IVocabularyTermId, VocabularyTermPE> termsMap = mapVocabularyTermByIdExecutor.map(context, ids);
+
+        for (IVocabularyTermId id : ids)
+        {
+            if (termsMap.get(id) == null)
+            {
+                throw new ObjectNotFoundException(id);
+            }
+        }
+
+        return termsMap;
+    }
+
+    private boolean isAllowedToChangeInternallyManaged(IOperationContext context)
+    {
+        Set<RoleAssignmentPE> roles = context.getSession().tryGetCreatorPerson().getAllPersonRoles();
+
+        for (RoleAssignmentPE role : roles)
+        {
+            if (RoleCode.ETL_SERVER.equals(role.getRole()) || (RoleCode.ADMIN.equals(role.getRole()) && role.getSpace() == null))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractVocabularyTermTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractVocabularyTermTest.java
new file mode 100644
index 00000000000..a6dc075fcf5
--- /dev/null
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractVocabularyTermTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2016 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.asapi.v3;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.IDataSetId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.IExperimentId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.fetchoptions.MaterialFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.IMaterialId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.MaterialPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.ISampleId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.VocabularyTerm;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
+
+/**
+ * @author pkupczyk
+ */
+public class AbstractVocabularyTermTest extends AbstractTest
+{
+
+    protected List<VocabularyTerm> listTerms(IVocabularyId vocabularyId)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        VocabularyTermSearchCriteria criteria = new VocabularyTermSearchCriteria();
+        criteria.withVocabulary().withCode().thatEquals(((VocabularyPermId) vocabularyId).getPermId());
+
+        VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
+        fetchOptions.sortBy().ordinal().asc();
+
+        SearchResult<VocabularyTerm> results = v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
+
+        v3api.logout(sessionToken);
+
+        return results.getObjects();
+    }
+
+    protected void assertTerms(List<VocabularyTerm> actualTerms, String... expectedCodes)
+    {
+        List<String> actualCodes = new ArrayList<String>();
+
+        for (VocabularyTerm actualTerm : actualTerms)
+        {
+            actualCodes.add(actualTerm.getCode());
+        }
+
+        assertEquals(actualCodes, Arrays.asList(expectedCodes),
+                "Actual codes: " + actualCodes + ", Expected codes: " + Arrays.asList(expectedCodes));
+    }
+
+    protected VocabularyTerm searchTerm(IVocabularyTermId id, VocabularyTermFetchOptions fetchOptions)
+    {
+        return searchTerms(Arrays.asList(id), fetchOptions).get(0);
+    }
+
+    protected List<VocabularyTerm> searchTerms(List<IVocabularyTermId> ids, VocabularyTermFetchOptions fetchOptions)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        VocabularyTermSearchCriteria criteria = new VocabularyTermSearchCriteria();
+        criteria.withOrOperator();
+        for (IVocabularyTermId id : ids)
+        {
+            criteria.withId().thatEquals(id);
+        }
+
+        SearchResult<VocabularyTerm> searchResult =
+                v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
+
+        assertEquals(searchResult.getObjects().size(), ids.size());
+
+        v3api.logout(sessionToken);
+
+        return searchResult.getObjects();
+    }
+
+    protected List<VocabularyTerm> searchTerms(VocabularyTermSearchCriteria criteria, VocabularyTermFetchOptions fetchOptions)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        SearchResult<VocabularyTerm> searchResult = v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
+
+        v3api.logout(sessionToken);
+
+        return searchResult.getObjects();
+    }
+
+    protected List<VocabularyTerm> searchTerms(String vocabularyCode)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        VocabularyTermSearchCriteria criteria = new VocabularyTermSearchCriteria();
+        criteria.withVocabulary().withCode().thatEquals(vocabularyCode);
+
+        VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
+        fetchOptions.sortBy().ordinal().asc();
+
+        SearchResult<VocabularyTerm> result = v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
+        v3api.logout(sessionToken);
+        return result.getObjects();
+    }
+
+    protected Experiment getExperiment(String permId)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        ExperimentPermId id = new ExperimentPermId(permId);
+
+        ExperimentFetchOptions fetchOptions = new ExperimentFetchOptions();
+        fetchOptions.withProperties();
+
+        Map<IExperimentId, Experiment> map = v3api.mapExperiments(sessionToken, Arrays.asList(id), fetchOptions);
+        assertEquals(map.size(), 1);
+
+        v3api.logout(sessionToken);
+
+        return map.get(id);
+    }
+
+    protected Sample getSample(String permId)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        SamplePermId id = new SamplePermId(permId);
+
+        SampleFetchOptions fetchOptions = new SampleFetchOptions();
+        fetchOptions.withProperties();
+
+        Map<ISampleId, Sample> map = v3api.mapSamples(sessionToken, Arrays.asList(id), fetchOptions);
+        assertEquals(map.size(), 1);
+
+        v3api.logout(sessionToken);
+
+        return map.get(id);
+    }
+
+    protected DataSet getDataSet(String permId)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        DataSetPermId id = new DataSetPermId(permId);
+
+        DataSetFetchOptions fetchOptions = new DataSetFetchOptions();
+        fetchOptions.withProperties();
+
+        Map<IDataSetId, DataSet> map = v3api.mapDataSets(sessionToken, Arrays.asList(id), fetchOptions);
+        assertEquals(map.size(), 1);
+
+        return map.get(id);
+    }
+
+    protected Material getMaterial(String materialCode, String materialTypeCode)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        MaterialPermId id = new MaterialPermId(materialCode, materialTypeCode);
+
+        MaterialFetchOptions fetchOptions = new MaterialFetchOptions();
+        fetchOptions.withProperties();
+
+        Map<IMaterialId, Material> map = v3api.mapMaterials(sessionToken, Arrays.asList(id), fetchOptions);
+        assertEquals(map.size(), 1);
+
+        return map.get(id);
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateVocabularyTermTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateVocabularyTermTest.java
index 826121c51dc..14088f883ed 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateVocabularyTermTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateVocabularyTermTest.java
@@ -17,9 +17,7 @@
 package ch.ethz.sis.openbis.systemtest.asapi.v3;
 
 import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.fail;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -29,74 +27,49 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.VocabularyTerm;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.create.VocabularyTermCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
-import ch.systemsx.cisd.common.test.AssertionUtil;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
 
 /**
  * @author pkupczyk
  */
 @Test(groups = { "before remote api" })
-public class CreateVocabularyTermTest extends AbstractTest
+public class CreateVocabularyTermTest extends AbstractVocabularyTermTest
 {
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Vocabulary term vocabulary id cannot be null.*")
     public void testCreateWithVocabularyIdNull()
     {
-        try
-        {
-            String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-            VocabularyTermCreation creation = termCreation();
-            creation.setVocabularyId(null);
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
 
-            v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
+        VocabularyTermCreation creation = termCreation();
+        creation.setVocabularyId(null);
 
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains("Vocabulary term vocabulary id cannot be null", e.getMessage());
-        }
+        v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
     }
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Vocabulary term code cannot be null or empty.*")
     public void testCreateWithCodeNull()
     {
-        try
-        {
-            String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-            VocabularyTermCreation creation = termCreation();
-            creation.setCode(null);
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
 
-            v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
+        VocabularyTermCreation creation = termCreation();
+        creation.setCode(null);
 
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains("Vocabulary term code cannot be null or empty", e.getMessage());
-        }
+        v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
     }
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Vocabulary term HUMAN \\(ORGANISM\\) already exists.*")
     public void testCreateWithCodeDuplicated()
     {
-        try
-        {
-            String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-            VocabularyTermCreation creation = termCreation();
-            creation.setCode("HUMAN");
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
 
-            v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
+        VocabularyTermCreation creation = termCreation();
+        creation.setCode("HUMAN");
 
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains("Vocabulary term HUMAN (ORGANISM) already exists", e.getMessage());
-        }
+        v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
     }
 
     @Test
@@ -107,22 +80,12 @@ public class CreateVocabularyTermTest extends AbstractTest
         createTerms(TEST_USER, PASSWORD, creation);
     }
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
     public void testCreateWithOfficalTermAndUnauthorizedUser()
     {
-        try
-        {
-            VocabularyTermCreation creation = termCreation();
-            creation.setOfficial(true);
-            createTerms(TEST_GROUP_OBSERVER, PASSWORD, creation);
-
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains(
-                    "None of method roles '[SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER]' could be found in roles of user 'observer'",
-                    e.getMessage());
-        }
+        VocabularyTermCreation creation = termCreation();
+        creation.setOfficial(true);
+        createTerms(TEST_GROUP_OBSERVER, PASSWORD, creation);
     }
 
     @Test
@@ -149,22 +112,12 @@ public class CreateVocabularyTermTest extends AbstractTest
         createTerms(TEST_USER, PASSWORD, creation);
     }
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_USER, SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
     public void testCreateWithUnofficalTermAndUnauthorizedUser()
     {
-        try
-        {
-            VocabularyTermCreation creation = termCreation();
-            creation.setOfficial(false);
-            createTerms(TEST_GROUP_OBSERVER, PASSWORD, creation);
-
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains(
-                    "None of method roles '[SPACE_USER, SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER]' could be found in roles of user 'observer'",
-                    e.getMessage());
-        }
+        VocabularyTermCreation creation = termCreation();
+        creation.setOfficial(false);
+        createTerms(TEST_GROUP_OBSERVER, PASSWORD, creation);
     }
 
     @Test
@@ -190,55 +143,27 @@ public class CreateVocabularyTermTest extends AbstractTest
         createTerms(TEST_USER, PASSWORD, creation);
     }
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Not allowed to add terms to an internally managed vocabulary.*")
     public void testCreateWithInternallyManagedVocabularyAndUnauthorizedUser()
     {
-        try
-        {
-            VocabularyTermCreation creation = termCreationInternallyManaged();
-            createTerms(TEST_SPACE_USER, PASSWORD, creation);
-
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains("Not allowed to add terms to an internally managed vocabulary", e.getMessage());
-        }
+        VocabularyTermCreation creation = termCreationInternallyManaged();
+        createTerms(TEST_SPACE_USER, PASSWORD, creation);
     }
 
-    @Test
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Position of term TIGER \\(ORGANISM\\) could not be found as the specified previous term IDONTEXIST \\(ORGANISM\\) does not exist.*")
     public void testCreateWithPreviousTermNonexistent()
     {
-        try
-        {
-            VocabularyTermCreation creation = termCreation();
-            creation.setPreviousTermId(new VocabularyTermPermId("IDONTEXIST", "ORGANISM"));
-            createTerms(TEST_USER, PASSWORD, creation);
-
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains(
-                    "Position of term TIGER (ORGANISM) could not be found as the specified previous term IDONTEXIST (ORGANISM) does not exist",
-                    e.getMessage());
-        }
+        VocabularyTermCreation creation = termCreation();
+        creation.setPreviousTermId(new VocabularyTermPermId("IDONTEXIST", "ORGANISM"));
+        createTerms(TEST_USER, PASSWORD, creation);
     }
 
-    @Test
-    public void testCreateWithPreviousTermFromDifferentDictionary()
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Position of term TIGER \\(ORGANISM\\) could not be found as the specified previous term MALE \\(GENDER\\) is in a different vocabulary \\(GENDER\\).*")
+    public void testCreateWithPreviousTermFromDifferentVocabulary()
     {
-        try
-        {
-            VocabularyTermCreation creation = termCreation();
-            creation.setPreviousTermId(new VocabularyTermPermId("MALE", "GENDER"));
-            createTerms(TEST_USER, PASSWORD, creation);
-
-            fail();
-        } catch (Exception e)
-        {
-            AssertionUtil.assertContains(
-                    "Position of term TIGER (ORGANISM) could not be found as the specified previous term MALE (GENDER) is in a different vocabulary (GENDER)",
-                    e.getMessage());
-        }
+        VocabularyTermCreation creation = termCreation();
+        creation.setPreviousTermId(new VocabularyTermPermId("MALE", "GENDER"));
+        createTerms(TEST_USER, PASSWORD, creation);
     }
 
     @Test
@@ -389,34 +314,4 @@ public class CreateVocabularyTermTest extends AbstractTest
         assertTerms(termsAfter, "RAT", "DOG", "HUMAN", creation.getCode(), "GORILLA", "FLY");
     }
 
-    private List<VocabularyTerm> listTerms(IVocabularyId vocabularyId)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        VocabularyTermSearchCriteria criteria = new VocabularyTermSearchCriteria();
-        criteria.withVocabulary().withCode().thatEquals(((VocabularyPermId) vocabularyId).getPermId());
-
-        VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
-        fetchOptions.sortBy().ordinal().asc();
-
-        SearchResult<VocabularyTerm> results = v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
-
-        v3api.logout(sessionToken);
-
-        return results.getObjects();
-    }
-
-    private void assertTerms(List<VocabularyTerm> actualTerms, String... expectedCodes)
-    {
-        List<String> actualCodes = new ArrayList<String>();
-
-        for (VocabularyTerm actualTerm : actualTerms)
-        {
-            actualCodes.add(actualTerm.getCode());
-        }
-
-        assertEquals(actualCodes, Arrays.asList(expectedCodes),
-                "Actual codes: " + actualCodes + ", Expected codes: " + Arrays.asList(expectedCodes));
-    }
-
 }
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/DeleteVocabularyTermTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/DeleteVocabularyTermTest.java
index 2a8b0b94daa..65c11b350a6 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/DeleteVocabularyTermTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/DeleteVocabularyTermTest.java
@@ -20,42 +20,26 @@ import static org.testng.Assert.assertEquals;
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 
 import org.testng.annotations.Test;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.IDataSetId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentPermId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.IExperimentId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.fetchoptions.MaterialFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.IMaterialId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.MaterialPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.ISampleId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.VocabularyTerm;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.create.VocabularyTermCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.delete.VocabularyTermDeletionOptions;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPermId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
 import ch.systemsx.cisd.common.exceptions.UserFailureException;
 
 /**
  * @author pkupczyk
  */
 @Test(groups = { "before remote api" })
-public class DeleteVocabularyTermTest extends AbstractDeletionTest
+public class DeleteVocabularyTermTest extends AbstractVocabularyTermTest
 {
 
     @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
@@ -436,80 +420,4 @@ public class DeleteVocabularyTermTest extends AbstractDeletionTest
         assertVocabularyTermPermIds(terms, termIdRat, termIdDog, termIdHuman, termIdGorilla);
     }
 
-    private List<VocabularyTerm> searchTerms(String vocabularyCode)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        VocabularyTermSearchCriteria criteria = new VocabularyTermSearchCriteria();
-        criteria.withVocabulary().withCode().thatEquals(vocabularyCode);
-
-        SearchResult<VocabularyTerm> result = v3api.searchVocabularyTerms(sessionToken, criteria, new VocabularyTermFetchOptions());
-        v3api.logout(sessionToken);
-        return result.getObjects();
-    }
-
-    private Experiment getExperiment(String permId)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        ExperimentPermId id = new ExperimentPermId(permId);
-
-        ExperimentFetchOptions fetchOptions = new ExperimentFetchOptions();
-        fetchOptions.withProperties();
-
-        Map<IExperimentId, Experiment> map = v3api.mapExperiments(sessionToken, Arrays.asList(id), fetchOptions);
-        assertEquals(map.size(), 1);
-
-        v3api.logout(sessionToken);
-
-        return map.get(id);
-    }
-
-    private Sample getSample(String permId)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        SamplePermId id = new SamplePermId(permId);
-
-        SampleFetchOptions fetchOptions = new SampleFetchOptions();
-        fetchOptions.withProperties();
-
-        Map<ISampleId, Sample> map = v3api.mapSamples(sessionToken, Arrays.asList(id), fetchOptions);
-        assertEquals(map.size(), 1);
-
-        v3api.logout(sessionToken);
-
-        return map.get(id);
-    }
-
-    private DataSet getDataSet(String permId)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        DataSetPermId id = new DataSetPermId(permId);
-
-        DataSetFetchOptions fetchOptions = new DataSetFetchOptions();
-        fetchOptions.withProperties();
-
-        Map<IDataSetId, DataSet> map = v3api.mapDataSets(sessionToken, Arrays.asList(id), fetchOptions);
-        assertEquals(map.size(), 1);
-
-        return map.get(id);
-    }
-
-    private Material getMaterial(String materialCode, String materialTypeCode)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        MaterialPermId id = new MaterialPermId(materialCode, materialTypeCode);
-
-        MaterialFetchOptions fetchOptions = new MaterialFetchOptions();
-        fetchOptions.withProperties();
-
-        Map<IMaterialId, Material> map = v3api.mapMaterials(sessionToken, Arrays.asList(id), fetchOptions);
-        assertEquals(map.size(), 1);
-
-        return map.get(id);
-    }
-
 }
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchVocabularyTermTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchVocabularyTermTest.java
index 2336ae2dc5b..ef341648a1e 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchVocabularyTermTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/SearchVocabularyTermTest.java
@@ -32,14 +32,14 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTerm
  * @author pkupczyk
  */
 @Test(groups = { "before remote api" })
-public class SearchVocabularyTermTest extends AbstractTest
+public class SearchVocabularyTermTest extends AbstractVocabularyTermTest
 {
 
     @Test
     public void testSearchWithEmptyFetchOptions()
     {
         VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
-        VocabularyTerm term = search(new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"), fetchOptions);
+        VocabularyTerm term = searchTerm(new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"), fetchOptions);
 
         assertEquals(term.getPermId(), new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"));
         assertEquals(term.getCode(), "PROPRIETARY");
@@ -59,7 +59,7 @@ public class SearchVocabularyTermTest extends AbstractTest
         VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
         fetchOptions.withVocabulary().withRegistrator();
 
-        VocabularyTerm term = search(new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"), fetchOptions);
+        VocabularyTerm term = searchTerm(new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"), fetchOptions);
 
         assertEquals(term.getPermId(), new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"));
         assertEquals(term.getCode(), "PROPRIETARY");
@@ -84,7 +84,7 @@ public class SearchVocabularyTermTest extends AbstractTest
         VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
         fetchOptions.withRegistrator();
 
-        VocabularyTerm term = search(new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"), fetchOptions);
+        VocabularyTerm term = searchTerm(new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"), fetchOptions);
 
         assertEquals(term.getPermId(), new VocabularyTermPermId("PROPRIETARY", "$STORAGE_FORMAT"));
         assertEquals(term.getCode(), "PROPRIETARY");
@@ -249,7 +249,7 @@ public class SearchVocabularyTermTest extends AbstractTest
         VocabularyTermFetchOptions fetchOptions = new VocabularyTermFetchOptions();
         fetchOptions.sortBy().code().asc();
 
-        List<VocabularyTerm> terms = search(criteria, fetchOptions);
+        List<VocabularyTerm> terms = searchTerms(criteria, fetchOptions);
 
         assertVocabularyTermPermIds(terms, expectedPermIds);
     }
@@ -268,32 +268,4 @@ public class SearchVocabularyTermTest extends AbstractTest
         return searchResult.getObjects();
     }
 
-    private VocabularyTerm search(VocabularyTermPermId permId, VocabularyTermFetchOptions fetchOptions)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        VocabularyTermSearchCriteria criteria = new VocabularyTermSearchCriteria();
-        criteria.withId().thatEquals(permId);
-
-        SearchResult<VocabularyTerm> searchResult =
-                v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
-
-        assertEquals(searchResult.getObjects().size(), 1);
-
-        v3api.logout(sessionToken);
-
-        return searchResult.getObjects().get(0);
-    }
-
-    private List<VocabularyTerm> search(VocabularyTermSearchCriteria criteria, VocabularyTermFetchOptions fetchOptions)
-    {
-        String sessionToken = v3api.login(TEST_USER, PASSWORD);
-
-        SearchResult<VocabularyTerm> searchResult = v3api.searchVocabularyTerms(sessionToken, criteria, fetchOptions);
-
-        v3api.logout(sessionToken);
-
-        return searchResult.getObjects();
-    }
-
 }
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateVocabularyTermTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateVocabularyTermTest.java
new file mode 100644
index 00000000000..c8377bb2362
--- /dev/null
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateVocabularyTermTest.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2016 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.asapi.v3;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.VocabularyTerm;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.create.VocabularyTermCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+
+/**
+ * @author pkupczyk
+ */
+@Test(groups = { "before remote api" })
+public class UpdateVocabularyTermTest extends AbstractVocabularyTermTest
+{
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Vocabulary term id cannot be null.*")
+    public void testUpdateWithVocabularyIdNull()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        updateTerms(TEST_USER, PASSWORD, update);
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Object with VocabularyTermPermId = \\[IDONTEXIST \\(MENEITHER\\)\\] has not been found.*")
+    public void testUpdateWithVocabularyIdNonexistent()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("IDONTEXIST", "MENEITHER"));
+        updateTerms(TEST_USER, PASSWORD, update);
+    }
+
+    @Test
+    public void testUpdateWithOfficalTermAndAuthorizedUser()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getOfficialTermId());
+        update.setDescription("Updated offical term");
+
+        List<VocabularyTerm> terms = updateTerms(TEST_USER, PASSWORD, update);
+
+        assertEquals(terms.get(0).getDescription(), update.getDescription().getValue());
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
+    public void testUpdateWithOfficalTermAndUnauthorizedUser()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getOfficialTermId());
+        update.setDescription("Updated offical term");
+
+        updateTerms(TEST_GROUP_OBSERVER, PASSWORD, update);
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Offical vocabulary term DOG \\(ORGANISM\\) cannot be updated to be unofficial.*")
+    public void testUpdateWithOfficialTermMadeUnofficial()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getOfficialTermId());
+        update.setOfficial(false);
+
+        updateTerms(TEST_USER, PASSWORD, update);
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
+    public void testUpdateWithOfficialTermMadeUnofficialUnauthorized()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getOfficialTermId());
+        update.setOfficial(false);
+
+        updateTerms(TEST_GROUP_OBSERVER, PASSWORD, update);
+    }
+
+    @Test
+    public void testUpdateWithUnofficalTermAndAuthorizedUser()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getUnofficialTermId());
+        update.setDescription("Updated unofficial term");
+
+        List<VocabularyTerm> terms = updateTerms(TEST_USER, PASSWORD, update);
+
+        assertEquals(terms.get(0).getDescription(), update.getDescription().getValue());
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_USER, SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
+    public void testUpdateWithUnofficalTermAndUnauthorizedUser()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getUnofficialTermId());
+        update.setDescription("Updated unofficial term");
+
+        List<VocabularyTerm> terms = updateTerms(TEST_GROUP_OBSERVER, PASSWORD, update);
+
+        assertEquals(terms.get(0).getDescription(), update.getDescription().getValue());
+    }
+
+    @Test
+    public void testUpdateWithUnofficialTermMadeOfficial()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getUnofficialTermId());
+        update.setOfficial(true);
+
+        List<VocabularyTerm> terms = updateTerms(TEST_USER, PASSWORD, update);
+
+        assertEquals(terms.get(0).isOfficial(), Boolean.TRUE);
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*None of method roles '\\[SPACE_POWER_USER, SPACE_ADMIN, INSTANCE_ADMIN, SPACE_ETL_SERVER, INSTANCE_ETL_SERVER\\]' could be found in roles of user 'observer'.*")
+    public void testUpdateWithUnofficialTermMadeOfficialUnauthorized()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getUnofficialTermId());
+        update.setOfficial(true);
+
+        updateTerms(TEST_GROUP_OBSERVER, PASSWORD, update);
+    }
+
+    @Test
+    public void testUpdateWithPreviousTermIdNull()
+    {
+        String vocabularyCode = "ORGANISM";
+
+        List<VocabularyTerm> termsBefore = searchTerms(vocabularyCode);
+        assertTerms(termsBefore, "RAT", "DOG", "HUMAN", "GORILLA", "FLY");
+
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("DOG", vocabularyCode));
+        update.setPreviousTermId(null);
+
+        updateTerms(TEST_USER, PASSWORD, update);
+
+        List<VocabularyTerm> termsAfter = searchTerms(vocabularyCode);
+        assertTerms(termsAfter, "DOG", "RAT", "HUMAN", "GORILLA", "FLY");
+    }
+
+    @Test
+    public void testUpdateWithPreviousTermIdNotNull()
+    {
+        String vocabularyCode = "ORGANISM";
+
+        List<VocabularyTerm> termsBefore = searchTerms(vocabularyCode);
+        assertTerms(termsBefore, "RAT", "DOG", "HUMAN", "GORILLA", "FLY");
+
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("DOG", vocabularyCode));
+        update.setPreviousTermId(new VocabularyTermPermId("GORILLA", vocabularyCode));
+
+        updateTerms(TEST_USER, PASSWORD, update);
+
+        List<VocabularyTerm> termsAfter = searchTerms(vocabularyCode);
+        assertTerms(termsAfter, "RAT", "HUMAN", "GORILLA", "DOG", "FLY");
+    }
+
+    @Test
+    public void testUpdateWithPreviousTermIdAndMultipleTerms()
+    {
+        String vocabularyCode = "ORGANISM";
+
+        List<VocabularyTerm> termsBefore = searchTerms(vocabularyCode);
+        assertTerms(termsBefore, "RAT", "DOG", "HUMAN", "GORILLA", "FLY");
+
+        VocabularyTermUpdate updateDog = new VocabularyTermUpdate();
+        updateDog.setVocabularyTermId(new VocabularyTermPermId("DOG", vocabularyCode));
+        updateDog.setPreviousTermId(new VocabularyTermPermId("GORILLA", vocabularyCode));
+
+        VocabularyTermUpdate updateGorilla = new VocabularyTermUpdate();
+        updateGorilla.setVocabularyTermId(new VocabularyTermPermId("GORILLA", vocabularyCode));
+        updateGorilla.setPreviousTermId(new VocabularyTermPermId("RAT", vocabularyCode));
+
+        updateTerms(TEST_USER, PASSWORD, updateDog, updateGorilla);
+
+        List<VocabularyTerm> termsAfter = searchTerms(vocabularyCode);
+        assertTerms(termsAfter, "RAT", "GORILLA", "HUMAN", "DOG", "FLY");
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Object with VocabularyTermPermId = \\[IDONTEXIST \\(ORGANISM\\)\\] has not been found.*")
+    public void testUpdateWithPreviousTermIdNonexistent()
+    {
+        String vocabularyCode = "ORGANISM";
+
+        List<VocabularyTerm> termsBefore = searchTerms(vocabularyCode);
+        assertTerms(termsBefore, "RAT", "DOG", "HUMAN", "GORILLA", "FLY");
+
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("DOG", vocabularyCode));
+        update.setPreviousTermId(new VocabularyTermPermId("IDONTEXIST", vocabularyCode));
+
+        updateTerms(TEST_USER, PASSWORD, update);
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Position of term DOG \\(ORGANISM\\) could not be found as the specified previous term MALE \\(GENDER\\) is in a different vocabulary \\(GENDER\\).*")
+    public void testUpdateWithPreviousTermIdFromDifferentVocabulary()
+    {
+        String vocabularyCode = "ORGANISM";
+
+        List<VocabularyTerm> termsBefore = searchTerms(vocabularyCode);
+        assertTerms(termsBefore, "RAT", "DOG", "HUMAN", "GORILLA", "FLY");
+
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("DOG", vocabularyCode));
+        update.setPreviousTermId(new VocabularyTermPermId("MALE", "GENDER"));
+
+        updateTerms(TEST_USER, PASSWORD, update);
+    }
+
+    @Test
+    public void testUpdateWithLabel()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("HUMAN", "ORGANISM"));
+        update.setLabel("a brand new label");
+
+        List<VocabularyTerm> terms = updateTerms(TEST_USER, PASSWORD, update);
+
+        assertEquals(terms.get(0).getLabel(), update.getLabel().getValue());
+    }
+
+    @Test
+    public void testUpdateWithDescription()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(new VocabularyTermPermId("HUMAN", "ORGANISM"));
+        update.setDescription("a brand new description");
+
+        List<VocabularyTerm> terms = updateTerms(TEST_USER, PASSWORD, update);
+
+        assertEquals(terms.get(0).getDescription(), update.getDescription().getValue());
+    }
+
+    @Test
+    public void testUpdateWithInternallyManagedVocabularyAndAuthorized()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getInternallyManagedTermId());
+        update.setDescription("a brand new description");
+
+        List<VocabularyTerm> terms = updateTerms(TEST_USER, PASSWORD, update);
+
+        assertEquals(terms.get(0).getDescription(), update.getDescription().getValue());
+    }
+
+    @Test(expectedExceptions = UserFailureException.class, expectedExceptionsMessageRegExp = ".*Not allowed to update terms of an internally managed vocabulary.*")
+    public void testUpdateWithInternallyManagedVocabularyAndUnauthorized()
+    {
+        VocabularyTermUpdate update = new VocabularyTermUpdate();
+        update.setVocabularyTermId(getInternallyManagedTermId());
+        update.setDescription("a brand new description");
+
+        updateTerms(TEST_GROUP_OBSERVER, PASSWORD, update);
+    }
+
+    @Test
+    public void testUpdateWithMultipleTerms()
+    {
+        // TODO
+    }
+
+    private List<VocabularyTerm> updateTerms(String user, String password, VocabularyTermUpdate... updates)
+    {
+        String sessionToken = v3api.login(user, password);
+
+        List<IVocabularyTermId> ids = new ArrayList<IVocabularyTermId>();
+        for (VocabularyTermUpdate update : updates)
+        {
+            ids.add(update.getVocabularyTermId());
+        }
+
+        v3api.updateVocabularyTerms(sessionToken, Arrays.asList(updates));
+
+        List<VocabularyTerm> terms = searchTerms(ids, new VocabularyTermFetchOptions());
+
+        return terms;
+    }
+
+    private VocabularyTermPermId getUnofficialTermId()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        VocabularyTermCreation creation = new VocabularyTermCreation();
+        creation.setCode("IAMUNOFFICIAL");
+        creation.setVocabularyId(new VocabularyPermId("ORGANISM"));
+        creation.setOfficial(false);
+
+        List<VocabularyTermPermId> permIds = v3api.createVocabularyTerms(sessionToken, Arrays.asList(creation));
+        return permIds.get(0);
+    }
+
+    private VocabularyTermPermId getOfficialTermId()
+    {
+        return new VocabularyTermPermId("DOG", "ORGANISM");
+    }
+
+    private VocabularyTermPermId getInternallyManagedTermId()
+    {
+        return new VocabularyTermPermId("96_WELLS_8X12", "$PLATE_GEOMETRY");
+    }
+
+}
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java
index 24993223a18..a17f18a6267 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java
@@ -90,6 +90,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.Vocabula
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.update.VocabularyTermUpdate;
 import ch.systemsx.cisd.common.api.IRpcService;
 
 /**
@@ -143,6 +144,8 @@ public interface IApplicationServerApi extends IRpcService
 
     public void updateMaterials(String sessionToken, List<MaterialUpdate> materialUpdates);
 
+    public void updateVocabularyTerms(String sessionToken, List<VocabularyTermUpdate> vocabularyTermUpdates);
+
     public Map<ISpaceId, Space> mapSpaces(String sessionToken, List<? extends ISpaceId> spaceIds,
             SpaceFetchOptions fetchOptions);
 
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/vocabulary/update/VocabularyTermUpdate.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/vocabulary/update/VocabularyTermUpdate.java
new file mode 100644
index 00000000000..083196382c4
--- /dev/null
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/vocabulary/update/VocabularyTermUpdate.java
@@ -0,0 +1,111 @@
+/*
+ * 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.asapi.v3.dto.vocabulary.update;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.FieldUpdateValue;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyTermId;
+import ch.systemsx.cisd.base.annotation.JsonObject;
+
+/**
+ * @author pkupczyk
+ */
+@JsonObject("as.dto.vocabulary.update.VocabularyTermUpdate")
+public class VocabularyTermUpdate implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty
+    private IVocabularyTermId vocabularyTermId;
+
+    @JsonProperty
+    private FieldUpdateValue<String> label = new FieldUpdateValue<String>();
+
+    @JsonProperty
+    private FieldUpdateValue<String> description = new FieldUpdateValue<String>();
+
+    @JsonProperty
+    private FieldUpdateValue<IVocabularyTermId> previousTermId = new FieldUpdateValue<IVocabularyTermId>();
+
+    @JsonProperty
+    private FieldUpdateValue<Boolean> official = new FieldUpdateValue<Boolean>();
+
+    @JsonIgnore
+    public IVocabularyTermId getVocabularyTermId()
+    {
+        return vocabularyTermId;
+    }
+
+    @JsonIgnore
+    public void setVocabularyTermId(IVocabularyTermId vocabularyTermId)
+    {
+        this.vocabularyTermId = vocabularyTermId;
+    }
+
+    @JsonIgnore
+    public void setLabel(String label)
+    {
+        this.label.setValue(label);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<String> getLabel()
+    {
+        return label;
+    }
+
+    @JsonIgnore
+    public void setDescription(String description)
+    {
+        this.description.setValue(description);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<String> getDescription()
+    {
+        return description;
+    }
+
+    @JsonIgnore
+    public void setPreviousTermId(IVocabularyTermId previousTermId)
+    {
+        this.previousTermId.setValue(previousTermId);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<IVocabularyTermId> getPreviousTermId()
+    {
+        return previousTermId;
+    }
+
+    @JsonIgnore
+    public void setOfficial(Boolean official)
+    {
+        this.official.setValue(official);
+    }
+
+    @JsonIgnore
+    public FieldUpdateValue<Boolean> isOfficial()
+    {
+        return official;
+    }
+
+}
diff --git a/openbis_api/sourceTest/java/ch/ethz/sis/openbis/generic/sharedapi/v3/dictionary.txt b/openbis_api/sourceTest/java/ch/ethz/sis/openbis/generic/sharedapi/v3/dictionary.txt
index f4cfe99ceb5..69194df39cc 100644
--- a/openbis_api/sourceTest/java/ch/ethz/sis/openbis/generic/sharedapi/v3/dictionary.txt
+++ b/openbis_api/sourceTest/java/ch/ethz/sis/openbis/generic/sharedapi/v3/dictionary.txt
@@ -1055,4 +1055,8 @@ get Replacements
 replace
 Vocabulary Term Deletion Options
 Create Samples Operation Result
-Vocabulary Search Criteria
\ No newline at end of file
+Vocabulary Search Criteria
+get Vocabulary Term Id
+set Vocabulary Term Id
+update Vocabulary Terms
+Vocabulary Term Update
\ No newline at end of file
-- 
GitLab