diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
index 9652cd18ec44e30daefa0f139253835c055742cb..a2df7d8aff09f3292cbde80229de4a47327f8d13 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
@@ -207,4 +207,8 @@ public class LDAPAuthenticationService implements IAuthenticationService
         return configured;
     }
 
+    public List<Principal> listPrincipalsByKeyValue(String key, String value)
+    {
+        return query.listPrincipalsByKeyValue(key, value);
+    }
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
index 1522ba4a397dd667d951aabb541e4600730bdbaa..13729994ed398288ac3019a62ce70c8fa05a9e62 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
@@ -78,6 +78,8 @@ public final class LDAPDirectoryConfiguration
 
     private String securityPrincipalPassword;
 
+    private String searchBase = "";
+
     /**
      * Returns <code>true</code> if this configuration is complete.
      */
@@ -322,6 +324,19 @@ public final class LDAPDirectoryConfiguration
             this.queryTemplate = queryTemplate;
         }
     }
+    
+    public String getSearchBase()
+    {
+        return searchBase;
+    }
+    
+    public void setSearchBase(String searchBase)
+    {
+        if (isResolved(searchBase))
+        {
+            this.searchBase = searchBase;
+        }
+    }
 
     /**
      * The read timeout (in s). Default value: <code>10s</code>
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPPrincipalQuery.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPPrincipalQuery.java
index c756ce12df881ddaf6345194c84920197fe18686..381c490751aeaefee04daffd08597f70dc3a07fb 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPPrincipalQuery.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPPrincipalQuery.java
@@ -284,7 +284,7 @@ public final class LDAPPrincipalQuery implements ISelfTestable
         {
             try
             {
-                return primListPrincipalsByKeyValue(key, value, additionalAttributesOrNull, limit);
+                return primListPrincipalsByKeyValue(config.getSearchBase(), key, value, additionalAttributesOrNull, limit);
             } catch (RuntimeException ex)
             {
                 contextHolder.set(null);
@@ -307,7 +307,7 @@ public final class LDAPPrincipalQuery implements ISelfTestable
         throw firstException;
     }
 
-    private List<Principal> primListPrincipalsByKeyValue(String key, String value,
+    private List<Principal> primListPrincipalsByKeyValue(String searchBase, String key, String value,
             Collection<String> additionalAttributesOrNull, int limit)
     {
         final List<Principal> principals = new ArrayList<Principal>();
@@ -318,7 +318,7 @@ public final class LDAPPrincipalQuery implements ISelfTestable
             final DirContext context = createContext(false);
             final SearchControls ctrl = new SearchControls();
             ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
-            final NamingEnumeration<SearchResult> enumeration = context.search("", query, ctrl);
+            final NamingEnumeration<SearchResult> enumeration = context.search(searchBase, query, ctrl);
             int count = 0;
             while (count++ < limit && enumeration.hasMore())
             {
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfigurationTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfigurationTest.java
index b9a518f92d94e81ff64d7b03a8709e48c16ff3e4..eb6fb42f845a32c9a2a4d8c70b0c81cd0da1e540 100644
--- a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfigurationTest.java
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfigurationTest.java
@@ -28,6 +28,25 @@ import org.testng.annotations.Test;
 public class LDAPDirectoryConfigurationTest
 {
 
+    @Test
+    public void testLDAPDirectoryConfigurationUnresolvedVariableSearchBase()
+    {
+        final LDAPDirectoryConfiguration config = new LDAPDirectoryConfiguration();
+        config.setSearchBase(" ");
+        assertEquals("", config.getSearchBase());
+        config.setQueryTemplate("${ldap.searchBase}");
+        assertEquals("", config.getSearchBase());
+    }
+    
+    @Test
+    public void testLDAPDirectoryConfigurationResolvedVariableSearchBase()
+    {
+        final LDAPDirectoryConfiguration config = new LDAPDirectoryConfiguration();
+        final String searchBase = "ou=a,o=b,c=c";
+        config.setSearchBase(searchBase);
+        assertEquals(searchBase, config.getSearchBase());
+    }
+
     @Test
     public void testLDAPDirectoryConfigurationUnresolvedVariableQueryTemplate()
     {
@@ -37,7 +56,7 @@ public class LDAPDirectoryConfigurationTest
         config.setQueryTemplate("${ldap.queryTemplate}");
         assertEquals(LDAPDirectoryConfiguration.DEFAULT_QUERY_TEMPLATE, config.getQueryTemplate());
     }
-
+    
     @Test
     public void testLDAPDirectoryConfigurationResolvedVariableQueryTemplate()
     {
diff --git a/common/source/java/genericCommonContext.xml b/common/source/java/genericCommonContext.xml
index f943fb1d092bc736a288bb46c5478cc02efa08f6..f9fac048565cc93e290b77f24248b70b8412a32d 100644
--- a/common/source/java/genericCommonContext.xml
+++ b/common/source/java/genericCommonContext.xml
@@ -66,6 +66,7 @@
         <property name="securityPrincipalDistinguishedName" value="${ldap.security.principal.distinguished.name}" />
         <property name="securityPrincipalPassword" value="${ldap.security.principal.password}" />
         <property name="referral" value="${ldap.referral}" /> 
+        <property name="searchBase" value="${ldap.searchBase}" /> 
         <property name="userIdAttributeName" value="${ldap.attributenames.user.id}" /> 
         <property name="emailAttributeName" value="${ldap.attributenames.email}" /> 
         <property name="firstNameAttributeName" value="${ldap.attributenames.first.name}" /> 
diff --git a/commonbase/sourceTest/java/ch/systemsx/cisd/common/test/ToStringMatcher.java b/commonbase/sourceTest/java/ch/systemsx/cisd/common/test/ToStringMatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..328b82754304b6d0677f941a3e251d100238ee60
--- /dev/null
+++ b/commonbase/sourceTest/java/ch/systemsx/cisd/common/test/ToStringMatcher.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.test;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class ToStringMatcher<T> extends BaseMatcher<T>
+{
+    private String expectedToStringString;
+
+    public ToStringMatcher(T expectedItem)
+    {
+        this(String.valueOf(expectedItem));
+    }
+
+    public ToStringMatcher(String expectedToStringString)
+    {
+        this.expectedToStringString = expectedToStringString;
+    }
+
+    @Override
+    public boolean matches(Object item)
+    {
+        return String.valueOf(item).equals(expectedToStringString);
+    }
+
+    @Override
+    public void describeTo(Description description)
+    {
+        description.appendText(expectedToStringString);
+    }
+
+}
diff --git a/datastore_server/etc/service.properties b/datastore_server/etc/service.properties
index 21d6021e6f979dcdab9110ac5f4fab9771912758..6da5a9c9691228400a045aab28bf866a60731178 100644
--- a/datastore_server/etc/service.properties
+++ b/datastore_server/etc/service.properties
@@ -212,6 +212,8 @@ validator.site.value-range = [0,Infinity)
 # E.g. 'code-extractor' property for the thread 'my-etl' should be specified as 'my-etl.code-extractor'
 inputs=main-thread, tsv-thread, csv-thread, simple-thread, hdf5-thread, dss-system-test-thread
 
+dss-rpc.put.CUSTOM-IMPORT = test-custom-import
+
 # True if incoming directories should be created on server startup if they don't exist. 
 # Default - false (server will fail at startup if one of incoming directories doesn't exist). 
 incoming-dir-create = true
diff --git a/datastore_server/source/core-plugins/core-plugins.properties b/datastore_server/source/core-plugins/core-plugins.properties
index ad076de30cf2a46969625829ac64e57daf8d3a76..36cf1a47b4385a3b705c054d2d457a8e7f259776 100644
--- a/datastore_server/source/core-plugins/core-plugins.properties
+++ b/datastore_server/source/core-plugins/core-plugins.properties
@@ -6,7 +6,7 @@
 # List of comma-separated regular expressions. If a technology (i.e. module) from the core plugins
 # folder is matching one of these regular expressions it is enabled.
 #
-#enabled-modules = 
+enabled-modules = import-test 
 
 #
 # List of comma-separated full core plugin names (or prefixes) of core plugins to be disabled 
diff --git a/datastore_server/source/core-plugins/import-test/1/as/custom-imports/test-custom-import/plugin.properties b/datastore_server/source/core-plugins/import-test/1/as/custom-imports/test-custom-import/plugin.properties
new file mode 100644
index 0000000000000000000000000000000000000000..fcbdb19ef13cab01d292b9a91fddadf0fea36323
--- /dev/null
+++ b/datastore_server/source/core-plugins/import-test/1/as/custom-imports/test-custom-import/plugin.properties
@@ -0,0 +1,4 @@
+name = Test custom import
+dss-code = STANDARD
+dropbox-name = test-custom-import
+description = This is a test custom import
\ No newline at end of file
diff --git a/datastore_server/source/core-plugins/import-test/1/as/services/import-test/import-test.py b/datastore_server/source/core-plugins/import-test/1/as/services/import-test/import-test.py
new file mode 100644
index 0000000000000000000000000000000000000000..ddf1d71e354693600f1cf287d2cfa83379490ae2
--- /dev/null
+++ b/datastore_server/source/core-plugins/import-test/1/as/services/import-test/import-test.py
@@ -0,0 +1,36 @@
+def process(context, parameters):
+    print(">>> import-test <<<");
+    
+    sessionToken = context.getSessionToken()
+    operation = parameters.get("operation");
+    uploadKey = parameters.get("uploadKey");
+    typeCode = parameters.get("typeCode");
+    asynchrounous = parameters.get("async");
+    userEmail = parameters.get("userEmail");
+    defaultSpaceIdentifier = parameters.get("defaultSpaceIdentifier");
+    spaceIdentifierOverride = parameters.get("spaceIdentifierOverride");
+    experimentIdentifierOverride = parameters.get("experimentIdentifierOverride");
+    updateExisting = parameters.get("updateExisting");
+    ignoreUnregistered = parameters.get("ignoreUnregistered");
+    customImportCode = parameters.get("customImportCode");
+    
+    if operation == "createExperiments":
+        return context.getImportService().createExperiments(sessionToken, uploadKey, typeCode, asynchrounous, userEmail);
+    elif operation == "updateExperiments":
+        return context.getImportService().updateExperiments(sessionToken, uploadKey, typeCode, asynchrounous, userEmail);
+    elif operation == "createSamples":
+        return context.getImportService().createSamples(sessionToken, uploadKey, typeCode, defaultSpaceIdentifier, spaceIdentifierOverride, experimentIdentifierOverride, updateExisting, asynchrounous, userEmail);
+    elif operation == "updateSamples":
+        return context.getImportService().updateSamples(sessionToken, uploadKey, typeCode, defaultSpaceIdentifier, spaceIdentifierOverride, experimentIdentifierOverride, asynchrounous, userEmail);
+    elif operation == "updateDataSets":
+        return context.getImportService().updateDataSets(sessionToken, uploadKey, typeCode, asynchrounous, userEmail);
+    elif operation == "createMaterials":
+        return context.getImportService().createMaterials(sessionToken, uploadKey, typeCode, updateExisting, asynchrounous, userEmail);
+    elif operation == "updateMaterials":
+        return context.getImportService().updateMaterials(sessionToken, uploadKey, typeCode, ignoreUnregistered, asynchrounous, userEmail);
+    elif operation == "generalImport":
+        return context.getImportService().generalImport(sessionToken, uploadKey, defaultSpaceIdentifier, updateExisting, asynchrounous, userEmail);
+    elif operation == "customImport":
+        return context.getImportService().customImport(sessionToken, uploadKey, customImportCode, asynchrounous, userEmail);
+
+    return None;
diff --git a/datastore_server/source/core-plugins/import-test/1/as/services/import-test/plugin.properties b/datastore_server/source/core-plugins/import-test/1/as/services/import-test/plugin.properties
new file mode 100644
index 0000000000000000000000000000000000000000..edcd58fc8199dc34bc0c90b0b5a273911b65b24f
--- /dev/null
+++ b/datastore_server/source/core-plugins/import-test/1/as/services/import-test/plugin.properties
@@ -0,0 +1,2 @@
+class = ch.ethz.sis.openbis.generic.server.asapi.v3.helper.service.JythonBasedCustomASServiceExecutor
+script-path = import-test.py
\ No newline at end of file
diff --git a/datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/plugin.properties b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/plugin.properties
new file mode 100644
index 0000000000000000000000000000000000000000..f71fcdefe5dd50af4ba8f82b6b7ebf51fedbadc1
--- /dev/null
+++ b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/plugin.properties
@@ -0,0 +1,6 @@
+incoming-dir = ${root-dir}/incoming-test-custom-import
+incoming-dir-create = true
+incoming-data-completeness-condition = auto-detection
+top-level-data-set-handler = ch.systemsx.cisd.etlserver.registrator.api.v2.JythonTopLevelDataSetHandlerV2
+script-path = script.py
+storage-processor = ch.systemsx.cisd.etlserver.DefaultStorageProcessor
\ No newline at end of file
diff --git a/datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/script.py b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/script.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fc3d060f7dbc447824223fc6530384862c0d6ec
--- /dev/null
+++ b/datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/script.py
@@ -0,0 +1,5 @@
+def process(transaction):
+    data = transaction.createNewDataSet("HCS_IMAGE", transaction.getIncoming().getName())
+    data.setExperiment(transaction.getExperiment("/CISD/NEMO/EXP1"))
+    data.setPropertyValue("COMMENT", "test comment " + transaction.getIncoming().getName())
+    transaction.moveFile(transaction.getIncoming().getPath(), data)
\ No newline at end of file
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java
index 08aa2477a357531cb2b13b031478f46bf5889111..208c4ab48e4bdf6e83a310ec5d5cd33d6199439c 100644
--- a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/AbstractFileTest.java
@@ -27,15 +27,17 @@ public class AbstractFileTest extends SystemTestCase
 {
 
     public static final String TEST_USER = "test";
-    
+
     public static final String TEST_SPACE_USER = "test_space";
 
+    public static final String ETL_SERVER_USER = "etlserver";
+
     public static final String PASSWORD = "password";
 
     protected IGeneralInformationService gis;
 
     protected IApplicationServerApi as;
-    
+
     protected IDataStoreServerApi dss;
 
     protected String dataSetCode;
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateExperimentsImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateExperimentsImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c5d208d9315b38a73e7ae4070d706f722305d35
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateExperimentsImportTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.delete.ExperimentDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier;
+
+/**
+ * @author pkupczyk
+ */
+public class CreateExperimentsImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testCreate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        String experimentCode = "TEST-IMPORT-" + UUID.randomUUID().toString();
+        ExperimentIdentifier experimentIdentifier = new ExperimentIdentifier("/TEST-SPACE/TEST-PROJECT/" + experimentCode);
+
+        try
+        {
+            ImportFile file = new ImportFile("identifier", "DESCRIPTION");
+            file.addLine(experimentIdentifier.getIdentifier(), "imported description");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Experiment experiment = getObject(sessionToken, experimentIdentifier);
+            assertNull(experiment);
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, "SIRNA_HCS");
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "createExperiments", parameters);
+
+            experiment = getObject(sessionToken, experimentIdentifier, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported description", experiment.getProperty("DESCRIPTION"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Experiment Batch Registration successfully performed");
+            } else
+            {
+                assertEquals("1 experiment(s) found and registered.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            ExperimentDeletionOptions options = new ExperimentDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteExperiments(sessionToken, Arrays.asList(experimentIdentifier), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateMaterialsImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateMaterialsImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c507327c7ba2d5ee66cb616ce2d6995a4adb54e0
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateMaterialsImportTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.delete.MaterialDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.MaterialPermId;
+
+/**
+ * @author pkupczyk
+ */
+public class CreateMaterialsImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testCreate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        MaterialPermId materialPermId = new MaterialPermId("TEST-IMPORT-" + UUID.randomUUID().toString(), "VIRUS");
+
+        try
+        {
+            ImportFile file = new ImportFile("code", "DESCRIPTION");
+            file.addLine(materialPermId.getCode(), "imported description");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Material material = getObject(sessionToken, materialPermId);
+            assertNull(material);
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, materialPermId.getTypeCode());
+            parameters.put(PARAM_UPDATE_EXISTING, false);
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "createMaterials", parameters);
+
+            material = getObject(sessionToken, materialPermId, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported description", material.getProperty("DESCRIPTION"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Material Batch Registration successfully performed");
+            } else
+            {
+                assertEquals("Registration/update of 1 material(s) is complete.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            MaterialDeletionOptions options = new MaterialDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteMaterials(sessionToken, Arrays.asList(materialPermId), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateSamplesImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateSamplesImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..83f7a31f810f3ed06dbb1bc185b63938514e760b
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateSamplesImportTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.delete.SampleDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier;
+
+/**
+ * @author pkupczyk
+ */
+public class CreateSamplesImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testCreate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        String sampleCode = "TEST-IMPORT-" + UUID.randomUUID().toString();
+        SampleIdentifier sampleIdentifier = new SampleIdentifier("/TEST-SPACE/" + sampleCode);
+
+        try
+        {
+            ImportFile file = new ImportFile("identifier", "COMMENT");
+            file.addLine(sampleIdentifier.getIdentifier(), "imported comment");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Sample sample = getObject(sessionToken, sampleIdentifier);
+            assertNull(sample);
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, "CELL_PLATE");
+            parameters.put(PARAM_UPDATE_EXISTING, false);
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "createSamples", parameters);
+
+            sample = getObject(sessionToken, sampleIdentifier, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported comment", sample.getProperty("COMMENT"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Sample Batch Registration successfully performed");
+            } else
+            {
+                assertEquals("Registration of 1 sample(s) is complete.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            SampleDeletionOptions options = new SampleDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteSamples(sessionToken, Arrays.asList(sampleIdentifier), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CustomImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CustomImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce545c8b938569d0ae08b3810deda520d9bea72e
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CustomImportTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.util.MultiPartContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.delete.DataSetDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
+
+/**
+ * @author pkupczyk
+ */
+public class CustomImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testImport(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        DataSetPermId dataSetPermId = new DataSetPermId("TEST-IMPORT-" + UUID.randomUUID().toString());
+
+        try
+        {
+            MultiPartContentProvider multiPart = new MultiPartContentProvider();
+            ContentProvider contentProvider = new StringContentProvider("test-file-content");
+            multiPart.addFilePart(TEST_UPLOAD_KEY, dataSetPermId.getPermId(), contentProvider, null);
+            multiPart.close();
+
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, multiPart);
+
+            DataSet dataSet = getObject(sessionToken, dataSetPermId);
+            assertNull(dataSet);
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_CUSTOM_IMPORT_CODE, "test-custom-import");
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "customImport", parameters);
+
+            dataSet = getObject(sessionToken, dataSetPermId, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("test comment " + dataSetPermId.getPermId(), dataSet.getProperty("COMMENT"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Custom import successfully performed");
+            } else
+            {
+                assertEquals("Import successfully completed.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            DataSetDeletionOptions options = new DataSetDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteDataSets(sessionToken, Arrays.asList(dataSetPermId), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..262d1f3c5f886cd138e8cb63535cd635992b7ed1
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.client.util.MultiPartContentProvider;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.delete.MaterialDeletionOptions;
+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.systemsx.cisd.common.utilities.TestResources;
+
+/**
+ * @author pkupczyk
+ */
+public class GeneralImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testImport(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        MaterialPermId materialPermId1 = new MaterialPermId("TEST-IMPORT-1", "VIRUS");
+        MaterialPermId materialPermId2 = new MaterialPermId("TEST-IMPORT-2", "VIRUS");
+
+        deleteMaterials(sessionToken, materialPermId1, materialPermId2);
+
+        try
+        {
+            Material material1 = getObject(sessionToken, materialPermId1);
+            assertNull(material1);
+
+            Material material2 = getObject(sessionToken, materialPermId2);
+            assertNull(material2);
+
+            TestResources resources = new TestResources(getClass());
+            File materialsFile = resources.getResourceFile("materials_excel_97_2003.xls");
+
+            MultiPartContentProvider multiPart = new MultiPartContentProvider();
+            ContentProvider contentProvider = new BytesContentProvider(FileUtils.readFileToByteArray(materialsFile));
+            multiPart.addFilePart(TEST_UPLOAD_KEY, materialsFile.getName(), contentProvider, null);
+            multiPart.close();
+
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, multiPart);
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_UPDATE_EXISTING, false);
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "generalImport", parameters);
+
+            material1 = getObject(sessionToken, materialPermId1, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported description 1", material1.getProperty("DESCRIPTION"));
+
+            material2 = getObject(sessionToken, materialPermId2, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported description 2", material2.getProperty("DESCRIPTION"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "General Batch Import successfully performed");
+            } else
+            {
+                assertEquals("Registration/update of 2 material(s) is complete.\nRegistration of 0 sample(s) is complete.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            deleteMaterials(sessionToken, materialPermId1, materialPermId2);
+        }
+    }
+
+    private void deleteMaterials(String sessionToken, IMaterialId... materialIds)
+    {
+        MaterialDeletionOptions options = new MaterialDeletionOptions();
+        options.setReason("cleanup");
+        as.deleteMaterials(sessionToken, Arrays.asList(materialIds), options);
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTestResources/materials_excel_97_2003.xls b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTestResources/materials_excel_97_2003.xls
new file mode 100644
index 0000000000000000000000000000000000000000..3e18171654156d191bf5dd8f036f415cb1a003d4
Binary files /dev/null and b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTestResources/materials_excel_97_2003.xls differ
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/ObjectsImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/ObjectsImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b41d7e9b67760a962e23299bd4b5fa35e374b9dd
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/ObjectsImportTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.MultiPartContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.id.IObjectId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IModificationDateHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.IDataSetId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.IExperimentId;
+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.sample.fetchoptions.SampleFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.ISampleId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.service.CustomASServiceExecutionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.service.id.CustomASServiceCode;
+import ch.ethz.sis.openbis.systemtest.asapi.v3.util.EmailUtil;
+import ch.ethz.sis.openbis.systemtest.asapi.v3.util.EmailUtil.Email;
+import ch.systemsx.cisd.common.http.JettyHttpClientFactory;
+import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider;
+import ch.systemsx.cisd.openbis.generic.shared.util.TestInstanceHostUtils;
+
+/**
+ * @author pkupczyk
+ */
+public class ObjectsImportTest extends AbstractFileTest
+{
+
+    protected static final String SERVICE_URL = TestInstanceHostUtils.getOpenBISUrl() + "/openbis/upload";
+
+    protected static final String TEST_UPLOAD_KEY = "test-import";
+
+    protected static final String TEST_EMAIL = "test@email.com";
+
+    protected static final long DEFAULT_TIMEOUT = 30000;
+
+    protected static final String FALSE_TRUE_PROVIDER = "sync-async";
+
+    protected static final String PARAM_UPLOAD_KEY = "uploadKey";
+
+    protected static final String PARAM_TYPE_CODE = "typeCode";
+
+    protected static final String PARAM_ASYNC = "async";
+
+    protected static final String PARAM_USER_EMAIL = "userEmail";
+
+    protected static final String PARAM_DEFAULT_SPACE_IDENTIFIER = "defaultSpaceIdentifier";
+
+    protected static final String PARAM_SPACE_IDENTIFIER_OVERRIDE = "spaceIdentifierOverride";
+
+    protected static final String PARAM_EXPERIMENT_IDENTIFIER_OVERRIDE = "experimentIdentifierOverride";
+
+    protected static final String PARAM_UPDATE_EXISTING = "updateExisting";
+
+    protected static final String PARAM_IGNORE_UNREGISTERED = "ignoreUnregistered";
+    
+    protected static final String PARAM_CUSTOM_IMPORT_CODE = "customImportCode";
+
+    protected IApplicationServerApi as;
+
+    @BeforeClass
+    protected void beforeClass() throws Exception
+    {
+        super.beforeClass();
+        as = ServiceProvider.getV3ApplicationService();
+    }
+
+    protected ContentResponse uploadFiles(String sessionToken, String uploadSessionKey, MultiPartContentProvider multiPart)
+            throws InterruptedException, TimeoutException, ExecutionException
+    {
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param("sessionID", sessionToken);
+        request.param("sessionKeysNumber", "1");
+        request.param("sessionKey_0", uploadSessionKey);
+        request.content(multiPart);
+
+        return request.send();
+    }
+
+    protected ContentResponse uploadFiles(String sessionToken, String uploadSessionKey, String... filesContent)
+            throws InterruptedException, TimeoutException, ExecutionException
+    {
+        MultiPartContentProvider multiPart = new MultiPartContentProvider();
+
+        for (int i = 0; i < filesContent.length; i++)
+        {
+            ContentProvider contentProvider = new StringContentProvider(filesContent[i]);
+
+            String fieldName = uploadSessionKey + "_" + i;
+            String fileName = "fileName_" + i;
+            multiPart.addFilePart(fieldName, fileName, contentProvider, null);
+        }
+
+        multiPart.close();
+
+        return uploadFiles(sessionToken, uploadSessionKey, multiPart);
+    }
+
+    protected String executeImport(String sessionToken, String operation, Map<String, Object> parameters)
+    {
+        CustomASServiceCode serviceId = new CustomASServiceCode("import-test");
+        CustomASServiceExecutionOptions options = new CustomASServiceExecutionOptions();
+        options.withParameter("operation", operation);
+        for (String name : parameters.keySet())
+        {
+            options.withParameter(name, parameters.get(name));
+        }
+        return (String) as.executeCustomASService(sessionToken, serviceId, options);
+    }
+
+    protected <T extends IModificationDateHolder> T getObject(String sessionToken, IObjectId objectId)
+    {
+        return getObject(sessionToken, objectId, 0, 0);
+    }
+
+    protected <T extends IModificationDateHolder> T getObject(String sessionToken, IObjectId objectId, long modifiedAfterTimestamp,
+            long timeoutAfterMillis)
+    {
+        long startMillis = System.currentTimeMillis();
+
+        while (true)
+        {
+            Map<IObjectId, T> objects = getObjects(sessionToken, objectId);
+            T object = objects.get(objectId);
+
+            if (object != null && object.getModificationDate() != null && object.getModificationDate().getTime() >= modifiedAfterTimestamp)
+            {
+                return object;
+            }
+
+            if (timeoutAfterMillis > 0 && System.currentTimeMillis() < startMillis + timeoutAfterMillis)
+            {
+                try
+                {
+                    Thread.sleep(100);
+                } catch (InterruptedException e)
+                {
+                    throw new RuntimeException(e);
+                }
+            } else
+            {
+                return null;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private <K, V> Map<K, V> getObjects(String sessionToken, IObjectId objectId)
+    {
+        if (objectId instanceof IExperimentId)
+        {
+            ExperimentFetchOptions fo = new ExperimentFetchOptions();
+            fo.withProperties();
+            return (Map<K, V>) as.getExperiments(sessionToken, Arrays.asList((IExperimentId) objectId), (ExperimentFetchOptions) fo);
+        } else if (objectId instanceof ISampleId)
+        {
+            SampleFetchOptions fo = new SampleFetchOptions();
+            fo.withProperties();
+            return (Map<K, V>) as.getSamples(sessionToken, Arrays.asList((ISampleId) objectId), (SampleFetchOptions) fo);
+        } else if (objectId instanceof IDataSetId)
+        {
+            DataSetFetchOptions fo = new DataSetFetchOptions();
+            fo.withProperties();
+            return (Map<K, V>) as.getDataSets(sessionToken, Arrays.asList((IDataSetId) objectId), (DataSetFetchOptions) fo);
+        } else if (objectId instanceof IMaterialId)
+        {
+            MaterialFetchOptions fo = new MaterialFetchOptions();
+            fo.withProperties();
+            return (Map<K, V>) as.getMaterials(sessionToken, Arrays.asList((IMaterialId) objectId), (MaterialFetchOptions) fo);
+        } else
+        {
+            throw new IllegalArgumentException("Unsupported object id " + objectId);
+        }
+    }
+
+    protected void assertNoEmails(long timestamp)
+    {
+        Email latestEmail = EmailUtil.findLatestEmail();
+        assertTrue("Timestamp: " + timestamp + ", Latest email: " + latestEmail, latestEmail == null || latestEmail.timestamp < timestamp);
+    }
+
+    protected void assertEmail(long timestamp, String expectedEmail, String expectedSubject)
+    {
+        Email latestEmail = EmailUtil.findLatestEmail();
+        assertTrue("Timestamp: " + timestamp + ", Latest email: " + latestEmail, latestEmail != null && latestEmail.timestamp >= timestamp);
+        assertEquals(expectedEmail, latestEmail.to);
+        assertTrue(latestEmail.subject, latestEmail.subject.contains(expectedSubject));
+    }
+
+    public static class ImportFile
+    {
+
+        private List<String> columns;
+
+        private List<List<String>> lines = new ArrayList<List<String>>();
+
+        public ImportFile(String... columns)
+        {
+            this.columns = Arrays.asList(columns);
+        }
+
+        public void addLine(String... values)
+        {
+            lines.add(Arrays.asList(values));
+        }
+
+        @Override
+        public String toString()
+        {
+            StringBuilder content = new StringBuilder();
+            content.append(String.join("\t", columns) + "\n");
+
+            for (List<String> line : lines)
+            {
+                content.append(String.join("\t", line) + "\n");
+            }
+
+            return content.toString();
+        }
+    }
+
+    @DataProvider(name = FALSE_TRUE_PROVIDER)
+    public static Object[][] provideFalseTrue()
+    {
+        return new Object[][] { { false }, { true } };
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateDataSetsImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateDataSetsImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a18bb9e0d9a33481bfe57cdab1ec9b3b05c2f3e2
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateDataSetsImportTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetKind;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.create.DataSetCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.create.PhysicalDataCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.delete.DataSetDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.FileFormatTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.ProprietaryStorageFormatPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.RelativeLocationLocatorTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.datastore.id.DataStorePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier;
+
+/**
+ * @author pkupczyk
+ */
+public class UpdateDataSetsImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUpdate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+        String etlServerSessionToken = as.login(ETL_SERVER_USER, PASSWORD);
+
+        DataSetPermId dataSetPermId = new DataSetPermId("TEST-IMPORT-" + UUID.randomUUID().toString());
+
+        try
+        {
+            PhysicalDataCreation physicalCreation = new PhysicalDataCreation();
+            physicalCreation.setLocation("test/location/" + dataSetPermId.getPermId());
+            physicalCreation.setFileFormatTypeId(new FileFormatTypePermId("TIFF"));
+            physicalCreation.setLocatorTypeId(new RelativeLocationLocatorTypePermId());
+            physicalCreation.setStorageFormatId(new ProprietaryStorageFormatPermId());
+
+            DataSetCreation creation = new DataSetCreation();
+            creation.setCode(dataSetPermId.getPermId());
+            creation.setDataSetKind(DataSetKind.PHYSICAL);
+            creation.setTypeId(new EntityTypePermId("HCS_IMAGE"));
+            creation.setExperimentId(new ExperimentIdentifier("/TEST-SPACE/TEST-PROJECT/EXP-SPACE-TEST"));
+            creation.setDataStoreId(new DataStorePermId("STANDARD"));
+            creation.setPhysicalData(physicalCreation);
+            creation.setProperty("COMMENT", "initial comment");
+
+            DataSet dataSet = getObject(sessionToken, dataSetPermId);
+            assertNull(dataSet);
+
+            as.createDataSets(etlServerSessionToken, Arrays.asList(creation));
+
+            dataSet = getObject(sessionToken, dataSetPermId);
+            assertEquals("initial comment", dataSet.getProperty("COMMENT"));
+
+            ImportFile file = new ImportFile("code", "COMMENT");
+            file.addLine(dataSetPermId.getPermId(), "imported comment");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, "HCS_IMAGE");
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "updateDataSets", parameters);
+
+            dataSet = getObject(sessionToken, dataSetPermId, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported comment", dataSet.getProperty("COMMENT"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Data Set Batch Update successfully performed");
+            } else
+            {
+                assertEquals("1 data set(s) found and registered.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            DataSetDeletionOptions options = new DataSetDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteDataSets(sessionToken, Arrays.asList(dataSetPermId), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateExperimentsImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateExperimentsImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..094fb142295cb166a518c6c46e84675cc41e9b2f
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateExperimentsImportTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.create.ExperimentCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.delete.ExperimentDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.id.ProjectIdentifier;
+
+/**
+ * @author pkupczyk
+ */
+public class UpdateExperimentsImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUpdate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        String experimentCode = "TEST-IMPORT-" + UUID.randomUUID().toString();
+        ExperimentIdentifier experimentIdentifier = new ExperimentIdentifier("/TEST-SPACE/TEST-PROJECT/" + experimentCode);
+
+        try
+        {
+            ExperimentCreation creation = new ExperimentCreation();
+            creation.setCode(experimentCode);
+            creation.setTypeId(new EntityTypePermId("SIRNA_HCS"));
+            creation.setProjectId(new ProjectIdentifier("/TEST-SPACE/TEST-PROJECT"));
+            creation.setProperty("DESCRIPTION", "initial value");
+
+            Experiment experiment = getObject(sessionToken, experimentIdentifier);
+            assertNull(experiment);
+
+            as.createExperiments(sessionToken, Arrays.asList(creation));
+
+            experiment = getObject(sessionToken, experimentIdentifier);
+            assertEquals("initial value", experiment.getProperty("DESCRIPTION"));
+
+            ImportFile file = new ImportFile("identifier", "DESCRIPTION");
+            file.addLine(experimentIdentifier.getIdentifier(), "imported description");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, "SIRNA_HCS");
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "updateExperiments", parameters);
+
+            experiment = getObject(sessionToken, experimentIdentifier, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported description", experiment.getProperty("DESCRIPTION"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Experiment Batch Update successfully performed");
+            } else
+            {
+                assertEquals("Update of 1 experiment(s) is complete.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            ExperimentDeletionOptions options = new ExperimentDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteExperiments(sessionToken, Arrays.asList(experimentIdentifier), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateMaterialsImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateMaterialsImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3a8e817b86e00a179ef5b34b786fd0f4906ae87
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateMaterialsImportTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.create.MaterialCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.delete.MaterialDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.MaterialPermId;
+
+/**
+ * @author pkupczyk
+ */
+public class UpdateMaterialsImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUpdate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        MaterialPermId materialPermId = new MaterialPermId("TEST-IMPORT-" + UUID.randomUUID().toString(), "VIRUS");
+
+        try
+        {
+            MaterialCreation creation = new MaterialCreation();
+            creation.setCode(materialPermId.getCode());
+            creation.setTypeId(new EntityTypePermId(materialPermId.getTypeCode()));
+            creation.setProperty("DESCRIPTION", "initial description");
+
+            Material material = getObject(sessionToken, materialPermId);
+            assertNull(material);
+
+            as.createMaterials(sessionToken, Arrays.asList(creation));
+
+            material = getObject(sessionToken, materialPermId);
+            assertEquals("initial description", material.getProperty("DESCRIPTION"));
+
+            ImportFile file = new ImportFile("code", "DESCRIPTION");
+            file.addLine(materialPermId.getCode(), "imported description");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, materialPermId.getTypeCode());
+            parameters.put(PARAM_IGNORE_UNREGISTERED, false);
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "updateMaterials", parameters);
+
+            material = getObject(sessionToken, materialPermId, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported description", material.getProperty("DESCRIPTION"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Material Batch Update successfully performed");
+            } else
+            {
+                assertEquals("1 material(s) updated.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            MaterialDeletionOptions options = new MaterialDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteMaterials(sessionToken, Arrays.asList(materialPermId), options);
+        }
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateSamplesImportTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateSamplesImportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..01effa654c52b8463219e950b899f07bffea1540
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateSamplesImportTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018 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.dss.systemtest.api.v3;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.create.SampleCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.delete.SampleDeletionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId;
+
+/**
+ * @author pkupczyk
+ */
+public class UpdateSamplesImportTest extends ObjectsImportTest
+{
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUpdate(boolean async) throws Exception
+    {
+        String sessionToken = as.login(TEST_USER, PASSWORD);
+
+        String sampleCode = "TEST-IMPORT-" + UUID.randomUUID().toString();
+        SampleIdentifier sampleIdentifier = new SampleIdentifier("/TEST-SPACE/" + sampleCode);
+
+        try
+        {
+            SampleCreation creation = new SampleCreation();
+            creation.setCode(sampleCode);
+            creation.setSpaceId(new SpacePermId("TEST-SPACE"));
+            creation.setTypeId(new EntityTypePermId("CELL_PLATE"));
+            creation.setProperty("COMMENT", "initial comment");
+
+            Sample sample = getObject(sessionToken, sampleIdentifier);
+            assertNull(sample);
+
+            as.createSamples(sessionToken, Arrays.asList(creation));
+
+            sample = getObject(sessionToken, sampleIdentifier);
+            assertEquals("initial comment", sample.getProperty("COMMENT"));
+
+            ImportFile file = new ImportFile("identifier", "COMMENT");
+            file.addLine(sampleIdentifier.getIdentifier(), "imported comment");
+            uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+
+            Map<String, Object> parameters = new HashMap<String, Object>();
+            parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
+            parameters.put(PARAM_TYPE_CODE, "CELL_PLATE");
+            parameters.put(PARAM_UPDATE_EXISTING, false);
+            parameters.put(PARAM_ASYNC, async);
+
+            if (async)
+            {
+                parameters.put(PARAM_USER_EMAIL, TEST_EMAIL);
+            }
+
+            long timestamp = System.currentTimeMillis();
+            String message = executeImport(sessionToken, "updateSamples", parameters);
+
+            sample = getObject(sessionToken, sampleIdentifier, timestamp, DEFAULT_TIMEOUT);
+            assertEquals("imported comment", sample.getProperty("COMMENT"));
+
+            if (async)
+            {
+                assertEquals("When the import is complete the confirmation or failure report will be sent by email.", message);
+                assertEmail(timestamp, TEST_EMAIL, "Sample Batch Update successfully performed");
+            } else
+            {
+                assertEquals("Update of 1 sample(s) is complete.", message);
+                assertNoEmails(timestamp);
+            }
+        } finally
+        {
+            SampleDeletionOptions options = new SampleDeletionOptions();
+            options.setReason("cleanup");
+            as.deleteSamples(sessionToken, Arrays.asList(sampleIdentifier), options);
+        }
+    }
+
+}
diff --git a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/dtos.js b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/dtos.js
index 468337974d49bee608a4cde96ae611340e84bea1..568fa4d5fc6ed3047e8ba81a4241e8dd6923ac4c 100644
--- a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/dtos.js
+++ b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/dtos.js
@@ -349,6 +349,7 @@ var sources = [
 	'as/dto/history/HistoryEntry',
 	'as/dto/history/PropertyHistoryEntry',
 	'as/dto/history/RelationHistoryEntry',
+	'as/dto/history/ContentCopyHistoryEntry',
 	
 	'as/dto/material/create/MaterialCreation',
 	'as/dto/material/create/CreateMaterialsOperation',
diff --git a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-jsVSjava.js b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-jsVSjava.js
index a5105c839c8b2c5c5491db15e98e4de7ebda270b..97a41b586970fce665d87ba54f786d11024124b2 100644
--- a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-jsVSjava.js
+++ b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-jsVSjava.js
@@ -14,7 +14,6 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 		};
 
 		var ignoreMessages = {
-			"ICustomASServiceExecutor" : "Java class ignored: ",
 			"ServiceContext" : "Java class ignored: ",
 			"CustomASServiceContext" : "Java class ignored: ",
 			"AbstractCollectionView" : "Java class ignored: ",
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 6065148dce5d454bbf4d69b34f294bb95830b09e..0c06dc4de6ca79de8788d1f9da0941bda6368ac6 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
@@ -453,7 +453,7 @@ import ch.systemsx.cisd.openbis.generic.shared.managed_property.IManagedProperty
  */
 @Component(ApplicationServerApi.INTERNAL_SERVICE_NAME)
 public class ApplicationServerApi extends AbstractServer<IApplicationServerApi> implements
-        IApplicationServerApi
+        IApplicationServerInternalApi
 {
     /**
      * Name of this service for which it is registered as Spring bean
@@ -486,6 +486,12 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
         return session == null ? null : session.getSessionToken();
     }
 
+    @Override
+    public String loginAsSystem()
+    {
+        return tryToAuthenticateAsSystem().getSessionToken();
+    }
+
     @Override
     @Transactional
     public String loginAsAnonymousUser()
@@ -1525,7 +1531,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Override
     public int getMinorVersion()
     {
-        return 3;
+        return 4;
     }
 
     @Override
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/IApplicationServerInternalApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/IApplicationServerInternalApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e623105fb4aad9348231d0af71b37e2bcb57004
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/IApplicationServerInternalApi.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * 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;
+
+import org.springframework.transaction.annotation.Transactional;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+
+/**
+ * Extension of {@link IApplicationServerApi} which are only for internal use. These methods are not accessible remotely.
+ * 
+ * @author Franz-Josef Elmer
+ */
+public interface IApplicationServerInternalApi extends IApplicationServerApi
+{
+    @Transactional
+    public String loginAsSystem();
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/CustomASServiceScriptRunnerFactory.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/CustomASServiceScriptRunnerFactory.java
index 7837c8e87a79da7faa2a7028d3326d21e73df1f8..0954144dde8b831564941cfc1cfb904b9238fcc0 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/CustomASServiceScriptRunnerFactory.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/CustomASServiceScriptRunnerFactory.java
@@ -20,6 +20,7 @@ import java.io.Serializable;
 
 import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.service.CustomASServiceExecutionOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.plugin.service.IImportService;
 import ch.ethz.sis.openbis.generic.asapi.v3.plugin.service.context.ServiceContext;
 import ch.systemsx.cisd.common.jython.JythonUtils;
 import ch.systemsx.cisd.common.jython.evaluator.Evaluator;
@@ -35,10 +36,14 @@ class CustomASServiceScriptRunnerFactory implements IScriptRunnerFactory
 
     private final IApplicationServerApi applicationService;
 
-    public CustomASServiceScriptRunnerFactory(String scriptPath, IApplicationServerApi applicationService)
+    private final IImportService importService;
+
+    public CustomASServiceScriptRunnerFactory(String scriptPath, IApplicationServerApi applicationService,
+            IImportService importService)
     {
         this.scriptPath = scriptPath;
         this.applicationService = applicationService;
+        this.importService = importService;
         Evaluator.getFactory().initialize();
     }
 
@@ -58,7 +63,7 @@ class CustomASServiceScriptRunnerFactory implements IScriptRunnerFactory
         {
             IJythonEvaluator evaluator = Evaluator.getFactory().create("", pythonPath, scriptPath, null, scriptString, false);
             String sessionToken = context.getSessionToken();
-            ExecutionContext executionContext = new ExecutionContext(sessionToken, applicationService);
+            ExecutionContext executionContext = new ExecutionContext(sessionToken, applicationService, importService);
             return new ServiceScriptRunner(evaluator, executionContext);
         } catch (EvaluatorException ex)
         {
@@ -105,10 +110,13 @@ class CustomASServiceScriptRunnerFactory implements IScriptRunnerFactory
 
         private final IApplicationServerApi applicationService;
 
-        ExecutionContext(String sessionToken, IApplicationServerApi applicationService)
+        private final IImportService importService;
+
+        ExecutionContext(String sessionToken, IApplicationServerApi applicationService, IImportService importService)
         {
             this.sessionToken = sessionToken;
             this.applicationService = applicationService;
+            this.importService = importService;
         }
 
         public String getSessionToken()
@@ -120,5 +128,10 @@ class CustomASServiceScriptRunnerFactory implements IScriptRunnerFactory
         {
             return applicationService;
         }
+
+        public IImportService getImportService()
+        {
+            return importService;
+        }
     }
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/ImportService.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/ImportService.java
new file mode 100644
index 0000000000000000000000000000000000000000..1371cd7537fb9de1f72a806340fe76c2b907492c
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/ImportService.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2018 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.helper.service;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.plugin.service.IImportService;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.shared.ICustomImportService;
+import ch.systemsx.cisd.openbis.generic.shared.IEntityImportService;
+import ch.systemsx.cisd.openbis.generic.shared.ResourceNames;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SampleType;
+
+/**
+ * @author pkupczyk
+ */
+@Component(value = ResourceNames.IMPORT_SERVICE)
+public class ImportService implements IImportService
+{
+
+    @Autowired
+    private IEntityImportService entityImportService;
+
+    @Autowired
+    private ICustomImportService customImportService;
+
+    @Override
+    public String createExperiments(String sessionToken, String uploadKey, String experimentTypeCode, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, experimentTypeCode, async, userEmail);
+
+        ExperimentType experimentType = new ExperimentType();
+        experimentType.setCode(experimentTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.registerExperiments(experimentType, uploadKey, async, userEmail);
+        return translateResults(results);
+    }
+
+    @Override
+    public String updateExperiments(String sessionToken, String uploadKey, String experimentTypeCode, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, experimentTypeCode, async, userEmail);
+
+        ExperimentType experimentType = new ExperimentType();
+        experimentType.setCode(experimentTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.updateExperiments(experimentType, uploadKey, async, userEmail);
+        return translateResults(results);
+    }
+
+    @Override
+    public String createSamples(String sessionToken, String uploadKey, String sampleTypeCode, String defaultSpaceIdentifier,
+            String spaceIdentifierOverride, String experimentIdentifierOverride, boolean updateExisting, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, sampleTypeCode, async, userEmail);
+
+        SampleType sampleType = new SampleType();
+        sampleType.setCode(sampleTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.registerSamplesWithSilentOverrides(sampleType, spaceIdentifierOverride,
+                experimentIdentifierOverride, uploadKey, async, userEmail, defaultSpaceIdentifier, updateExisting);
+        return translateResults(results);
+    }
+
+    @Override
+    public String updateSamples(String sessionToken, String uploadKey, String sampleTypeCode, String defaultSpaceIdentifier,
+            String spaceIdentifierOverride, String experimentIdentifierOverride, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, sampleTypeCode, async, userEmail);
+
+        SampleType sampleType = new SampleType();
+        sampleType.setCode(sampleTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.updateSamplesWithSilentOverrides(sampleType, spaceIdentifierOverride,
+                experimentIdentifierOverride, uploadKey, async, userEmail, defaultSpaceIdentifier);
+        return translateResults(results);
+    }
+
+    @Override
+    public String updateDataSets(String sessionToken, String uploadKey, String dataSetTypeCode, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, dataSetTypeCode, async, userEmail);
+
+        DataSetType dataSetType = new DataSetType();
+        dataSetType.setCode(dataSetTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.updateDataSets(dataSetType, uploadKey, async, userEmail);
+        return translateResults(results);
+    }
+
+    @Override
+    public String createMaterials(String sessionToken, String uploadKey, String materialTypeCode, boolean updateExisting, boolean async,
+            String userEmail)
+    {
+        check(sessionToken, uploadKey, materialTypeCode, async, userEmail);
+
+        MaterialType materialType = new MaterialType();
+        materialType.setCode(materialTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.registerMaterials(materialType, updateExisting, uploadKey, async, userEmail);
+        return translateResults(results);
+    }
+
+    @Override
+    public String updateMaterials(String sessionToken, String uploadKey, String materialTypeCode, boolean ignoreUnregistered, boolean async,
+            String userEmail)
+    {
+        check(sessionToken, uploadKey, materialTypeCode, async, userEmail);
+
+        MaterialType materialType = new MaterialType();
+        materialType.setCode(materialTypeCode);
+
+        List<BatchRegistrationResult> results = entityImportService.updateMaterials(materialType, uploadKey, ignoreUnregistered, async, userEmail);
+        return translateResults(results);
+    }
+
+    @Override
+    public String generalImport(String sessionToken, String uploadKey, String defaultSpaceIdentifier, boolean updateExisting, boolean async,
+            String userEmail)
+    {
+        check(sessionToken, uploadKey, async, userEmail);
+
+        List<BatchRegistrationResult> results =
+                entityImportService.registerOrUpdateSamplesAndMaterials(uploadKey, defaultSpaceIdentifier, updateExisting, async, userEmail);
+        return translateResults(results);
+    }
+
+    @Override
+    public String customImport(String sessionToken, String uploadKey, String customImportCode, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, async, userEmail);
+
+        if (customImportCode == null)
+        {
+            throw new UserFailureException("Custom import code cannot be null");
+        }
+
+        List<BatchRegistrationResult> results =
+                customImportService.performCustomImport(uploadKey, customImportCode, async, userEmail);
+        return translateResults(results);
+    }
+
+    protected void check(String sessionToken, String uploadKey, String typeCode, boolean async, String userEmail)
+    {
+        check(sessionToken, uploadKey, async, userEmail);
+
+        if (typeCode == null)
+        {
+            throw new UserFailureException("Type code cannot be null");
+        }
+    }
+
+    protected void check(String sessionToken, String uploadKey, boolean async, String userEmail)
+    {
+        if (sessionToken == null)
+        {
+            throw new UserFailureException("Session token cannot be null");
+        }
+        if (uploadKey == null)
+        {
+            throw new UserFailureException("Upload key cannot be null");
+        }
+        if (async && userEmail == null)
+        {
+            throw new UserFailureException("User email cannot be null for an asynchronous import");
+        }
+    }
+
+    protected String translateResults(List<BatchRegistrationResult> results)
+    {
+        StringBuilder message = new StringBuilder();
+        Iterator<BatchRegistrationResult> iter = results.iterator();
+
+        while (iter.hasNext())
+        {
+            BatchRegistrationResult result = iter.next();
+            if (result != null)
+            {
+                message.append(result.getMessage());
+                if (iter.hasNext())
+                {
+                    message.append("\n");
+                }
+            }
+        }
+
+        return message.toString();
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/JythonBasedCustomASServiceExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/JythonBasedCustomASServiceExecutor.java
index 08cbd921a0568ed1eb12b28883650f8d90e1b91d..3c56376af5a853328785a2115bd823b005d20b14 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/JythonBasedCustomASServiceExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/JythonBasedCustomASServiceExecutor.java
@@ -51,7 +51,8 @@ public class JythonBasedCustomASServiceExecutor implements ICustomASServiceExecu
     {
         if (factory == null)
         {
-            factory = new CustomASServiceScriptRunnerFactory(scriptPath, CommonServiceProvider.getApplicationServerApi());
+            factory = new CustomASServiceScriptRunnerFactory(scriptPath, CommonServiceProvider.getApplicationServerApi(),
+                    CommonServiceProvider.getImportService());
         }
         return factory;
     }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetHistoryTranslator.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetHistoryTranslator.java
index 53992330fa3424e89ac521dfd4c3da47288ac56c..e4e78d4c8c5cb6e657d15bdaa6d1735e43a23fd0 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetHistoryTranslator.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetHistoryTranslator.java
@@ -19,6 +19,7 @@ package ch.ethz.sis.openbis.generic.server.asapi.v3.translator.dataset;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -29,13 +30,17 @@ import org.springframework.stereotype.Component;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.history.DataSetRelationType;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.history.ContentCopyHistoryEntry;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.history.HistoryEntry;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.history.RelationHistoryEntry;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.history.fetchoptions.HistoryEntryFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.TranslationContext;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.experiment.IExperimentAuthorizationValidator;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryContentCopyRecord;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryPropertyRecord;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryRecord;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryRelationshipRecord;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryTranslator;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.sample.ISampleAuthorizationValidator;
@@ -44,6 +49,7 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.RelationType;
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import net.lemnik.eodsql.QueryTool;
 
+
 /**
  * @author pkupczyk
  */
@@ -174,4 +180,49 @@ public class DataSetHistoryTranslator extends HistoryTranslator implements IData
         return entry;
     }
 
+    @Override
+    protected List<? extends HistoryRecord> loadAbritraryHistory(TranslationContext context, Collection<Long> entityIds)
+    {
+        DataSetQuery query = QueryTool.getManagedQuery(DataSetQuery.class);
+        return query.getContentCopyHistory(new LongOpenHashSet(entityIds));
+    }
+
+    @Override
+    protected void createArbitraryEntries(Map<Long, List<HistoryEntry>> entriesMap, List<? extends HistoryRecord> records,
+            Map<Long, Person> authorMap, HistoryEntryFetchOptions fetchOptions)
+    {
+      for (HistoryRecord record : records)
+      {
+          HistoryContentCopyRecord contentCopyRecord = (HistoryContentCopyRecord) record;
+          List<HistoryEntry> entries = entriesMap.get(contentCopyRecord.dataSetId);
+
+          if (entries == null)
+          {
+              entries = new LinkedList<HistoryEntry>();
+              entriesMap.put(contentCopyRecord.dataSetId, entries);
+          }
+
+          entries.add(createContentCopyEntry(record, authorMap, fetchOptions));
+      }
+    }
+
+    private HistoryEntry createContentCopyEntry(HistoryRecord record, Map<Long, Person> authorMap, HistoryEntryFetchOptions fetchOptions)
+    {
+        HistoryContentCopyRecord contentCopyRecord = (HistoryContentCopyRecord) record;
+        ContentCopyHistoryEntry entry = new ContentCopyHistoryEntry();
+        entry.setExternalCode(contentCopyRecord.externalCode);
+        entry.setPath(contentCopyRecord.path);
+        entry.setGitCommitHash(contentCopyRecord.gitCommitHash);
+        entry.setGitRepositoryId(contentCopyRecord.gitRepositoryId);
+        entry.setExternalDmsId(contentCopyRecord.externalDmsId);
+        entry.setValidFrom(contentCopyRecord.validFrom);
+        entry.setValidTo(contentCopyRecord.validTo);
+        if (fetchOptions.hasAuthor())
+        {
+            entry.setAuthor(authorMap.get(record.authorId));
+            entry.getFetchOptions().withAuthorUsing(fetchOptions.withAuthor());
+        }
+        return entry;
+    }
+
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetQuery.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetQuery.java
index 0d4cbfe3e58293903618e1cc160356b5882b7d63..958e8f222170f0384dc5d0a28dc1abf2b12b90b5 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetQuery.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/dataset/DataSetQuery.java
@@ -20,6 +20,7 @@ import java.util.List;
 
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.common.ObjectQuery;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.common.ObjectRelationRecord;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryContentCopyRecord;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history.HistoryPropertyRecord;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.property.MaterialPropertyRecord;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.property.PropertyAssignmentRecord;
@@ -122,6 +123,16 @@ public interface DataSetQuery extends ObjectQuery
                     LongSetMapper.class }, fetchSize = FETCH_SIZE)
     public List<DataSetRelationshipRecord> getRelationshipsHistory(LongSet dataSetIds);
 
+    @Select(sql = "select dsch.id as id, dsch.data_id as dataSetId, dsch.external_code as externalCode, dsch.path as path, dsch.git_commit_hash as gitCommitHash, dsch.git_repository_id as gitRepositoryId, "
+            + "dsch.edms_id as externalDmsId, dsch.pers_id_author as authorId, dsch.valid_from_timestamp as validFrom, dsch.valid_until_timestamp as validTo "
+            + "from data_set_copies_history dsch "
+            + "where dsch.valid_until_timestamp is not null and dsch.data_id = any(?{1})", 
+            parameterBindings = {
+                    LongSetMapper.class
+                },
+            fetchSize = FETCH_SIZE)
+    public List<HistoryContentCopyRecord> getContentCopyHistory(LongSet dataSetIds);
+
     @Select(sql = "select ds_id from post_registration_dataset_queue where ds_id = any(?{1})", parameterBindings = {
             LongSetMapper.class }, fetchSize = FETCH_SIZE)
     public List<Long> getNotPostRegisteredDataSets(LongSet dataSetIds);
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryContentCopyRecord.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryContentCopyRecord.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1f0a0380307c03afd1771bdb1e58da3d6df454d
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryContentCopyRecord.java
@@ -0,0 +1,34 @@
+/*
+ * 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.translator.history;
+
+public class HistoryContentCopyRecord extends HistoryRecord
+{
+
+    public Long dataSetId;
+
+    public String externalCode;
+
+    public String path;
+
+    public String gitCommitHash;
+
+    public String gitRepositoryId;
+
+    public Long externalDmsId;
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryPropertyRecord.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryPropertyRecord.java
index 81ff60e28ac5204810d4d0ba99df0c24ab50df79..2e487675c3565a603db89cfdb9c0a322a32a6bee 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryPropertyRecord.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryPropertyRecord.java
@@ -16,20 +16,14 @@
 
 package ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history;
 
-import java.util.Date;
-
-import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.common.ObjectBaseRecord;
-
 /**
  * @author pkupczyk
  */
-public class HistoryPropertyRecord extends ObjectBaseRecord
+public class HistoryPropertyRecord extends HistoryRecord
 {
 
     public Long objectId;
 
-    public Long authorId;
-
     public String propertyCode;
 
     public String propertyValue;
@@ -38,8 +32,4 @@ public class HistoryPropertyRecord extends ObjectBaseRecord
 
     public String vocabularyPropertyValue;
 
-    public Date validFrom;
-
-    public Date validTo;
-
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRecord.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRecord.java
new file mode 100644
index 0000000000000000000000000000000000000000..e41af4d08174f3feb4f456906268623a73adadd8
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRecord.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.translator.history;
+
+import java.util.Date;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.common.ObjectBaseRecord;
+
+public class HistoryRecord extends ObjectBaseRecord
+{
+
+    public Date validFrom;
+
+    public Date validTo;
+
+    public Long authorId;
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRelationshipRecord.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRelationshipRecord.java
index 77e5cd732a697a039ee3961fb319b87f0b31c15e..5935e436612bd0319c9f96ba61fc77dc200d762f 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRelationshipRecord.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryRelationshipRecord.java
@@ -16,26 +16,16 @@
 
 package ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history;
 
-import java.util.Date;
-
-import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.common.ObjectBaseRecord;
-
 /**
  * @author pkupczyk
  */
-public class HistoryRelationshipRecord extends ObjectBaseRecord
+public class HistoryRelationshipRecord extends HistoryRecord
 {
 
     public Long objectId;
 
-    public Long authorId;
-
     public String relationType;
 
     public String relatedObjectId;
 
-    public Date validFrom;
-
-    public Date validTo;
-
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryTranslator.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryTranslator.java
index 65476e0c953b7a99b33de354f085314e16dc8b32..6769db7b11a59d650587d235431d1e2ae8ff7ff3 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryTranslator.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/history/HistoryTranslator.java
@@ -16,6 +16,7 @@
 
 package ch.ethz.sis.openbis.generic.server.asapi.v3.translator.history;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -51,6 +52,10 @@ public abstract class HistoryTranslator extends AbstractCachingTranslator<Long,
 
     protected abstract List<? extends HistoryRelationshipRecord> loadRelationshipHistory(TranslationContext context, Collection<Long> entityIds);
 
+    protected List<? extends HistoryRecord> loadAbritraryHistory(TranslationContext context, Collection<Long> entityIds) {
+        return null;
+    }
+
     @Override
     protected ObjectHolder<List<HistoryEntry>> createObject(TranslationContext context, Long entityId, HistoryEntryFetchOptions fetchOptions)
     {
@@ -62,30 +67,34 @@ public abstract class HistoryTranslator extends AbstractCachingTranslator<Long,
     {
         List<? extends HistoryPropertyRecord> properties = loadPropertyHistory(entityIds);
         List<? extends HistoryRelationshipRecord> relationships = loadRelationshipHistory(context, entityIds);
+        List<? extends HistoryRecord> arbitraryRecords = loadAbritraryHistory(context, entityIds);
 
         Map<Long, Person> authorMap = new HashMap<>();
 
         if (fetchOptions.hasAuthor())
         {
+
             Set<Long> authorIds = new HashSet<Long>();
+
+            List<HistoryRecord> completeHistory = new ArrayList<>();
             if (properties != null)
             {
-                for (HistoryPropertyRecord property : properties)
-                {
-                    if (property.authorId != null)
-                    {
-                        authorIds.add(property.authorId);
-                    }
-                }
+                completeHistory.addAll(properties);
             }
             if (relationships != null)
             {
-                for (HistoryRelationshipRecord relationship : relationships)
+                completeHistory.addAll(relationships);
+            }
+            if (arbitraryRecords != null)
+            {
+                completeHistory.addAll(arbitraryRecords);
+            }
+
+            for (HistoryRecord record : completeHistory)
+            {
+                if (record.authorId != null)
                 {
-                    if (relationship.authorId != null)
-                    {
-                        authorIds.add(relationship.authorId);
-                    }
+                    authorIds.add(record.authorId);
                 }
             }
             authorMap = personTranslator.translate(context, authorIds, fetchOptions.withAuthor());
@@ -101,6 +110,10 @@ public abstract class HistoryTranslator extends AbstractCachingTranslator<Long,
         {
             createRelationshipEntries(entriesMap, relationships, authorMap, fetchOptions);
         }
+        if (arbitraryRecords != null)
+        {
+            createArbitraryEntries(entriesMap, arbitraryRecords, authorMap, fetchOptions);
+        }
 
         for (Long entityId : entityIds)
         {
@@ -171,6 +184,11 @@ public abstract class HistoryTranslator extends AbstractCachingTranslator<Long,
         return entry;
     }
 
+    protected void createArbitraryEntries(Map<Long, List<HistoryEntry>> entriesMap, List<? extends HistoryRecord> records,
+            Map<Long, Person> authorMap, HistoryEntryFetchOptions fetchOptions)
+    {
+    }
+
     private void createRelationshipEntries(Map<Long, List<HistoryEntry>> entriesMap, List<? extends HistoryRelationshipRecord> records,
             Map<Long, Person> authorMap, HistoryEntryFetchOptions fetchOptions)
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
index 2616f00df8bfc246220b71e9081815eb76ed75ec..5e40a49e48f729c1639f27939625f2d6ecc7bf2a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
@@ -124,6 +124,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.translator.SearchableS
 import ch.systemsx.cisd.openbis.generic.client.web.server.translator.UserFailureExceptionTranslator;
 import ch.systemsx.cisd.openbis.generic.client.web.server.util.TSVRenderer;
 import ch.systemsx.cisd.openbis.generic.shared.ICommonServer;
+import ch.systemsx.cisd.openbis.generic.shared.ICustomImportService;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
 import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchDomain;
 import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchDomainSearchOption;
@@ -241,7 +242,7 @@ import ch.systemsx.cisd.openbis.generic.shared.util.WebClientConfigUtils;
  * @author Franz-Josef Elmer
  */
 public final class CommonClientService extends AbstractClientService implements
-        ICommonClientService
+        ICommonClientService, ICustomImportService
 {
     @Resource(name = "registration-queue")
     private ConsumerQueue asyncRegistrationQueue;
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
index d17c567bd75ed5b73d867adb9b616f1f5c367042..0c8be1bc0d4ff0ca450ad880f2a0e0454fa8c139 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
@@ -465,6 +465,22 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
         return getDAOFactory().getPersonDAO().countActivePersons();
     }
 
+    public SessionContextDTO tryToAuthenticateAsSystem()
+    {
+        final PersonPE systemUser = getSystemUser();
+        HibernateUtils.initialize(systemUser.getAllPersonRoles());
+        RoleAssignmentPE role = new RoleAssignmentPE();
+        role.setRole(RoleCode.ADMIN);
+        systemUser.addRoleAssignment(role);
+        String sessionToken =
+                sessionManager.tryToOpenSession(systemUser.getUserId(),
+                        new AuthenticatedPersonBasedPrincipalProvider(systemUser));
+        Session session = sessionManager.getSession(sessionToken);
+        session.setPerson(systemUser);
+        session.setCreatorPerson(systemUser);
+        return tryGetSession(sessionToken);
+    }
+
     @Override
     public SessionContextDTO tryAuthenticateAnonymously()
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
index b3bbb5e222a37fc108d5ff524695a3425b167fcc..585e93d97d65573b195fb37c12ec1f4035bd242a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
@@ -211,7 +211,6 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ScriptPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SearchableEntity;
 import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
-import ch.systemsx.cisd.openbis.generic.shared.dto.SessionContextDTO;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyTermWithStats;
@@ -323,27 +322,6 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         return new CommonServerLogger(getSessionManager(), context);
     }
 
-    //
-    // ISystemAuthenticator
-    //
-
-    @Override
-    public SessionContextDTO tryToAuthenticateAsSystem()
-    {
-        final PersonPE systemUser = getSystemUser();
-        HibernateUtils.initialize(systemUser.getAllPersonRoles());
-        RoleAssignmentPE role = new RoleAssignmentPE();
-        role.setRole(RoleCode.ADMIN);
-        systemUser.addRoleAssignment(role);
-        String sessionToken =
-                sessionManager.tryToOpenSession(systemUser.getUserId(),
-                        new AuthenticatedPersonBasedPrincipalProvider(systemUser));
-        Session session = sessionManager.getSession(sessionToken);
-        session.setPerson(systemUser);
-        session.setCreatorPerson(systemUser);
-        return tryGetSession(sessionToken);
-    }
-
     //
     // IGenericServer
     //
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
index b311ae93581827d73962be8973c7ee904c168b4a..8f31613e613dfb92e2ed794b50a36c2db7e830be 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
@@ -18,8 +18,9 @@ package ch.systemsx.cisd.openbis.generic.server;
 
 import org.springframework.context.ApplicationContext;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.ethz.sis.openbis.generic.asapi.v3.plugin.service.IImportService;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.ApplicationServerApi;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
 import ch.systemsx.cisd.common.mail.IMailClient;
 import ch.systemsx.cisd.common.mail.MailClient;
 import ch.systemsx.cisd.common.mail.MailClientParameters;
@@ -47,6 +48,11 @@ public class CommonServiceProvider
                 .getBean(ResourceNames.COMMON_SERVER);
     }
 
+    public static IImportService getImportService()
+    {
+        return (IImportService) applicationContext.getBean(ResourceNames.IMPORT_SERVICE);
+    }
+
     public static IDAOFactory getDAOFactory()
     {
         return (IDAOFactory) applicationContext.getBean("dao-factory");
@@ -66,9 +72,9 @@ public class CommonServiceProvider
         return new MailClient(mailClientParameters);
     }
 
-    public static IApplicationServerApi getApplicationServerApi()
+    public static IApplicationServerInternalApi getApplicationServerApi()
     {
-        return (IApplicationServerApi) applicationContext.getBean(ApplicationServerApi.INTERNAL_SERVICE_NAME);
+        return (IApplicationServerInternalApi) applicationContext.getBean(ApplicationServerApi.INTERNAL_SERVICE_NAME);
     }
 
     public static Object tryToGetBean(String beanName)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
index 2432d8f1e8f2d48d248e0c6d9131de54d8072f82..abb8ceb92a739faf722973bcd6621aaf609e19a6 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
@@ -316,6 +316,8 @@ public final class SampleTable extends AbstractSampleBusinessObject implements I
         {
             relationshipService.assignSampleToSpace(session, sample, sample.getSpace());
         }
+
+        RelationshipUtils.updateModificationDateAndModifier(sample, session, getTransactionTimeStamp());
     }
 
     /**
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.java
new file mode 100644
index 0000000000000000000000000000000000000000..db1fe91cf09dea6233ac680df34b6779da3e2bc1
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.task;
+
+import java.util.List;
+
+class Group
+{
+    private String name;
+
+    private List<String> ldapGroupKeys;
+
+    private List<String> admins;
+
+    private List<String> usersBlackList;
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public List<String> getAdmins()
+    {
+        return admins;
+    }
+
+    public List<String> getLdapGroupKeys()
+    {
+        return ldapGroupKeys;
+    }
+
+    public List<String> getUsersBlackList()
+    {
+        return usersBlackList;
+    }
+
+}
\ No newline at end of file
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..42b697b343c84f13e1d0604c754fcf19cd58c2c1
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.task;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.authentication.ldap.LDAPAuthenticationService;
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.logging.Log4jSimpleLogger;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.maintenance.IMaintenanceTask;
+import ch.systemsx.cisd.openbis.generic.server.CommonServiceProvider;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class UserManagementMaintenanceTask implements IMaintenanceTask
+{
+    private static final String CONFIGURATION_FILE_PATH_PROPERTY = "configuration-file-path";
+
+    private static final String DEFAULT_CONFIGURATION_FILE_PATH = "etc/user-management-maintenance-config.json";
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            UserManagementMaintenanceTask.class);
+
+    private File configurationFile;
+
+    private LDAPAuthenticationService ldapService;
+
+    @Override
+    public void setUp(String pluginName, Properties properties)
+    {
+        operationLog.info("Setup plugin " + pluginName);
+        configurationFile = new File(properties.getProperty(CONFIGURATION_FILE_PATH_PROPERTY, DEFAULT_CONFIGURATION_FILE_PATH));
+        if (configurationFile.isFile() == false)
+        {
+            throw new ConfigurationFailureException("Configuration file '" + configurationFile.getAbsolutePath()
+                    + "' doesn't exist or is a directory.");
+        }
+        ldapService = (LDAPAuthenticationService) CommonServiceProvider.getApplicationContext().getBean("ldap-authentication-service");
+        operationLog.info("Plugin '" + pluginName + "' initialized. Configuration file: " + configurationFile.getAbsolutePath());
+        
+    }
+
+    @Override
+    public void execute()
+    {
+        Map<String, Group> groups = readGroupDefinitions();
+        if (groups == null)
+        {
+            return;
+        }
+        Log4jSimpleLogger logger = new Log4jSimpleLogger(operationLog);
+        UserManager userManager = new UserManager(CommonServiceProvider.getApplicationServerApi(), logger);
+        for (Entry<String, Group> entry : groups.entrySet())
+        {
+            String key = entry.getKey();
+            Group group = entry.getValue();
+            List<String> ldapGroupKeys = group.getLdapGroupKeys();
+            if (ldapGroupKeys == null || ldapGroupKeys.isEmpty())
+            {
+                operationLog.error("No ldapGroupKeys specified for group '" + key + "'. Task aborted.");
+                return;
+            }
+            Map<String, Principal> principalsByUserId = new TreeMap<>();
+            for (String ldapGroupKey : ldapGroupKeys)
+            {
+                if (StringUtils.isBlank(ldapGroupKey))
+                {
+                    operationLog.error("Empty ldapGroupKey for group '" + key + "'. Task aborted.");
+                    return;
+                    
+                }
+                List<Principal> principals = ldapService.listPrincipalsByKeyValue("ou", ldapGroupKey);
+                if (principals.isEmpty())
+                {
+                    operationLog.error("No users found for ldapGroupKey '" + ldapGroupKey + "' for group '" + key + "'. Task aborted.");
+                    return;
+                }
+                for (Principal principal : principals)
+                {
+                    principalsByUserId.put(principal.getUserId(), principal);
+                }
+            }
+            userManager.addGroup(key, group, principalsByUserId);
+        }
+        userManager.manageUsers();
+        operationLog.info("finished");
+    }
+    
+    private Map<String, Group> readGroupDefinitions()
+    {
+        if (configurationFile.isFile() == false)
+        {
+            operationLog.error("Configuration file '" + configurationFile.getAbsolutePath() + "' doesn't exist or is a directory.");
+            return null;
+        }
+        String serializedConfig = FileUtilities.loadToString(configurationFile);
+        try
+        {
+            return deserialize(serializedConfig);
+        } catch (Exception e)
+        {
+            operationLog.error("Invalid content of configuration file '" + configurationFile.getAbsolutePath() + "': " + e, e);
+            return null;
+        }
+    }
+
+    private Map<String, Group> deserialize(String serializedConfig) throws Exception
+    {
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readValue(serializedConfig, new TypeReference<Map<String, Group>>(){});
+    }
+    
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..12effa23e0a731ec0140264e0f36beb0c51fb1c0
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.task;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.AuthorizationGroup;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.fetchoptions.AuthorizationGroupFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.AuthorizationGroupPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.IAuthorizationGroupId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.fetchoptions.PersonFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.IPersonId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.PersonPermId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.logging.ISimpleLogger;
+import ch.systemsx.cisd.common.logging.LogLevel;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+class UserManager
+{
+    private final IApplicationServerInternalApi service;
+
+    private final ISimpleLogger logger;
+    
+    private final Map<String, UserInfo> userInfosByUserId = new TreeMap<>();
+    
+    private final List<String> groupCodes = new ArrayList<>();
+
+    UserManager(IApplicationServerInternalApi service, ISimpleLogger logger)
+    {
+        this.service = service;
+        this.logger = logger;
+    }
+
+    public void addGroup(String key, Group group, Map<String, Principal> principalsByUserId)
+    {
+        groupCodes.add(key);
+        Set<String> admins = asSet(group.getAdmins());
+        Set<String> blackListedUsers = asSet(group.getUsersBlackList());
+        for (Principal principal : principalsByUserId.values())
+        {
+            String userId = principal.getUserId();
+            UserInfo userInfo = userInfosByUserId.get(userId);
+            if (userInfo == null)
+            {
+                userInfo = new UserInfo(principal);
+                userInfosByUserId.put(userId, userInfo);
+            }
+            userInfo.addGroupInfo(new GroupInfo(key, admins.contains(userId), blackListedUsers.contains(userId)));
+        }
+        logger.log(LogLevel.INFO, principalsByUserId.size() + " users for group " + key);
+    }
+
+    public void manageUsers()
+    {
+        String sessionToken = service.loginAsSystem();
+        Map<IPersonId, Person> users = getUsersWithRoleAssigments(sessionToken);
+        Map<IAuthorizationGroupId, AuthorizationGroup> authorizationGroups = getAuthorizationGroups(sessionToken);
+        for (UserInfo userInfo : userInfosByUserId.values())
+        {
+            manageUser(userInfo, users, authorizationGroups);
+        }
+        service.logout(sessionToken);
+    }
+    
+    private void manageUser(UserInfo userInfo, Map<IPersonId, Person> knownUsers, 
+            Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+    {
+        
+    }
+
+    private Map<IPersonId, Person> getUsersWithRoleAssigments(String sessionToken)
+    {
+        Function<String, PersonPermId> mapper = userId -> new PersonPermId(userId);
+        List<PersonPermId> userIds = userInfosByUserId.keySet().stream().map(mapper).collect(Collectors.toList());
+        PersonFetchOptions fetchOptions = new PersonFetchOptions();
+        fetchOptions.withRoleAssignments().withSpace();
+        Map<IPersonId, Person> users = service.getPersons(sessionToken, userIds, fetchOptions);
+        return users;
+    }
+
+    private Map<IAuthorizationGroupId, AuthorizationGroup> getAuthorizationGroups(String sessionToken)
+    {
+        Function<String, AuthorizationGroupPermId> mapper = key -> new AuthorizationGroupPermId(key);
+        List<AuthorizationGroupPermId> groupPermIds = groupCodes.stream().map(mapper).collect(Collectors.toList());
+        AuthorizationGroupFetchOptions fetchOptions = new AuthorizationGroupFetchOptions();
+        fetchOptions.withUsers();
+        return service.getAuthorizationGroups(sessionToken, groupPermIds, fetchOptions);
+    }
+    
+    private Set<String> asSet(List<String> users)
+    {
+        return users == null ? Collections.emptySet() : new TreeSet<>(users);
+    }
+
+    private static class UserInfo
+    {
+        private Principal principal;
+
+        private Map<String, GroupInfo> groupInfosByGroupKey = new TreeMap<>();
+
+        public UserInfo(Principal principal)
+        {
+            this.principal = principal;
+        }
+
+        public Principal getPrincipal()
+        {
+            return principal;
+        }
+
+        public void addGroupInfo(GroupInfo groupInfo)
+        {
+            groupInfosByGroupKey.put(groupInfo.getKey(), groupInfo);
+        }
+
+        @Override
+        public String toString()
+        {
+            return principal.getUserId() + " " + groupInfosByGroupKey.values();
+        }
+    }
+
+    private static class GroupInfo
+    {
+        private String key;
+
+        private boolean admin;
+
+        private boolean onBlackList;
+
+        GroupInfo(String key, boolean admin, boolean onBlackList)
+        {
+            this.key = key;
+            this.admin = admin;
+            this.onBlackList = onBlackList;
+        }
+
+        public String getKey()
+        {
+            return key;
+        }
+
+        public boolean isAdmin()
+        {
+            return admin;
+        }
+
+        public boolean isOnBlackList()
+        {
+            return onBlackList;
+        }
+
+        @Override
+        public String toString()
+        {
+            return (onBlackList ? "." : "") + key + (admin ? "*" : "");
+        }
+
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICustomImportService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICustomImportService.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a36b36f53a60fd32379bd3b9bdaef665ed32a45
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICustomImportService.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.shared;
+
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
+
+/**
+ * @author pkupczyk
+ */
+public interface ICustomImportService
+{
+
+    public List<BatchRegistrationResult> performCustomImport(String sessionKey, String customImportCode, boolean async, String userEmail);
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IEntityImportService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IEntityImportService.java
new file mode 100644
index 0000000000000000000000000000000000000000..effab330fc838ad57e0c91e370c7b50b97dff566
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IEntityImportService.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.shared;
+
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.BatchRegistrationResult;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.MaterialType;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SampleType;
+
+/**
+ * @author pkupczyk
+ */
+public interface IEntityImportService
+{
+
+    public List<BatchRegistrationResult> registerExperiments(ExperimentType experimentType, String sessionKey, boolean async, String userEmail);
+
+    public List<BatchRegistrationResult> updateExperiments(ExperimentType experimentType, String sessionKey, boolean async, String userEmail);
+
+    public List<BatchRegistrationResult> registerSamplesWithSilentOverrides(SampleType sampleType, String spaceIdentifierSilentOverrideOrNull,
+            String experimentIdentifierSilentOverrideOrNull, String sessionKey, boolean async, String userEmail, String defaultGroupIdentifier,
+            boolean updateExisting);
+
+    public List<BatchRegistrationResult> updateSamplesWithSilentOverrides(SampleType sampleType, String spaceIdentifierSilentOverrideOrNull,
+            String experimentIdentifierSilentOverrideOrNull, String sessionKey, boolean async, String userEmail, String defaultGroupIdentifier);
+
+    public List<BatchRegistrationResult> updateDataSets(DataSetType dataSetType, String sessionKey, boolean async, String userEmail);
+
+    public List<BatchRegistrationResult> registerMaterials(MaterialType materialType, boolean updateExisting, final String sessionKey, boolean async,
+            String userEmail);
+
+    public List<BatchRegistrationResult> updateMaterials(MaterialType materialType, String sessionKey, boolean ignoreUnregistered,
+            boolean async, String userEmail);
+
+    public List<BatchRegistrationResult> registerOrUpdateSamplesAndMaterials(String sessionKey, String defaultGroupIdentifier, boolean updateExisting,
+            boolean async, String userEmail);
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ResourceNames.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ResourceNames.java
index 03492b312079b84558098fe65c3523c72c1577a5..afb075c8cdeca7e570fc0280390f871633ec9530 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ResourceNames.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ResourceNames.java
@@ -60,6 +60,8 @@ public final class ResourceNames
     public final static String COMMON_SERVICE = "common-service";
 
     public final static String COMMON_SERVER = "common-server";
+    
+    public final static String IMPORT_SERVICE = "import-service";
 
     public final static String TRACKING_SERVER = "tracking-server";
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java
index d04c1aa2a9abb73875dd14e4d0615d9c7775629b..2e25dc082109cf4207591bd308e4b9ef9527b6fe 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/generic/client/web/server/GenericClientService.java
@@ -52,6 +52,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.translator.UserFailure
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.db.exception.SampleUniqueCodeViolationException;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.db.exception.SampleUniqueCodeViolationExceptionAbstract;
 import ch.systemsx.cisd.openbis.generic.shared.Constants;
+import ch.systemsx.cisd.openbis.generic.shared.IEntityImportService;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData;
@@ -110,7 +111,7 @@ import ch.systemsx.cisd.openbis.plugin.generic.shared.ResourceNames;
  * @author Franz-Josef Elmer
  */
 @Component(value = ResourceNames.GENERIC_PLUGIN_SERVICE)
-public class GenericClientService extends AbstractClientService implements IGenericClientService
+public class GenericClientService extends AbstractClientService implements IGenericClientService, IEntityImportService
 {
 
     @Resource(name = ResourceNames.GENERIC_PLUGIN_SERVER)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/history/ContentCopyHistoryEntry.js b/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/history/ContentCopyHistoryEntry.js
new file mode 100644
index 0000000000000000000000000000000000000000..62e47f831fca980b4aec1a6aec08968cb74dc7cf
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/history/ContentCopyHistoryEntry.js
@@ -0,0 +1,46 @@
+define([ "stjs", "util/Exceptions", "as/dto/history/HistoryEntry" ], function(stjs, exceptions, HistoryEntry) {
+	var ContentCopyHistoryEntry = function() {
+		HistoryEntry.call(this);
+	};
+	stjs.extend(ContentCopyHistoryEntry, HistoryEntry, [ HistoryEntry ], function(constructor, prototype) {
+		prototype['@type'] = 'as.dto.history.ContentCopyHistoryEntry';
+		constructor.serialVersionUID = 1;
+		prototype.externalCode = null;
+		prototype.path = null;
+		prototype.gitCommitHash = null;
+		prototype.gitRepositoryId = null;
+		prototype.externalDmsId = null;
+
+		prototype.getExternalCode = function() {
+			return this.externalCode;
+		};
+		prototype.setExternalCode = function(externalCode) {
+			this.externalCode = externalCode;
+		};
+		prototype.getPath = function() {
+			return this.path;
+		};
+		prototype.setPath = function(path) {
+			this.path = path;
+		};
+		prototype.getGitCommitHash = function() {
+			return this.gitCommitHash;
+		};
+		prototype.setGitCommitHash = function(gitCommitHash) {
+			this.gitCommitHash = gitCommitHash;
+		};
+		prototype.getGitRepositoryId = function() {
+			return this.gitRepositoryId;
+		};
+		prototype.setGitRepositoryId = function(gitRepositoryId) {
+			this.gitRepositoryId = gitRepositoryId;
+		};
+		prototype.getExternalDmsId = function() {
+			return this.externalDmsId;
+		};
+		prototype.setExternalDmsId = function(externalDmsId) {
+			this.externalDmsId = externalDmsId;
+		};
+}, {});
+	return ContentCopyHistoryEntry;
+})
\ No newline at end of file
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ExecuteOperationsTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ExecuteOperationsTest.java
index 73e604568179991bd24386f1b0de10c43fe504ba..034b5a18038fab6423f3da7256452c031893a5ad 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ExecuteOperationsTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ExecuteOperationsTest.java
@@ -23,19 +23,13 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 
-import java.io.File;
-import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
-import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang.time.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.PlatformTransactionManager;
@@ -92,6 +86,8 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.search.SpaceSearchCriteria
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.update.SpaceUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.update.UpdateSpacesOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.update.UpdateSpacesOperationResult;
+import ch.ethz.sis.openbis.systemtest.asapi.v3.util.EmailUtil;
+import ch.ethz.sis.openbis.systemtest.asapi.v3.util.EmailUtil.Email;
 import ch.systemsx.cisd.common.action.IDelegatedAction;
 import ch.systemsx.cisd.common.test.AssertionUtil;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SessionContextDTO;
@@ -106,10 +102,6 @@ public class ExecuteOperationsTest extends AbstractOperationExecutionTest
 
     private static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
 
-    private static final String EMAIL_DIR = "targets/email";
-
-    private static final String EMAIL_PATTERN = "Date: (.*)\nFrom: (.*)\nTo: (.*)\nSubject: (.*)\nContent:\n(.*)";
-
     private static final String EMAIL_FROM = "application_server@localhost";
 
     private static final String EMAIL_TO_1 = "test1@email.com";
@@ -412,7 +404,7 @@ public class ExecuteOperationsTest extends AbstractOperationExecutionTest
         assertEquals(historyEntry.getPropertyName(), "COMMENT");
         assertEquals(historyEntry.getPropertyValue(), "created");
     }
-    
+
     @Test
     public void testUpdateDataSetAndRelatedSample()
     {
@@ -842,7 +834,7 @@ public class ExecuteOperationsTest extends AbstractOperationExecutionTest
         CreateSpacesOperationResult result = (CreateSpacesOperationResult) results.getResults().get(0);
         assertEquals(result.getObjectIds(), Arrays.asList(new SpacePermId(creation.getCode())));
 
-        Email email = findLatestEmail();
+        Email email = EmailUtil.findLatestEmail();
         assertEquals(email.from, EMAIL_FROM);
         assertEquals(email.to, EMAIL_TO_1 + ", " + EMAIL_TO_2);
         assertEquals(email.subject, String.format("Operation execution %s finished", options.getExecutionId()));
@@ -872,7 +864,7 @@ public class ExecuteOperationsTest extends AbstractOperationExecutionTest
                 }
             }, "Code cannot be empty");
 
-        Email email = findLatestEmail();
+        Email email = EmailUtil.findLatestEmail();
         assertEquals(email.from, EMAIL_FROM);
         assertEquals(email.to, EMAIL_TO_1 + ", " + EMAIL_TO_2);
         assertEquals(email.subject, String.format("Operation execution %s failed", options.getExecutionId()));
@@ -897,61 +889,6 @@ public class ExecuteOperationsTest extends AbstractOperationExecutionTest
         return getExecution(sessionToken, options.getExecutionId(), fullOperationExecutionFetchOptions());
     }
 
-    private Email findLatestEmail()
-    {
-        File emailDir = new File(EMAIL_DIR);
-
-        if (emailDir.exists())
-        {
-            File[] emails = emailDir.listFiles();
-
-            if (emails != null && emails.length > 0)
-            {
-                Arrays.sort(emails, new Comparator<File>()
-                    {
-                        @Override
-                        public int compare(File f1, File f2)
-                        {
-                            return -f1.getName().compareTo(f2.getName());
-                        }
-                    });
-
-                File latestEmail = emails[0];
-                try
-                {
-                    String latestEmailContent = FileUtils.readFileToString(latestEmail);
-                    Pattern pattern = Pattern.compile(EMAIL_PATTERN, Pattern.DOTALL);
-
-                    Matcher m = pattern.matcher(latestEmailContent);
-                    if (m.find())
-                    {
-                        Email email = new Email();
-                        email.from = m.group(2);
-                        email.to = m.group(3);
-                        email.subject = m.group(4);
-                        email.content = m.group(5);
-                        return email;
-                    } else
-                    {
-                        throw new RuntimeException("Latest email content does not match the expected email pattern. The latest email content was:\n"
-                                + latestEmailContent + "\nThe expected email pattern was:\n" + EMAIL_PATTERN);
-                    }
-
-                } catch (IOException e)
-                {
-                    throw new RuntimeException("Could not read the latest email " + latestEmail.getAbsolutePath(), e);
-                }
-
-            } else
-            {
-                throw new RuntimeException("No emails found in " + emailDir.getAbsolutePath() + " directory");
-            }
-        } else
-        {
-            throw new RuntimeException("Email directory " + emailDir.getAbsolutePath() + " does not exist");
-        }
-    }
-
     private Set<String> getAllSpaceCodes()
     {
         // do it in a separate transaction to get only spaces that won't be rolled back
@@ -992,15 +929,4 @@ public class ExecuteOperationsTest extends AbstractOperationExecutionTest
         }
     }
 
-    private class Email
-    {
-        private String from;
-
-        private String to;
-
-        private String subject;
-
-        private String content;
-    }
-
 }
\ No newline at end of file
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/util/EmailUtil.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/util/EmailUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..ccf9b78fbefe860c61a3fcbb19a3bbac84060dd1
--- /dev/null
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/util/EmailUtil.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2018 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.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+
+/**
+ * @author pkupczyk
+ */
+public class EmailUtil
+{
+
+    private static final String EMAIL_DIR = "targets/email";
+
+    private static final String EMAIL_PATTERN = "Date: (.*)\nFrom: (.*)\nTo: (.*)\nSubject: (.*)\nContent:\n(.*)";
+
+    public static Email findLatestEmail()
+    {
+        File emailDir = new File(EMAIL_DIR);
+
+        if (emailDir.exists())
+        {
+            File[] emails = emailDir.listFiles();
+
+            if (emails != null && emails.length > 0)
+            {
+                Arrays.sort(emails, new Comparator<File>()
+                    {
+                        @Override
+                        public int compare(File f1, File f2)
+                        {
+                            return -f1.getName().compareTo(f2.getName());
+                        }
+                    });
+
+                File latestEmail = emails[0];
+                try
+                {
+                    String latestEmailContent = FileUtils.readFileToString(latestEmail);
+                    Pattern pattern = Pattern.compile(EMAIL_PATTERN, Pattern.DOTALL);
+
+                    Matcher m = pattern.matcher(latestEmailContent);
+                    if (m.find())
+                    {
+                        Email email = new Email();
+                        email.timestamp = latestEmail.lastModified();
+                        email.from = m.group(2);
+                        email.to = m.group(3);
+                        email.subject = m.group(4);
+                        email.content = m.group(5);
+                        return email;
+                    } else
+                    {
+                        throw new RuntimeException("Latest email content does not match the expected email pattern. The latest email content was:\n"
+                                + latestEmailContent + "\nThe expected email pattern was:\n" + EMAIL_PATTERN);
+                    }
+
+                } catch (IOException e)
+                {
+                    throw new RuntimeException("Could not read the latest email " + latestEmail.getAbsolutePath(), e);
+                }
+
+            } else
+            {
+                return null;
+            }
+        } else
+        {
+            throw new RuntimeException("Email directory " + emailDir.getAbsolutePath() + " does not exist");
+        }
+    }
+
+    public static class Email
+    {
+
+        public long timestamp;
+
+        public String from;
+
+        public String to;
+
+        public String subject;
+
+        public String content;
+
+        @Override
+        public String toString()
+        {
+            return new ReflectionToStringBuilder(this).toString();
+        }
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0fe3386b39b0cfb6e9051eacd946116f932c381
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.task;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.AuthorizationGroup;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.fetchoptions.AuthorizationGroupFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.AuthorizationGroupPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.IAuthorizationGroupId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.fetchoptions.PersonFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.IPersonId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.PersonPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.Role;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.RoleAssignment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.RoleLevel;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.fetchoptions.RoleAssignmentFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.logging.MockLogger;
+import ch.systemsx.cisd.common.test.RecordingMatcher;
+import ch.systemsx.cisd.common.test.ToStringMatcher;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class UserManagerTest
+{
+    private static final String SESSION_TOKEN = "session-123";
+
+    private static final Principal U1 = new Principal("u1", "Albert", "Einstein", "a.e@abc.de");
+
+    private static final Principal U2 = new Principal("u2", "Isaac", "Newton", "i.n@abc.de");
+
+    private static final Principal U3 = new Principal("u3", "Alan", "Turing", "a.t@abc.de");
+
+    private Mockery context;
+
+    private IApplicationServerInternalApi service;
+
+    private UserManager userManager;
+
+    private MockLogger logger;
+
+    @BeforeMethod
+    public void setUp()
+    {
+        context = new Mockery();
+        service = context.mock(IApplicationServerInternalApi.class);
+        context.checking(new Expectations()
+            {
+                {
+                    one(service).loginAsSystem();
+                    will(returnValue(SESSION_TOKEN));
+
+                    one(service).logout(SESSION_TOKEN);
+                }
+            });
+        logger = new MockLogger();
+        userManager = new UserManager(service, logger);
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        // To following line of code should also be called at the end of each test method.
+        // Otherwise one does not known which test failed.
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testAddNewNormalUser()
+    {
+        // Given
+        RecordingMatcher<List<IPersonId>> personsMatcher = prepareGetUsersWithRoleAssigments(new PersonBuilder(U1).get());
+        RecordingMatcher<List<AuthorizationGroupPermId>> groupsMatcher = prepareGetAuthorizationGroups();
+
+        userManager.addGroup("G1", new Group(), principals(U2, U1));
+
+        // When
+        userManager.manageUsers();
+
+        // Then
+        assertEquals(personsMatcher.recordedObject().toString(), "[u1, u2]");
+        assertEquals(groupsMatcher.recordedObject().toString(), "[G1]");
+        context.assertIsSatisfied();
+    }
+
+    // @Test
+    public void test2()
+    {
+        // Given
+
+        // When
+        userManager.manageUsers();
+
+        // Then
+        context.assertIsSatisfied();
+    }
+
+    // @Test
+    public void test3()
+    {
+        // Given
+
+        // When
+        userManager.manageUsers();
+
+        // Then
+        context.assertIsSatisfied();
+    }
+
+    private RecordingMatcher<List<IPersonId>> prepareGetUsersWithRoleAssigments(Person... persons)
+    {
+        Map<IPersonId, Person> result = new LinkedHashMap<>();
+        for (Person person : persons)
+        {
+            result.put(person.getPermId(), person);
+        }
+        RecordingMatcher<List<IPersonId>> matcher = new RecordingMatcher<>();
+        context.checking(new Expectations()
+            {
+                {
+                    PersonFetchOptions fetchOptions = new PersonFetchOptions();
+                    fetchOptions.withRoleAssignments().withSpace();
+                    one(service).getPersons(with(SESSION_TOKEN), with(matcher), with(new ToStringMatcher<>(fetchOptions)));
+                    will(returnValue(result));
+                }
+            });
+        return matcher;
+    }
+
+    private RecordingMatcher<List<AuthorizationGroupPermId>> prepareGetAuthorizationGroups(AuthorizationGroup... authorizationGroups)
+    {
+        Map<IAuthorizationGroupId, AuthorizationGroup> result = new LinkedHashMap<>();
+        for (AuthorizationGroup authorizationGroup : authorizationGroups)
+        {
+            result.put(authorizationGroup.getPermId(), authorizationGroup);
+        }
+        RecordingMatcher<List<AuthorizationGroupPermId>> matcher = new RecordingMatcher<>();
+        context.checking(new Expectations()
+            {
+                {
+                    AuthorizationGroupFetchOptions fetchOptions = new AuthorizationGroupFetchOptions();
+                    fetchOptions.withUsers();
+                    one(service).getAuthorizationGroups(with(SESSION_TOKEN), with(matcher), with(new ToStringMatcher<>(fetchOptions)));
+                    will(returnValue(result));
+                }
+            });
+        return matcher;
+    }
+
+    private Map<String, Principal> principals(Principal... principals)
+    {
+        Map<String, Principal> map = new TreeMap<>();
+        for (Principal principal : principals)
+        {
+            map.put(principal.getUserId(), principal);
+        }
+        return map;
+    }
+
+    private RoleAssignment ra(Role role)
+    {
+        return ra(role, null);
+    }
+
+    private RoleAssignment ra(Role role, String spaceCodeOrNull)
+    {
+        RoleAssignment roleAssignment = new RoleAssignment();
+        RoleAssignmentFetchOptions fetchOptions = new RoleAssignmentFetchOptions();
+        fetchOptions.withSpace();
+        roleAssignment.setFetchOptions(fetchOptions);
+        roleAssignment.setRole(role);
+        roleAssignment.setRoleLevel(RoleLevel.INSTANCE);
+        if (spaceCodeOrNull != null)
+        {
+            Space space = new Space();
+            space.setCode(spaceCodeOrNull);
+            space.setPermId(new SpacePermId(spaceCodeOrNull));
+            space.setFetchOptions(new SpaceFetchOptions());
+            roleAssignment.setSpace(space);
+            roleAssignment.setRoleLevel(RoleLevel.SPACE);
+        }
+        return roleAssignment;
+    }
+
+    private static Person createPerson(Principal principal, PersonFetchOptions fetchOptions)
+    {
+        Person person = new Person();
+        person.setFetchOptions(fetchOptions);
+        person.setUserId(principal.getUserId());
+        person.setPermId(new PersonPermId(principal.getUserId()));
+        person.setEmail(principal.getEmail());
+        person.setFirstName(principal.getFirstName());
+        person.setLastName(principal.getLastName());
+        person.setRoleAssignments(new ArrayList<RoleAssignment>());
+        person.setActive(true);
+        return person;
+    }
+
+    private static final class PersonBuilder
+    {
+        private Person person;
+
+        PersonBuilder(Principal principal)
+        {
+            PersonFetchOptions fetchOptions = new PersonFetchOptions();
+            fetchOptions.withRoleAssignments().withSpace();
+            person = createPerson(principal, fetchOptions);
+        }
+
+        Person get()
+        {
+            return person;
+        }
+
+        PersonBuilder roleAssignments(RoleAssignment... roleAssignments)
+        {
+            for (RoleAssignment roleAssignment : roleAssignments)
+            {
+                person.getRoleAssignments().add(roleAssignment);
+            }
+            return this;
+        }
+
+        PersonBuilder deactive()
+        {
+            person.setActive(false);
+            return this;
+        }
+    }
+
+    private static final class AuthorizationGroupBuilder
+    {
+        private AuthorizationGroup authorizationGroup;
+
+        AuthorizationGroupBuilder(String code)
+        {
+            authorizationGroup = new AuthorizationGroup();
+            AuthorizationGroupFetchOptions fetchOptions = new AuthorizationGroupFetchOptions();
+            fetchOptions.withUsers();
+            authorizationGroup.setFetchOptions(fetchOptions);
+            authorizationGroup.setCode(code);
+            authorizationGroup.setPermId(new AuthorizationGroupPermId(code));
+            authorizationGroup.setUsers(new ArrayList<>());
+        }
+
+        public AuthorizationGroupBuilder users(Principal... principals)
+        {
+            PersonFetchOptions personFetchOptions = new PersonFetchOptions();
+            Function<Principal, Person> mapper = p -> createPerson(p, personFetchOptions);
+            authorizationGroup.getUsers().addAll(Arrays.asList(principals).stream().map(mapper).collect(Collectors.toList()));
+            return this;
+        }
+
+        AuthorizationGroup get()
+        {
+            return authorizationGroup;
+        }
+    }
+}
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/history/ContentCopyHistoryEntry.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/history/ContentCopyHistoryEntry.java
new file mode 100644
index 0000000000000000000000000000000000000000..6fa5665e3af68fec44f57c026f301170909249de
--- /dev/null
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/history/ContentCopyHistoryEntry.java
@@ -0,0 +1,112 @@
+/*
+ * 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.asapi.v3.dto.history;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import ch.systemsx.cisd.base.annotation.JsonObject;
+
+@JsonObject("as.dto.history.ContentCopyHistoryEntry")
+public class ContentCopyHistoryEntry extends HistoryEntry
+{
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty
+    private String externalCode;
+
+    @JsonProperty
+    private String path;
+
+    @JsonProperty
+    private String gitCommitHash;
+
+    @JsonProperty
+    private String gitRepositoryId;
+
+    @JsonProperty
+    private Long externalDmsId;
+
+
+    @JsonIgnore
+    public String getExternalCode()
+    {
+        return externalCode;
+    }
+
+    @JsonIgnore
+    public void setExternalCode(String externalCode)
+    {
+        this.externalCode = externalCode;
+    }
+
+    @JsonIgnore
+    public String getPath()
+    {
+        return path;
+    }
+
+    @JsonIgnore
+    public void setPath(String path)
+    {
+        this.path = path;
+    }
+
+    @JsonIgnore
+    public String getGitCommitHash()
+    {
+        return gitCommitHash;
+    }
+
+    @JsonIgnore
+    public void setGitCommitHash(String gitCommitHash)
+    {
+        this.gitCommitHash = gitCommitHash;
+    }
+
+    @JsonIgnore
+    public String getGitRepositoryId()
+    {
+        return gitRepositoryId;
+    }
+
+    @JsonIgnore
+    public void setGitRepositoryId(String gitRepositoryId)
+    {
+        this.gitRepositoryId = gitRepositoryId;
+    }
+
+    @JsonIgnore
+    public Long getExternalDmsId()
+    {
+        return externalDmsId;
+    }
+
+    @JsonIgnore
+    public void setExternalDmsId(Long externalDmsId)
+    {
+        this.externalDmsId = externalDmsId;
+    }
+
+    @JsonIgnore
+    public static long getSerialversionuid()
+    {
+        return serialVersionUID;
+    }
+
+}
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/ICustomASServiceExecutor.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/ICustomASServiceExecutor.java
index 2746d4af4ec37cd7a8115d0c6a34952eb369af09..b9c32d6ebf546b0b043371d9f75655fe30edc618 100644
--- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/ICustomASServiceExecutor.java
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/ICustomASServiceExecutor.java
@@ -16,6 +16,8 @@
 
 package ch.ethz.sis.openbis.generic.asapi.v3.plugin.service;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
+
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.service.CustomASService;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.service.CustomASServiceExecutionOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.plugin.service.context.CustomASServiceContext;
@@ -25,6 +27,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.plugin.service.context.CustomASServi
  * 
  * @author Franz-Josef Elmer
  */
+@JsonIgnoreType
 public interface ICustomASServiceExecutor
 {
     public Object executeService(CustomASServiceContext context, CustomASServiceExecutionOptions options);
diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/IImportService.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/IImportService.java
new file mode 100644
index 0000000000000000000000000000000000000000..11cff71a171dcdf760ca0beb5a5ae01cd32eb4c6
--- /dev/null
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/IImportService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 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.plugin.service;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
+
+/**
+ * @author pkupczyk
+ */
+@JsonIgnoreType
+public interface IImportService
+{
+
+    public String createExperiments(String sessionToken, String uploadKey, String experimentTypeCode, boolean async, String userEmail);
+
+    public String updateExperiments(String sessionToken, String uploadKey, String experimentTypeCode, boolean async, String userEmail);
+
+    public String createSamples(String sessionToken, String uploadKey, String sampleTypeCode, String defaultSpaceIdentifier,
+            String spaceIdentifierOverride, String experimentIdentifierOverride, boolean updateExisting, boolean async, String userEmail);
+
+    public String updateSamples(String sessionToken, String uploadKey, String sampleTypeCode, String defaultSpaceIdentifier,
+            String spaceIdentifierOverride, String experimentIdentifierOverride, boolean async, String userEmail);
+
+    public String updateDataSets(String sessionToken, String uploadKey, String dataSetTypeCode, boolean async, String userEmail);
+
+    public String createMaterials(String sessionToken, String uploadKey, String materialTypeCode, boolean updateExisting, boolean async,
+            String userEmail);
+
+    public String updateMaterials(String sessionToken, String uploadKey, String materialTypeCode, boolean ignoreUnregistered, boolean async,
+            String userEmail);
+
+    public String generalImport(String sessionToken, String uploadKey, String defaultSpaceIdentifier, boolean updateExisting, boolean async,
+            String userEmail);
+
+    public String customImport(String sessionToken, String uploadKey, String customImportCode, boolean async, String userEmail);
+
+}
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 62b71a076b04313c444a657fd7e8caf8af9d37e9..a2cab867610a17f5fc260e385fa827ce599e150e 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
@@ -79,6 +79,7 @@ COMPONENT
 CONTAINER
 contains
 containsAll
+ContentCopyHistoryEntry
 count
 createDataSets
 createExperiments
@@ -2122,3 +2123,7 @@ Person Fetch Options All WebApp Settings Handler
 Person Fetch Options WebApp Settings Handler
 WebApp Settings Fetch Options All Settings Handler
 WebApp Settings Fetch Options Settings Handler
+
+custom Import
+general Import
+I Import Service
\ No newline at end of file
diff --git a/openbis_standard_technologies/dist/server/service.properties b/openbis_standard_technologies/dist/server/service.properties
index 865cac6396424b6a9b52bd1fa36ab186612808e9..8665d1c2cf2c86ca76f3fc7fe0c129a7767e5a5d 100644
--- a/openbis_standard_technologies/dist/server/service.properties
+++ b/openbis_standard_technologies/dist/server/service.properties
@@ -81,6 +81,8 @@ ldap.security.authentication-method =
 # "ignore" - ignore referrals
 # "throw" - throw ReferralException when a referral is encountered
 ldap.referral =
+# The search base. 
+ldap.searchBase =
 # The attribute name for the user id, defaults to "uid"
 ldap.attributenames.user.id =
 # The attribute name for the email, defaults to "mail"