From 99909ee8b9ee1ad0f28e714e491657785d553cc9 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Fri, 25 May 2018 20:03:04 +0200
Subject: [PATCH] SSDM-6207 : V3 AS API Batch Imports - UploadServiceServlet
 Refactoring - introduce AS session workspace and change the AS upload servlet
 to use it

---
 .../api/v3/CreateExperimentsImportTest.java   |   4 +
 .../api/v3/CreateMaterialsImportTest.java     |   4 +
 .../api/v3/CreateSamplesImportTest.java       |   4 +
 .../systemtest/api/v3/CustomImportTest.java   |   4 +
 .../systemtest/api/v3/GeneralImportTest.java  |   4 +
 .../systemtest/api/v3/ObjectsImportTest.java  |  27 +-
 .../api/v3/UpdateDataSetsImportTest.java      |   4 +
 .../api/v3/UpdateExperimentsImportTest.java   |   4 +
 .../api/v3/UpdateMaterialsImportTest.java     |   4 +
 .../api/v3/UpdateSamplesImportTest.java       |   4 +
 .../systemtest/api/v3/UploadServletTest.java  | 475 ++++++++++++++++++
 .../web/server/UploadServiceServlet.java      |  31 +-
 .../client/web/server/UploadedFilesBean.java  |  20 +-
 .../generic/server/AbstractServer.java        |  10 +-
 .../server/ISessionWorkspaceProvider.java     |  31 ++
 .../generic/server/SessionFactory.java        |   8 +-
 .../server/SessionWorkspaceProvider.java      | 107 ++++
 .../source/java/genericApplicationContext.xml |   1 +
 openbis/source/java/service.properties        |   2 +
 .../web/server/UploadServiceServletTest.java  |  15 +-
 20 files changed, 733 insertions(+), 30 deletions(-)
 create mode 100644 datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UploadServletTest.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ISessionWorkspaceProvider.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionWorkspaceProvider.java

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
index 8c5d208d931..546641eb423 100644
--- 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
@@ -46,6 +46,7 @@ public class CreateExperimentsImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("identifier", "DESCRIPTION");
             file.addLine(experimentIdentifier.getIdentifier(), "imported description");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Experiment experiment = getObject(sessionToken, experimentIdentifier);
             assertNull(experiment);
@@ -75,6 +76,9 @@ public class CreateExperimentsImportTest extends ObjectsImportTest
                 assertEquals("1 experiment(s) found and registered.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             ExperimentDeletionOptions options = new ExperimentDeletionOptions();
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
index c507327c7ba..4c842d79324 100644
--- 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
@@ -45,6 +45,7 @@ public class CreateMaterialsImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("code", "DESCRIPTION");
             file.addLine(materialPermId.getCode(), "imported description");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Material material = getObject(sessionToken, materialPermId);
             assertNull(material);
@@ -75,6 +76,9 @@ public class CreateMaterialsImportTest extends ObjectsImportTest
                 assertEquals("Registration/update of 1 material(s) is complete.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             MaterialDeletionOptions options = new MaterialDeletionOptions();
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
index 83f7a31f810..c4f23a444ec 100644
--- 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
@@ -46,6 +46,7 @@ public class CreateSamplesImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("identifier", "COMMENT");
             file.addLine(sampleIdentifier.getIdentifier(), "imported comment");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Sample sample = getObject(sessionToken, sampleIdentifier);
             assertNull(sample);
@@ -76,6 +77,9 @@ public class CreateSamplesImportTest extends ObjectsImportTest
                 assertEquals("Registration of 1 sample(s) is complete.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             SampleDeletionOptions options = new SampleDeletionOptions();
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
index ce545c8b938..165a7af966d 100644
--- 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
@@ -51,6 +51,7 @@ public class CustomImportTest extends ObjectsImportTest
             multiPart.close();
 
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, multiPart);
+            assertUploadedFiles(sessionToken, "test-file-content");
 
             DataSet dataSet = getObject(sessionToken, dataSetPermId);
             assertNull(dataSet);
@@ -80,6 +81,9 @@ public class CustomImportTest extends ObjectsImportTest
                 assertEquals("Import successfully completed.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             DataSetDeletionOptions options = new DataSetDeletionOptions();
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
index 8fa63d6701d..ac838263e91 100644
--- 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
@@ -66,6 +66,7 @@ public class GeneralImportTest extends ObjectsImportTest
             multiPart.close();
 
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, multiPart);
+            assertUploadedFiles(sessionToken, FileUtils.readFileToString(materialsFile));
 
             Map<String, Object> parameters = new HashMap<String, Object>();
             parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
@@ -95,6 +96,9 @@ public class GeneralImportTest extends ObjectsImportTest
                 assertEquals("Registration/update of 2 material(s) is complete.\nRegistration of 0 sample(s) is complete.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             deleteMaterials(sessionToken, materialPermId1, materialPermId2);
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
index b41d7e9b677..97808fd9202 100644
--- 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
@@ -16,6 +16,7 @@
 
 package ch.ethz.sis.openbis.generic.dss.systemtest.api.v3;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -23,6 +24,7 @@ import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
+import org.apache.commons.io.FileUtils;
 import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentProvider;
 import org.eclipse.jetty.client.api.ContentResponse;
@@ -85,7 +87,7 @@ public class ObjectsImportTest extends AbstractFileTest
     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;
@@ -129,6 +131,29 @@ public class ObjectsImportTest extends AbstractFileTest
         return uploadFiles(sessionToken, uploadSessionKey, multiPart);
     }
 
+    protected void assertUploadedFiles(String sessionToken, String... fileContents) throws Exception
+    {
+        File sessionWorkspaceRootDir = new File("targets/sessionWorkspace");
+        File sessionWorkspace = new File(sessionWorkspaceRootDir, sessionToken);
+        File[] files = sessionWorkspace.listFiles();
+
+        List<String> expectedContents = new ArrayList<String>(Arrays.asList(fileContents));
+        List<String> actualContents = new ArrayList<String>();
+
+        if (files != null)
+        {
+            for (File file : files)
+            {
+                actualContents.add(FileUtils.readFileToString(file));
+            }
+        }
+
+        expectedContents.sort(String.CASE_INSENSITIVE_ORDER);
+        actualContents.sort(String.CASE_INSENSITIVE_ORDER);
+
+        assertEquals(expectedContents, actualContents);
+    }
+
     protected String executeImport(String sessionToken, String operation, Map<String, Object> parameters)
     {
         CustomASServiceCode serviceId = new CustomASServiceCode("import-test");
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
index a18bb9e0d9a..5c86b06f0f6 100644
--- 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
@@ -78,6 +78,7 @@ public class UpdateDataSetsImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("code", "COMMENT");
             file.addLine(dataSetPermId.getPermId(), "imported comment");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Map<String, Object> parameters = new HashMap<String, Object>();
             parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
@@ -104,6 +105,9 @@ public class UpdateDataSetsImportTest extends ObjectsImportTest
                 assertEquals("1 data set(s) found and registered.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             DataSetDeletionOptions options = new DataSetDeletionOptions();
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
index 094fb142295..c907b907b6c 100644
--- 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
@@ -63,6 +63,7 @@ public class UpdateExperimentsImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("identifier", "DESCRIPTION");
             file.addLine(experimentIdentifier.getIdentifier(), "imported description");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Map<String, Object> parameters = new HashMap<String, Object>();
             parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
@@ -89,6 +90,9 @@ public class UpdateExperimentsImportTest extends ObjectsImportTest
                 assertEquals("Update of 1 experiment(s) is complete.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             ExperimentDeletionOptions options = new ExperimentDeletionOptions();
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
index d3a8e817b86..07f9e790eab 100644
--- 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
@@ -60,6 +60,7 @@ public class UpdateMaterialsImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("code", "DESCRIPTION");
             file.addLine(materialPermId.getCode(), "imported description");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Map<String, Object> parameters = new HashMap<String, Object>();
             parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
@@ -87,6 +88,9 @@ public class UpdateMaterialsImportTest extends ObjectsImportTest
                 assertEquals("1 material(s) updated.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             MaterialDeletionOptions options = new MaterialDeletionOptions();
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
index 01effa654c5..1c873ede746 100644
--- 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
@@ -63,6 +63,7 @@ public class UpdateSamplesImportTest extends ObjectsImportTest
             ImportFile file = new ImportFile("identifier", "COMMENT");
             file.addLine(sampleIdentifier.getIdentifier(), "imported comment");
             uploadFiles(sessionToken, TEST_UPLOAD_KEY, file.toString());
+            assertUploadedFiles(sessionToken, file.toString());
 
             Map<String, Object> parameters = new HashMap<String, Object>();
             parameters.put(PARAM_UPLOAD_KEY, TEST_UPLOAD_KEY);
@@ -90,6 +91,9 @@ public class UpdateSamplesImportTest extends ObjectsImportTest
                 assertEquals("Update of 1 sample(s) is complete.", message);
                 assertNoEmails(timestamp);
             }
+
+            assertUploadedFiles(sessionToken);
+
         } finally
         {
             SampleDeletionOptions options = new SampleDeletionOptions();
diff --git a/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UploadServletTest.java b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UploadServletTest.java
new file mode 100644
index 00000000000..d81b844f018
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/ethz/sis/openbis/generic/dss/systemtest/api/v3/UploadServletTest.java
@@ -0,0 +1,475 @@
+/*
+ * 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.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jetty.client.HttpClient;
+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.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.systemsx.cisd.common.http.JettyHttpClientFactory;
+import ch.systemsx.cisd.openbis.datastoreserver.systemtests.SystemTestCase;
+import ch.systemsx.cisd.openbis.generic.client.web.server.UploadedFilesBean;
+import ch.systemsx.cisd.openbis.generic.shared.util.TestInstanceHostUtils;
+
+/**
+ * @author pkupczyk
+ */
+public class UploadServletTest extends SystemTestCase
+{
+
+    private static final String SERVICE_URL = TestInstanceHostUtils.getOpenBISUrl() + "/openbis/upload";
+
+    private static final String PARAM_SESSION_ID = "sessionID";
+
+    private static final String PARAM_SESSION_KEY_PREFIX = "sessionKey_";
+
+    private static final String PARAM_SESSION_KEYS_NUMBER = "sessionKeysNumber";
+
+    private static final String USER = "test";
+
+    private static final String PASSWORD = "password";
+
+    private static final String FALSE_TRUE_PROVIDER = "false-true-provider";
+
+    private IApplicationServerApi as;
+
+    @BeforeClass
+    private void beforeClass() throws Exception
+    {
+        as = applicationContext.getBean(IApplicationServerApi.class);
+    }
+
+    @BeforeMethod
+    private void beforeMethod()
+    {
+        JettyHttpClientFactory.getHttpClient().getCookieStore().removeAll();
+    }
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUploadSingleFile(boolean withSessionToken) throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        if (withSessionToken)
+        {
+            cleanSessionWorkspace(sessionToken);
+        } else
+        {
+            initHttpSession(sessionToken);
+            cleanOSTempFolder();
+        }
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        if (withSessionToken)
+        {
+            request.param(PARAM_SESSION_ID, sessionToken);
+        }
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        request.send();
+
+        if (withSessionToken)
+        {
+            assertSessionWorkspaceFiles(sessionToken, "testContent");
+        } else
+        {
+            assertOSTempFolderFiles("testContent");
+        }
+    }
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUploadMultipleFilesUnderOneSessionKey(boolean withSessionToken) throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        if (withSessionToken)
+        {
+            cleanSessionWorkspace(sessionToken);
+        } else
+        {
+            initHttpSession(sessionToken);
+            cleanOSTempFolder();
+        }
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName1", "testFileName1", new StringContentProvider("testContent1"), null);
+        multipart.addFilePart("testFieldName2", "testFileName2", new StringContentProvider("testContent2"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        if (withSessionToken)
+        {
+            request.param(PARAM_SESSION_ID, sessionToken);
+        }
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        request.send();
+
+        if (withSessionToken)
+        {
+            assertSessionWorkspaceFiles(sessionToken, "testContent1", "testContent2");
+        } else
+        {
+            assertOSTempFolderFiles("testContent1", "testContent2");
+        }
+    }
+
+    @Test(dataProvider = FALSE_TRUE_PROVIDER)
+    public void testUploadMultipleFilesUnderMultipleSessionKeys(boolean withSessionToken) throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        if (withSessionToken)
+        {
+            cleanSessionWorkspace(sessionToken);
+        } else
+        {
+            initHttpSession(sessionToken);
+            cleanOSTempFolder();
+        }
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName1", "testFileName1", new StringContentProvider("testContent1"), null);
+        multipart.addFilePart("testFieldName2", "testFileName2", new StringContentProvider("testContent2"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        if (withSessionToken)
+        {
+            request.param(PARAM_SESSION_ID, sessionToken);
+        }
+        request.param(PARAM_SESSION_KEYS_NUMBER, "2");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "1", "testFieldName2");
+        request.content(multipart);
+
+        request.send();
+
+        if (withSessionToken)
+        {
+            assertSessionWorkspaceFiles(sessionToken, "testContent1", "testContent2");
+        } else
+        {
+            assertOSTempFolderFiles("testContent1", "testContent2");
+        }
+    }
+
+    @Test
+    public void testUploadWithHttpSessionAndWithoutSessionToken() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        initHttpSession(sessionToken);
+
+        cleanSessionWorkspace(sessionToken);
+        cleanOSTempFolder();
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        request.send();
+
+        assertSessionWorkspaceFiles(sessionToken);
+        assertOSTempFolderFiles("testContent");
+    }
+
+    @Test
+    public void testUploadWithoutHttpSessionAndWithoutSessionToken() throws Exception
+    {
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        ContentResponse response = request.send();
+        assertEquals("<message type=\"error\">Pre-existing session required but none found</message>", response.getContentAsString());
+    }
+
+    @Test
+    public void testUploadWithoutHttpSessionAndWithSessionToken() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        cleanSessionWorkspace(sessionToken);
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        request.send();
+
+        assertSessionWorkspaceFiles(sessionToken, "testContent");
+    }
+
+    @Test
+    public void testUploadWithoutSessionKeysNumber() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        ContentResponse response = request.send();
+        assertEquals("<message type=\"error\">No form field 'sessionKeysNumber' could be found in the transmitted form.</message>",
+                response.getContentAsString());
+    }
+
+    @Test
+    public void testUploadWithIncorrectSessionKeysNumberFormat() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "thisShouldBeANumber");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        ContentResponse response = request.send();
+        assertEquals("<message type=\"error\">For input string: \"thisShouldBeANumber\"</message>",
+                response.getContentAsString());
+    }
+
+    @Test
+    public void testUploadWithoutSessionKeyPrefix() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.content(multipart);
+
+        ContentResponse response = request.send();
+
+        assertEquals("<message type=\"error\">No field '" + PARAM_SESSION_KEY_PREFIX + "0' could be found in the transmitted form.</message>",
+                response.getContentAsString());
+    }
+
+    @Test
+    public void testUploadWithTooFewSessionKeyPrefixes() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "2");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        ContentResponse response = request.send();
+
+        assertEquals("<message type=\"error\">No field '" + PARAM_SESSION_KEY_PREFIX + "1' could be found in the transmitted form.</message>",
+                response.getContentAsString());
+    }
+
+    @Test
+    public void testUploadWithLogout() throws Exception
+    {
+        String sessionToken = as.login(USER, PASSWORD);
+
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("testFieldName", "testFileName", new StringContentProvider("testContent"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "testFieldName");
+        request.content(multipart);
+
+        request.send();
+
+        assertSessionWorkspaceFiles(sessionToken, "testContent");
+
+        as.logout(sessionToken);
+
+        assertSessionWorkspaceFiles(sessionToken);
+    }
+
+    private File getSessionWorkspace(String sessionToken) throws Exception
+    {
+        File sessionWorkspaceRootDir = new File("targets/sessionWorkspace");
+        return new File(sessionWorkspaceRootDir, sessionToken);
+    }
+
+    private void cleanSessionWorkspace(String sessionToken) throws Exception
+    {
+        File sessionWorkspace = getSessionWorkspace(sessionToken);
+
+        if (sessionWorkspace.exists())
+        {
+            FileUtils.deleteQuietly(sessionWorkspace);
+        }
+
+        assertSessionWorkspaceFiles(sessionToken);
+    }
+
+    private void assertSessionWorkspaceFiles(String sessionToken, String... fileContents) throws Exception
+    {
+        assertFiles(getSessionWorkspace(sessionToken).listFiles(), fileContents);
+    }
+
+    private void initHttpSession(String sessionToken) throws Exception
+    {
+        // upload a dummy file to initialize HTTP session
+        MultiPartContentProvider multipart = new MultiPartContentProvider();
+        multipart.addFilePart("initHttpSession", "initHttpSession", new StringContentProvider("initHttpSession"), null);
+        multipart.close();
+
+        HttpClient client = JettyHttpClientFactory.getHttpClient();
+        Request request = client.newRequest(SERVICE_URL).method(HttpMethod.POST);
+        request.param(PARAM_SESSION_ID, sessionToken);
+        request.param(PARAM_SESSION_KEYS_NUMBER, "1");
+        request.param(PARAM_SESSION_KEY_PREFIX + "0", "initHttpSession");
+        request.content(multipart);
+
+        request.send();
+    }
+
+    private File getOSTempFolder() throws Exception
+    {
+        return new File(System.getProperty("java.io.tmpdir"));
+    }
+
+    private void cleanOSTempFolder() throws Exception
+    {
+        File tempFolder = getOSTempFolder();
+
+        if (tempFolder.exists())
+        {
+            File[] files = tempFolder.listFiles(new OSTempFolderFileFilter());
+
+            for (File file : files)
+            {
+                FileUtils.deleteQuietly(file);
+            }
+        }
+
+        assertOSTempFolderFiles();
+    }
+
+    private void assertOSTempFolderFiles(String... fileContents) throws Exception
+    {
+        assertFiles(getOSTempFolder().listFiles(new OSTempFolderFileFilter()), fileContents);
+    }
+
+    private void assertFiles(File[] files, String... fileContents) throws Exception
+    {
+        List<String> expectedContents = new ArrayList<String>(Arrays.asList(fileContents));
+        List<String> actualContents = new ArrayList<String>();
+
+        if (files != null)
+        {
+            for (File file : files)
+            {
+                actualContents.add(FileUtils.readFileToString(file));
+            }
+        }
+
+        expectedContents.sort(String.CASE_INSENSITIVE_ORDER);
+        actualContents.sort(String.CASE_INSENSITIVE_ORDER);
+
+        assertEquals(expectedContents, actualContents);
+    }
+
+    private static class OSTempFolderFileFilter implements FileFilter
+    {
+
+        @Override
+        public boolean accept(File file)
+        {
+            return file.getName().startsWith(UploadedFilesBean.class.getSimpleName());
+        }
+
+    }
+
+    @DataProvider(name = FALSE_TRUE_PROVIDER)
+    public static Object[][] provideFalseTrue()
+    {
+        return new Object[][] { { false }, { true } };
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServlet.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServlet.java
index 2653065254d..660b9952ab3 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServlet.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServlet.java
@@ -29,6 +29,7 @@ import javax.servlet.http.HttpSession;
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.HttpSessionRequiredException;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -43,6 +44,7 @@ import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.openbis.generic.server.ComponentNames;
+import ch.systemsx.cisd.openbis.generic.server.ISessionWorkspaceProvider;
 import ch.systemsx.cisd.openbis.generic.server.SessionConstants;
 import ch.systemsx.cisd.openbis.generic.shared.IOpenBisSessionManager;
 import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
@@ -65,8 +67,7 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
  * @author Christian Ribeaud
  */
 @Controller
-@RequestMapping(
-{ "/upload", "/openbis/upload" })
+@RequestMapping({ "/upload", "/openbis/upload" })
 public final class UploadServiceServlet extends AbstractController
 {
     private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
@@ -77,6 +78,9 @@ public final class UploadServiceServlet extends AbstractController
     @Resource(name = ComponentNames.SESSION_MANAGER)
     protected IOpenBisSessionManager sessionManager;
 
+    @Autowired
+    private ISessionWorkspaceProvider sessionWorkspaceProvider;
+
     @Private
     UploadServiceServlet(ISessionFilesSetter sessionFilesSetter)
     {
@@ -91,8 +95,7 @@ public final class UploadServiceServlet extends AbstractController
         this(new SessionFilesSetter());
     }
 
-    @SuppressWarnings(
-    { "unchecked", "rawtypes" })
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     private final static Iterator<String> cast(final Iterator iterator)
     {
         return iterator;
@@ -208,7 +211,7 @@ public final class UploadServiceServlet extends AbstractController
             {
                 // Note: addFilesToSession has a side effect - adds extracted files to the session
                 boolean fileExtracted =
-                        sessionFilesSetter.addFilesToSession(session, multipartRequest, sessionKey);
+                        sessionFilesSetter.addFilesToSession(sessionToken, session, multipartRequest, sessionKey, sessionWorkspaceProvider);
                 atLeastOneFileUploaded = atLeastOneFileUploaded || fileExtracted;
             }
             if (atLeastOneFileUploaded == false)
@@ -246,25 +249,25 @@ public final class UploadServiceServlet extends AbstractController
          * 
          * @return <code>true</code> if at least one file has been found and added
          */
-        public boolean addFilesToSession(final HttpSession session,
-                final MultipartHttpServletRequest multipartRequest, String sessionKey);
+        public boolean addFilesToSession(String sessionToken, final HttpSession session,
+                final MultipartHttpServletRequest multipartRequest, String sessionKey, ISessionWorkspaceProvider sessionWorkspaceProvider);
     }
 
     @Private
     static class SessionFilesSetter implements ISessionFilesSetter
     {
         @Override
-        public boolean addFilesToSession(final HttpSession session,
-                final MultipartHttpServletRequest multipartRequest, String sessionKey)
+        public boolean addFilesToSession(String sessionToken, final HttpSession session,
+                final MultipartHttpServletRequest multipartRequest, String sessionKey, ISessionWorkspaceProvider sessionWorkspaceProvider)
         {
-            return addFilesToSessionUsingBean(session, multipartRequest, sessionKey,
-                    new UploadedFilesBean());
+            return addFilesToSessionUsingBean(sessionToken, session, multipartRequest, sessionKey,
+                    new UploadedFilesBean(), sessionWorkspaceProvider);
         }
 
         @Private
-        boolean addFilesToSessionUsingBean(final HttpSession session,
+        boolean addFilesToSessionUsingBean(String sessionToken, final HttpSession session,
                 final MultipartHttpServletRequest multipartRequest, String sessionKey,
-                final UploadedFilesBean uploadedFiles)
+                final UploadedFilesBean uploadedFiles, ISessionWorkspaceProvider sessionWorkspaceProvider)
         {
             assert StringUtils.isBlank(sessionKey) == false;
             boolean fileUploaded = false;
@@ -277,7 +280,7 @@ public final class UploadServiceServlet extends AbstractController
                     final MultipartFile multipartFile = multipartRequest.getFile(fileName);
                     if (multipartFile.isEmpty() == false)
                     {
-                        uploadedFiles.addMultipartFile(multipartFile);
+                        uploadedFiles.addMultipartFile(sessionToken, multipartFile, sessionWorkspaceProvider);
                         fileUploaded = true;
                     }
                 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadedFilesBean.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadedFilesBean.java
index 37d717f3429..408d55ee7cb 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadedFilesBean.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadedFilesBean.java
@@ -26,8 +26,10 @@ import org.apache.commons.io.FileUtils;
 import org.springframework.web.multipart.MultipartFile;
 
 import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked;
+import ch.systemsx.cisd.common.shared.basic.string.StringUtils;
 import ch.systemsx.cisd.openbis.common.spring.IUncheckedMultipartFile;
 import ch.systemsx.cisd.openbis.common.spring.MultipartFileAdapter;
+import ch.systemsx.cisd.openbis.generic.server.ISessionWorkspaceProvider;
 
 /**
  * A bean that contains the uploaded files.
@@ -40,19 +42,31 @@ public final class UploadedFilesBean
 
     private List<IUncheckedMultipartFile> multipartFiles = new ArrayList<IUncheckedMultipartFile>();
 
-    private final File createTempFile() throws IOException
+    private final File createTempFile(String sessionToken, ISessionWorkspaceProvider sessionWorkspaceProvider) throws IOException
     {
-        final File tempFile = File.createTempFile(CLASS_SIMPLE_NAME, null);
+        File tempFolder = null;
+
+        if (false == StringUtils.isBlank(sessionToken) && sessionWorkspaceProvider != null)
+        {
+            tempFolder = sessionWorkspaceProvider.getSessionWorkspace(sessionToken);
+        }
+
+        final File tempFile = File.createTempFile(CLASS_SIMPLE_NAME, null, tempFolder);
         tempFile.deleteOnExit();
         return tempFile;
     }
 
     public final void addMultipartFile(final MultipartFile multipartFile)
+    {
+        addMultipartFile(null, multipartFile, null);
+    }
+
+    public final void addMultipartFile(String sessionToken, final MultipartFile multipartFile, ISessionWorkspaceProvider sessionWorkspaceProvider)
     {
         assert multipartFile != null : "Unspecified multipart file.";
         try
         {
-            final File tempFile = createTempFile();
+            final File tempFile = createTempFile(sessionToken, sessionWorkspaceProvider);
             multipartFile.transferTo(tempFile);
             final FileMultipartFileAdapter multipartFileAdapter =
                     new FileMultipartFileAdapter(multipartFile, tempFile);
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
index 8a889f811ab..c3fb261e0c7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
@@ -171,6 +171,9 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
     @Autowired
     private IAuthorizationConfig authorizationConfig;
 
+    @Autowired
+    private ISessionWorkspaceProvider sessionWorkspaceProvider;
+
     private IApplicationServerApi v3Api;
 
     protected String CISDHelpdeskEmail;
@@ -407,6 +410,7 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
         try
         {
             sessionManager.closeSession(sessionToken);
+            sessionWorkspaceProvider.deleteSessionWorkspace(sessionToken);
             SessionFactory.cleanUpSessionOnDataStoreServers(sessionToken,
                     daoFactory.getDataStoreDAO(), dssFactory);
         } catch (InvalidSessionException e)
@@ -422,6 +426,7 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
         try
         {
             sessionManager.expireSession(sessionToken);
+            sessionWorkspaceProvider.deleteSessionWorkspace(sessionToken);
             SessionFactory.cleanUpSessionOnDataStoreServers(sessionToken,
                     daoFactory.getDataStoreDAO(), dssFactory);
         } catch (InvalidSessionException e)
@@ -929,8 +934,9 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
         if (person != null)
         {
             SpacePE homeGroup =
-                    groupIdOrNull == null ? null : getDAOFactory().getSpaceDAO().getByTechId(
-                            groupIdOrNull);
+                    groupIdOrNull == null ? null
+                            : getDAOFactory().getSpaceDAO().getByTechId(
+                                    groupIdOrNull);
             person.setHomeSpace(homeGroup);
             getDAOFactory().getPersonDAO().updatePerson(person);
         }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ISessionWorkspaceProvider.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ISessionWorkspaceProvider.java
new file mode 100644
index 00000000000..5f127a53e37
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ISessionWorkspaceProvider.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.io.File;
+
+/**
+ * @author pkupczyk
+ */
+public interface ISessionWorkspaceProvider
+{
+
+    File getSessionWorkspace(String sessionToken);
+
+    void deleteSessionWorkspace(String sessionToken);
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionFactory.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionFactory.java
index 501d8045536..d16c72b71c5 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionFactory.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionFactory.java
@@ -45,15 +45,18 @@ public final class SessionFactory implements ISessionFactory<Session>
 
     private final IDataStoreServiceFactory dssFactory;
 
+    private final ISessionWorkspaceProvider sessionWorkspaceProvider;
+
     public SessionFactory()
     {
-        this(null, null);
+        this(null, null, null);
     }
 
-    public SessionFactory(IDAOFactory daoFactory, IDataStoreServiceFactory dssFactory)
+    public SessionFactory(IDAOFactory daoFactory, IDataStoreServiceFactory dssFactory, ISessionWorkspaceProvider sessionWorkspaceProvider)
     {
         this.datastoreDAO = (daoFactory != null) ? daoFactory.getDataStoreDAO() : null;
         this.dssFactory = dssFactory;
+        this.sessionWorkspaceProvider = sessionWorkspaceProvider;
     }
 
     //
@@ -75,6 +78,7 @@ public final class SessionFactory implements ISessionFactory<Session>
                     @Override
                     public void cleanup()
                     {
+                        sessionWorkspaceProvider.deleteSessionWorkspace(sessionToken);
                         cleanUpSessionOnDataStoreServers(sessionToken, datastoreDAO, dssFactory);
                     }
                 });
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionWorkspaceProvider.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionWorkspaceProvider.java
new file mode 100644
index 00000000000..3d447f134c0
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/SessionWorkspaceProvider.java
@@ -0,0 +1,107 @@
+/*
+ * 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.io.File;
+import java.util.Properties;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+import org.apache.log4j.Logger;
+import org.springframework.stereotype.Component;
+
+import ch.systemsx.cisd.common.filesystem.QueueingPathRemoverService;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.properties.PropertyUtils;
+import ch.systemsx.cisd.common.spring.ExposablePropertyPlaceholderConfigurer;
+
+/**
+ * @author pkupczyk
+ */
+@Component(value = "session-workspace-provider")
+public class SessionWorkspaceProvider implements ISessionWorkspaceProvider
+{
+
+    private static final String SESSION_WORKSPACE_ROOT_DIR_KEY = "session-workspace-root-dir";
+
+    private static final String SESSION_WORKSPACE_ROOT_DIR_DEFAULT = "sessionWorkspace";
+
+    private static final String SESSION_WORKSPACE_SHREDDER_QUEUE_FILE = ".shredder";
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, SessionWorkspaceProvider.class);
+
+    @Resource(name = ExposablePropertyPlaceholderConfigurer.PROPERTY_CONFIGURER_BEAN_NAME)
+    private ExposablePropertyPlaceholderConfigurer servicePropertiesPlaceholder;
+
+    private File sessionWorkspaceRootDir;
+
+    @PostConstruct
+    private void init() throws Exception
+    {
+        Properties serviceProperties = servicePropertiesPlaceholder.getResolvedProps();
+
+        String sessionWorkspaceRootDirString =
+                PropertyUtils.getProperty(serviceProperties, SESSION_WORKSPACE_ROOT_DIR_KEY, SESSION_WORKSPACE_ROOT_DIR_DEFAULT);
+        sessionWorkspaceRootDir = new File(sessionWorkspaceRootDirString);
+
+        operationLog.info("Session workspace root dir '" + sessionWorkspaceRootDir.getCanonicalPath() + "'");
+
+        if (false == sessionWorkspaceRootDir.exists())
+        {
+            sessionWorkspaceRootDir.mkdirs();
+        }
+
+        QueueingPathRemoverService.start(sessionWorkspaceRootDir, new File(SESSION_WORKSPACE_SHREDDER_QUEUE_FILE));
+
+        operationLog.info("Session workspace shredder service started");
+    }
+
+    @Override
+    public File getSessionWorkspace(String sessionToken)
+    {
+        File sessionWorkspace = new File(sessionWorkspaceRootDir, sessionToken);
+
+        if (false == sessionWorkspace.exists())
+        {
+            sessionWorkspace.mkdirs();
+            operationLog.info("Session workspace '" + sessionToken + "' created");
+        }
+
+        return sessionWorkspace;
+    }
+
+    @Override
+    public void deleteSessionWorkspace(String sessionToken)
+    {
+        try
+        {
+            File sessionWorkspace = new File(sessionWorkspaceRootDir, sessionToken);
+
+            if (sessionWorkspace.exists())
+            {
+                QueueingPathRemoverService.removeRecursively(sessionWorkspace);
+                operationLog.info("Session workspace '" + sessionToken + "' added to shredder queue");
+            }
+        } catch (Exception e)
+        {
+            operationLog.warn("Session workspace '" + sessionToken + "' could not be shredded", e);
+        }
+    }
+
+}
diff --git a/openbis/source/java/genericApplicationContext.xml b/openbis/source/java/genericApplicationContext.xml
index 362ea3aeb07..331b1366310 100644
--- a/openbis/source/java/genericApplicationContext.xml
+++ b/openbis/source/java/genericApplicationContext.xml
@@ -130,6 +130,7 @@
 			<bean class="ch.systemsx.cisd.openbis.generic.server.SessionFactory">
 				<constructor-arg ref="dao-factory" />
 				<constructor-arg ref="dss-factory" />
+				<constructor-arg ref="session-workspace-provider" />
 			</bean>
 		</constructor-arg>
 		<constructor-arg>
diff --git a/openbis/source/java/service.properties b/openbis/source/java/service.properties
index 568792007cb..3aa53d02629 100644
--- a/openbis/source/java/service.properties
+++ b/openbis/source/java/service.properties
@@ -20,6 +20,8 @@ create-continuous-sample-codes = false
 
 data-set-types-with-no-experiment-needed = (?!REQUIRES\\_EXPERIMENT).*
 
+session-workspace-root-dir = targets/sessionWorkspace
+
 # Supported: currently only 'postgresql' is supported
 database.engine = postgresql
 database.create-from-scratch = false
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServletTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServletTest.java
index 24d96d6c23b..18884125e7b 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServletTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/client/web/server/UploadServiceServletTest.java
@@ -39,8 +39,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.UploadServiceServlet.S
  * 
  * @author Izabela Adamczyk
  */
-@Friend(toClasses =
-{ UploadServiceServlet.class, ISessionFilesSetter.class, SessionFilesSetter.class })
+@Friend(toClasses = { UploadServiceServlet.class, ISessionFilesSetter.class, SessionFilesSetter.class })
 public final class UploadServiceServletTest extends AssertJUnit
 {
 
@@ -231,8 +230,8 @@ public final class UploadServiceServletTest extends AssertJUnit
                         one(multipartHttpServletRequest).getParameter(SESSION_KEY_PREFIX + i);
                         will(returnValue(sessionKey));
 
-                        one(sessionFilesSetter).addFilesToSession(httpSession,
-                                multipartHttpServletRequest, sessionKey);
+                        one(sessionFilesSetter).addFilesToSession("", httpSession,
+                                multipartHttpServletRequest, sessionKey, null);
                         will(returnValue(false));
                     }
 
@@ -269,8 +268,8 @@ public final class UploadServiceServletTest extends AssertJUnit
                         one(multipartHttpServletRequest).getParameter(SESSION_KEY_PREFIX + i);
                         will(returnValue(sessionKey));
 
-                        one(sessionFilesSetter).addFilesToSession(httpSession,
-                                multipartHttpServletRequest, sessionKey);
+                        one(sessionFilesSetter).addFilesToSession("", httpSession,
+                                multipartHttpServletRequest, sessionKey, null);
                         will(returnValue(true));
                     }
                     expectSendResponse(this);
@@ -299,8 +298,8 @@ public final class UploadServiceServletTest extends AssertJUnit
                         one(multipartHttpServletRequest).getParameter(SESSION_KEY_PREFIX + i);
                         will(returnValue(sessionKey));
 
-                        one(sessionFilesSetter).addFilesToSession(httpSession,
-                                multipartHttpServletRequest, sessionKey);
+                        one(sessionFilesSetter).addFilesToSession("", httpSession,
+                                multipartHttpServletRequest, sessionKey, null);
                         will(returnValue(i != numberOfSessionKeys - 1));
                     }
                     expectSendResponse(this);
-- 
GitLab