From 7f12b02dc0d70c524626e94f78bd0a93c46d64ba Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Sun, 8 Apr 2018 17:57:09 +0200
Subject: [PATCH] SSDM-6205 : V3 AS API Batch Imports - Expose legacy methods
 using AS service

---
 datastore_server/etc/service.properties       |   2 +
 .../core-plugins/core-plugins.properties      |   2 +-
 .../test-custom-import/plugin.properties      |   4 +
 .../1/as/services/import-test/import-test.py  |  36 +++
 .../as/services/import-test/plugin.properties |   2 +
 .../test-custom-import/plugin.properties      |   6 +
 .../drop-boxes/test-custom-import/script.py   |   5 +
 .../systemtest/api/v3/AbstractFileTest.java   |   6 +-
 .../api/v3/CreateExperimentsImportTest.java   |  76 +++++
 .../api/v3/CreateMaterialsImportTest.java     |  76 +++++
 .../api/v3/CreateSamplesImportTest.java       |  77 ++++++
 .../systemtest/api/v3/CustomImportTest.java   |  81 ++++++
 .../systemtest/api/v3/GeneralImportTest.java  | 111 ++++++++
 .../materials_excel_97_2003.xls               | Bin 0 -> 28160 bytes
 .../systemtest/api/v3/ObjectsImportTest.java  | 261 ++++++++++++++++++
 .../api/v3/UpdateDataSetsImportTest.java      | 106 +++++++
 .../api/v3/UpdateExperimentsImportTest.java   |  91 ++++++
 .../api/v3/UpdateMaterialsImportTest.java     |  89 ++++++
 .../api/v3/UpdateSamplesImportTest.java       |  92 ++++++
 .../CustomASServiceScriptRunnerFactory.java   |  19 +-
 .../v3/helper/service/ImportService.java      | 212 ++++++++++++++
 .../JythonBasedCustomASServiceExecutor.java   |   3 +-
 .../web/server/CommonClientService.java       |   3 +-
 .../generic/server/CommonServiceProvider.java |   6 +
 .../generic/server/ICustomImportService.java  |  31 +++
 .../generic/server/IEntityImportService.java  |  55 ++++
 .../server/business/bo/SampleTable.java       |   2 +
 .../openbis/generic/shared/ResourceNames.java |   2 +
 .../web/server/GenericClientService.java      |   3 +-
 .../asapi/v3/ExecuteOperationsTest.java       |  84 +-----
 .../systemtest/asapi/v3/util/EmailUtil.java   | 115 ++++++++
 .../v3/plugin/service/IImportService.java     |  48 ++++
 32 files changed, 1618 insertions(+), 88 deletions(-)
 create mode 100644 datastore_server/source/core-plugins/import-test/1/as/custom-imports/test-custom-import/plugin.properties
 create mode 100644 datastore_server/source/core-plugins/import-test/1/as/services/import-test/import-test.py
 create mode 100644 datastore_server/source/core-plugins/import-test/1/as/services/import-test/plugin.properties
 create mode 100644 datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/plugin.properties
 create mode 100644 datastore_server/sourceTest/core-plugins/generic-test/1/dss/drop-boxes/test-custom-import/script.py
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateExperimentsImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateMaterialsImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateSamplesImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CustomImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/GeneralImportTestResources/materials_excel_97_2003.xls
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/ObjectsImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateDataSetsImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateExperimentsImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateMaterialsImportTest.java
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateSamplesImportTest.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/helper/service/ImportService.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ICustomImportService.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/IEntityImportService.java
 create mode 100644 openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/util/EmailUtil.java
 create mode 100644 openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/IImportService.java

diff --git a/datastore_server/etc/service.properties b/datastore_server/etc/service.properties
index 21d6021e6f9..6da5a9c9691 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 ad076de30cf..36cf1a47b43 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 00000000000..fcbdb19ef13
--- /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 00000000000..ddf1d71e354
--- /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 00000000000..edcd58fc819
--- /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 00000000000..f71fcdefe5d
--- /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 00000000000..7fc3d060f7d
--- /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 08aa2477a35..208c4ab48e4 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 00000000000..51fbf9a17a1
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateExperimentsImportTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.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.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);
+
+        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);
+        }
+    }
+
+}
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 00000000000..145141637b7
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateMaterialsImportTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.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.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");
+
+        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);
+        }
+    }
+
+}
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 00000000000..16778971b6d
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CreateSamplesImportTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.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.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);
+
+        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);
+        }
+    }
+
+}
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 00000000000..1a9baee379f
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/CustomImportTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.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.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());
+
+        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);
+        }
+    }
+
+}
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 00000000000..275e704d060
--- /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");
+
+        try
+        {
+            deleteMaterials(sessionToken, materialPermId1, materialPermId2);
+
+            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
GIT binary patch
literal 28160
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
E7h3*wfB*mh

literal 0
HcmV?d00001

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 00000000000..b41d7e9b677
--- /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 00000000000..017f9322028
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateDataSetsImportTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.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());
+
+        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);
+        }
+    }
+
+}
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 00000000000..b2b28709521
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateExperimentsImportTest.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.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.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);
+
+        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);
+        }
+    }
+
+}
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 00000000000..a049a46f085
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateMaterialsImportTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.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");
+
+        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);
+        }
+    }
+
+}
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 00000000000..c6b1e6cd364
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UpdateSamplesImportTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.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);
+
+        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);
+        }
+    }
+
+}
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 7837c8e87a7..0954144dde8 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 00000000000..1866d55cff7
--- /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.server.ICustomImportService;
+import ch.systemsx.cisd.openbis.generic.server.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 08cbd921a05..3c56376af5a 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/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
index 2616f00df8b..6eb4083dcc9 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
@@ -123,6 +123,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.translator.SearchableE
 import ch.systemsx.cisd.openbis.generic.client.web.server.translator.SearchableSearchDomainTranslator;
 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.server.ICustomImportService;
 import ch.systemsx.cisd.openbis.generic.shared.ICommonServer;
 import ch.systemsx.cisd.openbis.generic.shared.IServer;
 import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.SearchDomain;
@@ -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/CommonServiceProvider.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
index b311ae93581..074dae2a3ad 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
@@ -19,6 +19,7 @@ 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.systemsx.cisd.common.mail.IMailClient;
 import ch.systemsx.cisd.common.mail.MailClient;
@@ -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");
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ICustomImportService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ICustomImportService.java
new file mode 100644
index 00000000000..1a2a9e0cb6b
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/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.server;
+
+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/server/IEntityImportService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/IEntityImportService.java
new file mode 100644
index 00000000000..f3015cc1eee
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/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.server;
+
+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/server/business/bo/SampleTable.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
index 2432d8f1e8f..abb8ceb92a7 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/shared/ResourceNames.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ResourceNames.java
index 03492b31207..afb075c8cde 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 d04c1aa2a9a..c86812246e5 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
@@ -49,6 +49,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.UploadedFilesBean;
 import ch.systemsx.cisd.openbis.generic.client.web.server.queue.ConsumerQueue;
 import ch.systemsx.cisd.openbis.generic.client.web.server.queue.ConsumerTask;
 import ch.systemsx.cisd.openbis.generic.client.web.server.translator.UserFailureExceptionTranslator;
+import ch.systemsx.cisd.openbis.generic.server.IEntityImportService;
 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;
@@ -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/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 73e60456817..034b5a18038 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 00000000000..ccf9b78fbef
--- /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_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 00000000000..b1c439a3df5
--- /dev/null
+++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/asapi/v3/plugin/service/IImportService.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * @author pkupczyk
+ */
+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);
+
+}
-- 
GitLab