diff --git a/datastore_server/.classpath b/datastore_server/.classpath
index 105dc732c45ae51449a0f9690e05c8fd70ce1f39..1247b650f9c67ddc8cdd4a914e9c3e848dd26684 100644
--- a/datastore_server/.classpath
+++ b/datastore_server/.classpath
@@ -19,5 +19,8 @@
 	<classpathentry kind="lib" path="/libraries/jmock/objenesis/objenesis-1.0.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/openbis"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/server-common"/>
+	<classpathentry kind="lib" path="/libraries/jetty/jetty.jar" sourcepath="/libraries/jetty/src/jetty.zip"/>
+	<classpathentry kind="lib" path="/libraries/jetty/jetty-util.jar" sourcepath="/libraries/jetty/src/jetty-util.zip"/>
+	<classpathentry kind="lib" path="/libraries/jetty/servlet-api-2.5.jar" sourcepath="/libraries/jetty/src/servlet-api-2.5.zip"/>
 	<classpathentry kind="output" path="targets/classes"/>
 </classpath>
diff --git a/datastore_server/resource/eclipse/Dataset Download Service.launch b/datastore_server/resource/eclipse/Dataset Download Service.launch
new file mode 100644
index 0000000000000000000000000000000000000000..2cb0da27a61bec52db6b179a06cfa37e0ff82973
--- /dev/null
+++ b/datastore_server/resource/eclipse/Dataset Download Service.launch	
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadService.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="ch.systemsx.cisd.openbis.dss.generic.server.DatasetDownloadService"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="datastore_server"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea"/>
+</launchConfiguration>
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ApplicationContext.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ApplicationContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e5f7ea1e4cf972e8b8cb9614c6620893d571082
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ApplicationContext.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+
+/**
+ *  Application context. It contains the object accessing the openBIS for retrieving the data set,
+ *  configuration parameters, and the name of the application which will be a part of its URL. 
+ *
+ * @author Franz-Josef Elmer
+ */
+class ApplicationContext
+{
+    private final IETLLIMSService dataSetService;
+    
+    private final ConfigParameters configParameters;
+
+    private final String applicationName;
+    
+    ApplicationContext(IETLLIMSService service, ConfigParameters configParameters,
+            String applicationName)
+    {
+        this.dataSetService = service;
+        this.configParameters = configParameters;
+        this.applicationName = applicationName;
+    }
+
+    public final IETLLIMSService getDataSetService()
+    {
+        return dataSetService;
+    }
+
+    public final ConfigParameters getConfigParameters()
+    {
+        return configParameters;
+    }
+
+    public final String getApplicationName()
+    {
+        return applicationName;
+    }
+    
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ConfigParameters.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ConfigParameters.java
new file mode 100644
index 0000000000000000000000000000000000000000..362d70a70eb1aa35eb47fe6fa2b8563cf58309f8
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ConfigParameters.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.util.Properties;
+
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.utilities.PropertyUtils;
+
+/**
+ * Configuration parameters for the Data Set Download Server.
+ * 
+ * @author Franz-Josef Elmer
+ */
+final class ConfigParameters
+{
+
+    private static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, ConfigParameters.class);
+
+    static final String SERVER_URL_KEY = "server-url";
+
+    static final String PORT_KEY = "port";
+
+    static final String STOREROOT_DIR_KEY = "storeroot-dir";
+
+    static final String SESSION_TIMEOUT_KEY = "session-timeout";
+
+    private static final String KEYSTORE = "keystore.";
+
+    static final String KEYSTORE_PATH_KEY = KEYSTORE + "path";
+
+    static final String KEYSTORE_PASSWORD_KEY = KEYSTORE + "password";
+
+    static final String KEYSTORE_KEY_PASSWORD_KEY = KEYSTORE + "key-password";
+
+    private final String storePath;
+
+    private final int port;
+
+    private final String serverURL;
+
+    private final int sessionTimeout;
+
+    private final String keystorePath;
+
+    private final String keystorePassword;
+
+    private final String keystoreKeyPassword;
+
+    /**
+     * Creates an instance based on the specified properties.
+     * 
+     * @throws ConfigurationFailureException if a property is missed or has an invalid value.
+     */
+    public ConfigParameters(final Properties properties)
+    {
+        storePath = PropertyUtils.getMandatoryProperty(properties, STOREROOT_DIR_KEY);
+        port = getMandatoryIntegerProperty(properties, PORT_KEY);
+        serverURL = PropertyUtils.getMandatoryProperty(properties, SERVER_URL_KEY);
+        sessionTimeout = getMandatoryIntegerProperty(properties, SESSION_TIMEOUT_KEY) * 60;
+        keystorePath = PropertyUtils.getMandatoryProperty(properties, KEYSTORE_PATH_KEY);
+        keystorePassword = PropertyUtils.getMandatoryProperty(properties, KEYSTORE_PASSWORD_KEY);
+        keystoreKeyPassword =
+                PropertyUtils.getMandatoryProperty(properties, KEYSTORE_KEY_PASSWORD_KEY);
+    }
+
+    private final static int getMandatoryIntegerProperty(final Properties properties,
+            final String key)
+    {
+        final String property = PropertyUtils.getMandatoryProperty(properties, key);
+        try
+        {
+            return Integer.parseInt(property);
+        } catch (final NumberFormatException ex)
+        {
+            throw new ConfigurationFailureException("Configuration parameter '" + key
+                    + "' is not an integer number: " + property);
+        }
+    }
+
+    public final String getStorePath()
+    {
+        return storePath;
+    }
+
+    public final int getPort()
+    {
+        return port;
+    }
+
+    public final String getServerURL()
+    {
+        return serverURL;
+    }
+
+    public final int getSessionTimeout()
+    {
+        return sessionTimeout;
+    }
+
+    public final String getKeystorePath()
+    {
+        return keystorePath;
+    }
+
+    public final String getKeystorePassword()
+    {
+        return keystorePassword;
+    }
+
+    public final String getKeystoreKeyPassword()
+    {
+        return keystoreKeyPassword;
+    }
+
+    public final void log()
+    {
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info(String.format("Store root directory: '%s'.", storePath));
+            operationLog.info(String.format("Port number: %d.", port));
+            operationLog.info(String.format("URL of openBIS server: '%s'.", serverURL));
+            operationLog.info(String.format("Session timeout (minutes): %d.", sessionTimeout));
+            operationLog.info(String.format("Keystore path: '%s'.", keystorePath));
+        }
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DataSetService.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DataSetService.java
new file mode 100644
index 0000000000000000000000000000000000000000..37ff1eb6b025c119967468e72705141b739108e2
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DataSetService.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.dto.DataStorePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.DatabaseInstancePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalDataPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.IAuthSession;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
+
+/**
+ * A <code>IDataSetService</code> implementation.
+ * 
+ * @author Franz-Josef Elmer
+ */
+final class DataSetService implements IETLLIMSService
+{
+    private IETLLIMSService service;
+
+    DataSetService(final ConfigParameters configParameters)
+    {
+        String url = configParameters.getServerURL() + "/rmi-etl";
+        service = HttpInvokerUtils.createServiceStub(IETLLIMSService.class, url, 5);
+    }
+
+    //
+    // IETLLIMSService
+    //
+
+    public final ExternalDataPE tryGetDataSet(final String sessionToken, final String dataSetCode)
+            throws UserFailureException
+    {
+        return service.tryGetDataSet(sessionToken, dataSetCode);
+    }
+
+    public final String authenticate(final String user, final String password)
+            throws UserFailureException
+    {
+        return service.authenticate(user, password);
+    }
+
+    public final void closeSession(final String sessionToken) throws UserFailureException
+    {
+        service.closeSession(sessionToken);
+    }
+
+    public final int getVersion()
+    {
+        return service.getVersion();
+    }
+
+    public String createDataSetCode(String sessionToken) throws UserFailureException
+    {
+        return null;
+    }
+
+    public DatabaseInstancePE getHomeDatabaseInstance(String sessionToken)
+    {
+        return null;
+    }
+
+    public void registerDataSet(String sessionToken, SampleIdentifier sampleIdentifier,
+            String procedureTypeCode, ExternalData externalData) throws UserFailureException
+    {
+    }
+
+    public ExperimentPE tryToGetBaseExperiment(String sessionToken,
+            SampleIdentifier sampleIdentifier) throws UserFailureException
+    {
+        return null;
+    }
+
+    public SamplePropertyPE[] tryToGetPropertiesOfTopSampleRegisteredFor(String sessionToken,
+            SampleIdentifier sampleIdentifier) throws UserFailureException
+    {
+        return null;
+    }
+
+    public DataStorePE getDataStore(String sessionToken, ExperimentIdentifier experimentIdentifier,
+            String dataSetTypeCode) throws UserFailureException
+    {
+        return null;
+    }
+
+    public IAuthSession getSession(String sessionToken) throws UserFailureException
+    {
+        return null;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadService.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadService.java
new file mode 100644
index 0000000000000000000000000000000000000000..99dd5f59f5974b113e767708116a0a97f744da33
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadService.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.Properties;
+
+import org.apache.log4j.Logger;
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.security.SslSocketConnector;
+import org.mortbay.jetty.servlet.Context;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.logging.LogInitializer;
+import ch.systemsx.cisd.common.utilities.PropertyUtils;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.IWebService;
+
+/**
+ * Main class of the service. Starts up jetty with {@link DatasetDownloadServlet}.
+ * 
+ * @author Franz-Josef Elmer
+ */
+public class DatasetDownloadService
+{
+    static final String APPLICATION_CONTEXT_KEY = "application-context";
+
+    private static final String PREFIX = "data-set-download.";
+
+    private static final int PREFIX_LENGTH = PREFIX.length();
+
+    private static final String SERVICE_PROPERTIES_FILE = "etc/service.properties";
+
+    private static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, DatasetDownloadService.class);
+
+    private static Server server;
+
+    public static final void start()
+    {
+        assert server == null : "Server already started";
+        final ApplicationContext applicationContext = createApplicationContext();
+        server = createServer(applicationContext);
+        try
+        {
+            server.start();
+            selfTest(applicationContext);
+            if (operationLog.isInfoEnabled())
+            {
+                operationLog.info("Data set download server ready on port "
+                        + applicationContext.getConfigParameters().getPort());
+            }
+        } catch (final Exception ex)
+        {
+            operationLog.error("Failed to start server.", ex);
+        }
+    }
+
+    public static final void stop()
+    {
+        assert server != null : "Server has not been started.";
+        if (server.isRunning())
+        {
+            try
+            {
+                server.stop();
+            } catch (final Exception ex)
+            {
+                operationLog.error("Failed to stop server.", ex);
+            }
+        }
+        server = null;
+    }
+
+    public static void main(final String[] args)
+    {
+        LogInitializer.init();
+        start();
+    }
+
+    private final static Server createServer(final ApplicationContext applicationContext)
+    {
+        final ConfigParameters configParameters = applicationContext.getConfigParameters();
+        final int port = configParameters.getPort();
+        final Server thisServer = new Server();
+        final SslSocketConnector socketConnector = new SslSocketConnector();
+        socketConnector.setPort(port);
+        socketConnector.setMaxIdleTime(30000);
+        socketConnector.setKeystore(configParameters.getKeystorePath());
+        socketConnector.setPassword(configParameters.getKeystorePassword());
+        socketConnector.setKeyPassword(configParameters.getKeystoreKeyPassword());
+        thisServer.addConnector(socketConnector);
+        final Context context = new Context(thisServer, "/", Context.SESSIONS);
+        context.setAttribute(APPLICATION_CONTEXT_KEY, applicationContext);
+        context.addServlet(DatasetDownloadServlet.class, "/"
+                + applicationContext.getApplicationName() + "/*");
+        return thisServer;
+    }
+
+    private final static void selfTest(final ApplicationContext applicationContext)
+    {
+        final int version = applicationContext.getDataSetService().getVersion();
+        if (IWebService.VERSION != version)
+        {
+            throw new ConfigurationFailureException(
+                    "This client has the wrong service version for the server (client: "
+                            + IWebService.VERSION + ", server: " + version + ").");
+        }
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info("openBIS service (interface version " + version + ") is reachable");
+        }
+    }
+
+    private final static ApplicationContext createApplicationContext()
+    {
+        final ConfigParameters configParameters = getConfigParameters();
+        final IETLLIMSService dataSetService = new DataSetService(configParameters);
+        final ApplicationContext applicationContext =
+                new ApplicationContext(dataSetService, configParameters, "dataset-download");
+        return applicationContext;
+    }
+
+    private final static ConfigParameters getConfigParameters()
+    {
+        final Properties properties;
+        if (new File(SERVICE_PROPERTIES_FILE).exists() == false)
+        {
+            properties = new Properties();
+        } else
+        {
+            properties = PropertyUtils.loadProperties(SERVICE_PROPERTIES_FILE);
+        }
+        final Properties systemProperties = System.getProperties();
+        final Enumeration<?> propertyNames = systemProperties.propertyNames();
+        while (propertyNames.hasMoreElements())
+        {
+            final String name = (String) propertyNames.nextElement();
+            if (name.startsWith(PREFIX))
+            {
+                final String value = systemProperties.getProperty(name);
+                properties.setProperty(name.substring(PREFIX_LENGTH), value);
+            }
+        }
+        final ConfigParameters configParameters = new ConfigParameters(properties);
+        configParameters.log();
+        return configParameters;
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..c864640e07c10d72e371c58d7e3b0835cf305e7d
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServlet.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import static ch.systemsx.cisd.openbis.dss.generic.server.DatasetDownloadService.APPLICATION_CONTEXT_KEY;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalDataPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType;
+import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorTypePE;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class DatasetDownloadServlet extends HttpServlet
+{
+    private static final String TEXT_MODE_DISPLAY = "txt";
+
+    static final String DATA_SET_KEY = "data-set";
+
+    static final String DATASET_CODE_KEY = "dataSetCode";
+
+    static final String SESSION_ID_KEY = "sessionID";
+
+    static final String DISPLAY_MODE_KEY = "mode";
+
+    static final String BINARY_CONTENT_TYPE = "binary";
+
+    private static final long serialVersionUID = 1L;
+
+    protected static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, DatasetDownloadServlet.class);
+
+    protected static final Logger notificationLog =
+            LogFactory.getLogger(LogCategory.NOTIFY, DatasetDownloadServlet.class);
+
+    private static final Comparator<File> FILE_COMPARATOR = new Comparator<File>()
+        {
+            public int compare(File file1, File file2)
+            {
+                return createSortableName(file1).compareTo(createSortableName(file2));
+            }
+
+            private String createSortableName(File file)
+            {
+                return (file.isDirectory() ? "D" : "F") + file.getName().toUpperCase();
+            }
+        };
+
+    private ApplicationContext applicationContext;
+
+    public DatasetDownloadServlet()
+    {
+    }
+
+    DatasetDownloadServlet(ApplicationContext applicationContext)
+    {
+        this.applicationContext = applicationContext;
+    }
+
+    @Override
+    public final void init(final ServletConfig servletConfig) throws ServletException
+    {
+        super.init(servletConfig);
+        try
+        {
+            ServletContext context = servletConfig.getServletContext();
+            applicationContext = (ApplicationContext) context.getAttribute(APPLICATION_CONTEXT_KEY);
+        } catch (Exception ex)
+        {
+            notificationLog.fatal("Failure during '" + servletConfig.getServletName()
+                    + "' servlet initialization.", ex);
+            throw new ServletException(ex);
+        }
+    }
+
+    // helper class to store parsed URL request
+    private static class RequestParams
+    {
+        private final String dataSetCode;
+
+        private final String pathInfo;
+
+        private final String sessionIdOrNull;
+
+        private final boolean isPlainTextMode;
+
+        private final String urlPrefixWithDataset;
+
+        public RequestParams(String dataSetCode, String pathInfo, String sessionIdOrNull,
+                String urlPrefixWithDataset, boolean isPlainTextMode)
+        {
+            this.dataSetCode = dataSetCode;
+            this.pathInfo = pathInfo;
+            this.sessionIdOrNull = sessionIdOrNull;
+            this.urlPrefixWithDataset = urlPrefixWithDataset;
+            this.isPlainTextMode = isPlainTextMode;
+        }
+
+        public String getDataSetCode()
+        {
+            return dataSetCode;
+        }
+
+        public String getPathInfo()
+        {
+            return pathInfo;
+        }
+
+        public String tryGetSessionId()
+        {
+            return sessionIdOrNull;
+        }
+
+        public boolean isPlainTextMode()
+        {
+            return isPlainTextMode;
+        }
+
+        public String getURLPrefix()
+        {
+            return urlPrefixWithDataset;
+        }
+    }
+
+    @Override
+    protected final void doGet(final HttpServletRequest request, final HttpServletResponse response)
+            throws ServletException, IOException
+    {
+        IRendererFactory rendererFactory = null;
+        try
+        {
+            RequestParams requestParams =
+                    parseRequestURL(request, applicationContext.getApplicationName());
+            rendererFactory = createRendererFactory(requestParams.isPlainTextMode());
+
+            obtainDataSetFromServer(requestParams.getDataSetCode(),
+                    requestParams.tryGetSessionId(), request);
+
+            HttpSession session = request.getSession(false);
+            if (session == null)
+            {
+                printSessionExpired(response);
+            } else
+            {
+                printResponse(response, rendererFactory, requestParams, session);
+            }
+        } catch (Exception e)
+        {
+            if (rendererFactory == null)
+            {
+                rendererFactory = new PlainTextRendererFactory();
+            }
+            printError(rendererFactory, request, response, e);
+        }
+    }
+
+    private void printResponse(final HttpServletResponse response,
+            IRendererFactory rendererFactory, RequestParams requestParams, HttpSession session)
+            throws UnsupportedEncodingException, IOException
+    {
+        String dataSetCode = requestParams.getDataSetCode();
+        ExternalDataPE dataSet = tryToGetDataSet(session, dataSetCode);
+        if (dataSet == null)
+        {
+            throw new UserFailureException("Unknown data set '" + dataSetCode + "'.");
+        }
+        File rootDir = createDataSetRootDirectory(dataSet);
+        RenderingContext context =
+                new RenderingContext(rootDir, requestParams.getURLPrefix(), requestParams
+                        .getPathInfo());
+        renderPage(rendererFactory, response, dataSet, context);
+    }
+
+    private IRendererFactory createRendererFactory(boolean plainTextMode)
+    {
+        if (plainTextMode)
+        {
+            return new PlainTextRendererFactory();
+        } else
+        {
+            return new HTMLRendererFactory();
+        }
+    }
+
+    private static RequestParams parseRequestURL(HttpServletRequest request, String applicationName)
+            throws UnsupportedEncodingException
+    {
+        final String urlPrefix = "/" + applicationName + "/";
+        final String requestURI = URLDecoder.decode(request.getRequestURI(), "UTF-8");
+        if (requestURI.startsWith(urlPrefix) == false)
+        {
+            throw new EnvironmentFailureException("Request URI '" + requestURI
+                    + "' expected to start with '" + urlPrefix + "'.");
+        }
+        final String fullPathInfo = requestURI.substring(urlPrefix.length());
+        final int indexOfFirstSeparator = fullPathInfo.indexOf('/');
+        final String dataSetCode;
+        final String pathInfo;
+        if (indexOfFirstSeparator < 0)
+        {
+            dataSetCode = fullPathInfo;
+            pathInfo = "";
+        } else
+        {
+            dataSetCode = fullPathInfo.substring(0, indexOfFirstSeparator);
+            pathInfo = fullPathInfo.substring(indexOfFirstSeparator + 1);
+        }
+        final String urlPrefixWithDataset =
+                requestURI.substring(0, requestURI.length() - pathInfo.length());
+
+        final String sessionIDOrNull = request.getParameter(SESSION_ID_KEY);
+        final String displayMode = request.getParameter(DISPLAY_MODE_KEY);
+        final boolean isTextMode = (displayMode != null && displayMode.equals(TEXT_MODE_DISPLAY));
+
+        return new RequestParams(dataSetCode, pathInfo, sessionIDOrNull, urlPrefixWithDataset,
+                isTextMode);
+    }
+
+    private void printError(IRendererFactory rendererFactory, final HttpServletRequest request,
+            final HttpServletResponse response, Exception exception) throws IOException
+    {
+        if (exception instanceof UserFailureException == false)
+        {
+            StringBuffer url = request.getRequestURL();
+            String queryString = request.getQueryString();
+            if (StringUtils.isNotBlank(queryString))
+            {
+                url.append("?").append(queryString);
+            }
+            operationLog.error("Request " + url + " caused an exception: ", exception);
+        } else if (operationLog.isInfoEnabled())
+        {
+            operationLog.info("User failure: " + exception.getMessage());
+        }
+        String message = exception.getMessage();
+        String errorText = StringUtils.isBlank(message) ? exception.toString() : message;
+        IErrorRenderer errorRenderer = rendererFactory.createErrorRenderer();
+        response.setContentType(rendererFactory.getContentType());
+        PrintWriter writer = response.getWriter();
+        errorRenderer.setWriter(writer);
+        errorRenderer.printErrorMessage(errorText);
+        writer.flush();
+        writer.close();
+    }
+
+    private void renderPage(IRendererFactory rendererFactory, HttpServletResponse response,
+            ExternalDataPE dataSet, RenderingContext renderingContext) throws IOException
+    {
+        File file = renderingContext.getFile();
+        if (file.exists() == false)
+        {
+            throw new EnvironmentFailureException("File '" + file.getName() + "' does not exist.");
+        }
+        if (file.isDirectory())
+        {
+            createPage(rendererFactory, response, dataSet, renderingContext, file);
+        } else
+        {
+            deliverFile(response, dataSet, file);
+        }
+    }
+
+    private void createPage(IRendererFactory rendererFactory, HttpServletResponse response,
+            ExternalDataPE dataSet, RenderingContext renderingContext, File file)
+            throws IOException
+    {
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info("For data set '" + dataSet.getCode() + "' show directory "
+                    + file.getAbsolutePath());
+        }
+        IDirectoryRenderer directoryRenderer =
+                rendererFactory.createDirectoryRenderer(renderingContext);
+        response.setContentType(rendererFactory.getContentType());
+        PrintWriter writer = null;
+        try
+        {
+            writer = response.getWriter();
+            directoryRenderer.setWriter(writer);
+            directoryRenderer.printHeader(dataSet);
+            String relativeParentPath = renderingContext.getRelativeParentPath();
+            if (relativeParentPath != null)
+            {
+                directoryRenderer.printLinkToParentDirectory(relativeParentPath);
+            }
+            File[] children = file.listFiles();
+            Arrays.sort(children, FILE_COMPARATOR);
+            for (File child : children)
+            {
+                String name = child.getName();
+                File rootDir = renderingContext.getRootDir();
+                String relativePath = FileUtilities.getRelativeFile(rootDir, child);
+                String normalizedRelativePath = relativePath.replace('\\', '/');
+                if (child.isDirectory())
+                {
+                    directoryRenderer.printDirectory(name, normalizedRelativePath);
+                } else
+                {
+                    directoryRenderer.printFile(name, normalizedRelativePath, child.length());
+                }
+            }
+            directoryRenderer.printFooter();
+            writer.flush();
+
+        } finally
+        {
+            IOUtils.closeQuietly(writer);
+        }
+    }
+
+    private void deliverFile(final HttpServletResponse response, ExternalDataPE dataSet, File file)
+            throws IOException, FileNotFoundException
+    {
+        long size = file.length();
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info("For data set '" + dataSet.getCode() + "' deliver file "
+                    + file.getAbsolutePath() + " (" + size + " bytes).");
+        }
+        response.setContentLength((int) size);
+        response.setHeader("Content-Disposition", "inline; filename=" + file.getName());
+        ServletOutputStream outputStream = null;
+        FileInputStream fileInputStream = null;
+        response.setContentType(BINARY_CONTENT_TYPE);
+        try
+        {
+            outputStream = response.getOutputStream();
+            fileInputStream = new FileInputStream(file);
+            IOUtils.copy(fileInputStream, outputStream);
+        } finally
+        {
+            IOUtils.closeQuietly(fileInputStream);
+            IOUtils.closeQuietly(outputStream);
+        }
+    }
+
+    private void printSessionExpired(final HttpServletResponse response) throws IOException
+    {
+        PrintWriter writer = response.getWriter();
+        writer.write("<html><body>Download session expired.</body></html>");
+        writer.flush();
+        writer.close();
+    }
+
+    private void obtainDataSetFromServer(String dataSetCode, String sessionIdOrNull,
+            final HttpServletRequest request)
+    {
+        if (sessionIdOrNull != null)
+        {
+            IETLLIMSService dataSetService = applicationContext.getDataSetService();
+            ExternalDataPE dataSet = dataSetService.tryGetDataSet(sessionIdOrNull, dataSetCode);
+            if (operationLog.isInfoEnabled())
+            {
+                String actionDesc = (dataSet != null) ? "obtained from" : "not found in";
+                operationLog.info(String.format("Data set '%s' %s openBIS server.", dataSetCode,
+                        actionDesc));
+            }
+            HttpSession session = request.getSession(true);
+            ConfigParameters configParameters = applicationContext.getConfigParameters();
+            session.setMaxInactiveInterval(configParameters.getSessionTimeout());
+            if (dataSet != null)
+            {
+                putDataSetToMap(session, dataSetCode, dataSet);
+            }
+        }
+    }
+
+    private File createDataSetRootDirectory(ExternalDataPE dataSet)
+    {
+        String path = dataSet.getLocation();
+        LocatorTypePE locatorType = dataSet.getLocatorType();
+        if (locatorType.getCode().equals(LocatorType.DEFAULT_LOCATOR_TYPE_CODE))
+        {
+            path = applicationContext.getConfigParameters().getStorePath() + "/" + path;
+        }
+        File dataSetRootDirectory = new File(path);
+        if (dataSetRootDirectory.exists() == false)
+        {
+            throw new UserFailureException("Data set '" + dataSet.getCode()
+                    + "' not found in store at '" + dataSetRootDirectory.getAbsolutePath() + "'.");
+        }
+        return dataSetRootDirectory;
+    }
+
+    private void putDataSetToMap(HttpSession session, String dataSetCode, ExternalDataPE dataSet)
+    {
+        getDataSets(session).put(dataSetCode, dataSet);
+    }
+
+    private ExternalDataPE tryToGetDataSet(HttpSession session, String dataSetCode)
+    {
+        return getDataSets(session).get(dataSetCode);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, ExternalDataPE> getDataSets(HttpSession session)
+    {
+        Map<String, ExternalDataPE> map =
+                (Map<String, ExternalDataPE>) session.getAttribute(DATA_SET_KEY);
+        if (map == null)
+        {
+            map = new HashMap<String, ExternalDataPE>();
+            session.setAttribute(DATA_SET_KEY, map);
+        }
+        return map;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/HTMLDirectoryRenderer.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/HTMLDirectoryRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..c06f6cb672a420a01907bffb61534f2ad136886d
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/HTMLDirectoryRenderer.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+
+import ch.systemsx.cisd.common.exceptions.CheckedExceptionTunnel;
+import ch.systemsx.cisd.common.utilities.Template;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalDataPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.GroupPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProcedurePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+
+/**
+ * An <code>IDirectoryRenderer</code> implementation which renders on HTML pages.
+ * 
+ * @author Franz-Josef Elmer
+ */
+final class HTMLDirectoryRenderer implements IDirectoryRenderer
+{
+    private static final String DATASET_DESCRIPTION =
+            "${group}/${project}/${experiment}/${sample}/${dataset}";
+
+    private static final String DATASET_DOWNLOAD_SERVICE = "Data Set Download Service";
+
+    private static final String TITLE =
+            "<title> " + DATASET_DOWNLOAD_SERVICE + ": " + DATASET_DESCRIPTION + "</title>";
+
+    private static final String CSS =
+            "<style type='text/css'> "
+                    + "* { margin: 3px; }"
+                    + "html { height: 100%;  }"
+                    + "body { height: 100%; font-family: verdana, tahoma, helvetica; font-size: 11px; text-align:left; }"
+                    + "h1 { text-align: center; padding: 1em; color: #1E4E8F;}"
+                    + ".td_hd { border: 1px solid #FFFFFF; padding 3px; background-color: #DDDDDD; height: 1.5em; }"
+                    + ".div_hd { background-color: #1E4E8F; color: white; font-weight: bold; padding: 3px; }"
+                    + "table { border-collapse: collapse; padding: 1em; }"
+                    + "tr, td { font-family: verdana, tahoma, helvetica; font-size: 11px; }"
+                    + ".td_file { font-family: verdana, tahoma, helvetica; font-size: 11px; height: 1.5em }"
+                    + ".wrapper { min-height: 100%; height: auto !important; height: 100%; margin: 0em auto -4em; }"
+                    + ".footer { height: 4em; text-align: center; }" + "</style>";
+
+    private static final Template ROW_TEMPLATE =
+            new Template(
+                    "<tr><td class='td_file'><a href='${path}'>${name}</td><td>${size}</td></tr>");
+
+    private static final Template HEADER_TEMPLATE =
+            new Template("<html><head>" + TITLE + CSS + "</head>"
+                    + "<body><div class='wrapper'><h1>" + DATASET_DOWNLOAD_SERVICE + "</h1>"
+                    + "<div class='div_hd'>Information about data set</div>" + "<table>"
+                    + "<tr><td class='td_hd'>Group:</td><td>${group}</td></tr>"
+                    + "<tr><td class='td_hd'>Project:</td><td>${project}</td></tr>"
+                    + "<tr><td class='td_hd'>Experiment:</td><td>${experiment}</td></tr>"
+                    + "<tr><td class='td_hd'>Sample:</td><td>${sample}</td></tr>"
+                    + "<tr><td class='td_hd'>Data Set Code:</td><td>${dataset}</td></tr></table> "
+                    + "<div class='div_hd'>Files</div>" + "<table> " + "${folder}" + "");
+
+    private static final Template FOOTER_TEMPLATE =
+            new Template("</table> </div> <div class='footer'>${footer} </div> </body></html>");
+
+    private PrintWriter writer;
+
+    private final String urlPrefix;
+
+    private final String relativePathOrNull;
+
+    HTMLDirectoryRenderer(final RenderingContext context)
+    {
+        this.relativePathOrNull = context.getRelativePathOrNull();
+        final String prefix = context.getUrlPrefix();
+        this.urlPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
+    }
+
+    public void setWriter(final PrintWriter writer)
+    {
+        this.writer = writer;
+    }
+
+    public void printHeader(final ExternalDataPE dataSet)
+    {
+        final String datasetCode = dataSet.getCode();
+        final String sampleCode = dataSet.getAssociatedSampleCode();
+        final ProcedurePE procedure = dataSet.getProcedure();
+        final ExperimentPE experiment = procedure.getExperiment();
+        final String experimentCode = experiment.getCode();
+        final ProjectPE project = experiment.getProject();
+        final String projectCode = project.getCode();
+        final GroupPE group = project.getGroup();
+        final String groupCode = group.getCode();
+        final Template template = HEADER_TEMPLATE.createFreshCopy();
+        template.bind("group", groupCode);
+        template.bind("project", projectCode);
+        template.bind("experiment", experimentCode);
+        template.bind("sample", sampleCode);
+        template.bind("dataset", datasetCode);
+        if (StringUtils.isNotBlank(relativePathOrNull))
+        {
+            template.bind("folder", "<tr><td class='td_hd'>Folder:</td><td>" + relativePathOrNull
+                    + "</td></tr>");
+        } else
+        {
+            template.bind("folder", "");
+        }
+        writer.println(template.createText());
+    }
+
+    public void printLinkToParentDirectory(final String relativePath)
+    {
+        printRow("..", relativePath, "");
+    }
+
+    public void printDirectory(final String name, final String relativePath)
+    {
+        printRow(name, relativePath, "");
+    }
+
+    public void printFile(final String name, final String relativePath, final long size)
+    {
+        printRow(name, relativePath, renderFileSize(size));
+    }
+
+    private void printRow(final String name, final String relativePath, final String fileSize)
+    {
+        final Template template = ROW_TEMPLATE.createFreshCopy();
+        template.bind("path", urlPrefix + encodeURL(relativePath));
+        template.bind("name", name);
+        template.bind("size", fileSize);
+        writer.println(template.createText());
+    }
+
+    private String encodeURL(final String url)
+    {
+        try
+        {
+            return URLEncoder.encode(url, "UTF-8");
+        } catch (final UnsupportedEncodingException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        }
+    }
+
+    private final static String renderFileSize(final long size)
+    {
+        return FileUtils.byteCountToDisplaySize(size);
+    }
+
+    public void printFooter()
+    {
+        final Template template = FOOTER_TEMPLATE.createFreshCopy();
+        template
+                .bind("footer",
+                        "Copyright &copy; 2008 ETHZ - <a href='http://www.cisd.systemsx.ethz.ch/'>CISD</a>");
+        writer.println(template.createText());
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/HTMLRendererFactory.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/HTMLRendererFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..a211fbbff5107a13734c40fd18164cd5fe669335
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/HTMLRendererFactory.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.io.PrintWriter;
+
+/**
+ * Factory for rendering file system view in HTML.
+ *
+ * @author Franz-Josef Elmer
+ */
+public class HTMLRendererFactory implements IRendererFactory
+{
+    public String getContentType()
+    {
+        return "text/html";
+    }
+
+
+    public IDirectoryRenderer createDirectoryRenderer(RenderingContext context)
+    {
+        return new HTMLDirectoryRenderer(context);
+    }
+
+    public IErrorRenderer createErrorRenderer()
+    {
+        return new HTMLErrorRenderer();
+    }
+
+    private static class HTMLErrorRenderer implements IErrorRenderer
+    {
+        private PrintWriter writer;
+
+        public void setWriter(PrintWriter writer)
+        {
+            this.writer = writer;
+        }
+        
+        public void printErrorMessage(String errorMessage)
+        {
+            writer.println("<html><body><h1>Error</h1>");
+            writer.println(errorMessage);
+            writer.println("</body></html>");
+        }
+
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IDirectoryRenderer.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IDirectoryRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..2d7ed4338f13a1b311f75efddaa87a893185653b
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IDirectoryRenderer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalDataPE;
+
+/**
+ * Interface of a renderer of a directory.
+ * 
+ * @author Franz-Josef Elmer
+ */
+public interface IDirectoryRenderer extends IWriterInjector
+{
+    public void printHeader(ExternalDataPE dataSet);
+
+    public void printLinkToParentDirectory(String relativePath);
+
+    public void printDirectory(String name, String relativePath);
+
+    public void printFile(String name, String relativePath, long size);
+
+    public void printFooter();
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IErrorRenderer.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IErrorRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..d44d88501d7e6833b8058e0f0ca8f3f9de7f92a6
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IErrorRenderer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+/**
+ *  Interface for a renderer of an error messages. 
+ *
+ * @author Franz-Josef Elmer
+ */
+public interface IErrorRenderer extends IWriterInjector
+{
+    /**
+     * Prints the error message set by
+     * {@link IWriterInjector#setWriter(java.io.PrintWriter)}.
+     */
+    public void printErrorMessage(String errorMessage);
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IRendererFactory.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IRendererFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ab85e7b74aaece984d8a172138a09a661dc5074
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IRendererFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+/**
+ * Interface of a factory of renderers.
+ *
+ * @author Franz-Josef Elmer
+ */
+public interface IRendererFactory
+{
+    public String getContentType();
+
+    public IDirectoryRenderer createDirectoryRenderer(RenderingContext context);
+    
+    public IErrorRenderer createErrorRenderer();
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IWriterInjector.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IWriterInjector.java
new file mode 100644
index 0000000000000000000000000000000000000000..4cecc0782440e89d901284891c9e8be5180a57bc
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/IWriterInjector.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.io.PrintWriter;
+
+/**
+ * Interface of objects which allows injection of a {@link PrintWriter}.
+ *
+ * @author Franz-Josef Elmer
+ */
+public interface IWriterInjector
+{
+    /**
+     * Sets the writer.
+     */
+    public void setWriter(PrintWriter writer);
+
+}
\ No newline at end of file
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/PlainTextRendererFactory.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/PlainTextRendererFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bf45fe05224e86be3ff15e73ed2a5dfcca880c2
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/PlainTextRendererFactory.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.io.PrintWriter;
+
+import org.apache.commons.io.FileUtils;
+
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalDataPE;
+
+/**
+ * Factory for rendering file system view in plain text.
+ * 
+ * @author Tomasz Pylak
+ */
+public class PlainTextRendererFactory implements IRendererFactory
+{
+
+    public IDirectoryRenderer createDirectoryRenderer(RenderingContext context)
+    {
+        return new PlainTextDirectoryRenderer();
+    }
+
+    public IErrorRenderer createErrorRenderer()
+    {
+        return new IErrorRenderer()
+            {
+                private PrintWriter writer;
+
+                public void printErrorMessage(String errorMessage)
+                {
+                    writer.println("Error:");
+                    writer.println(errorMessage);
+                }
+
+                public void setWriter(PrintWriter writer)
+                {
+                    this.writer = writer;
+                }
+            };
+    }
+
+    public String getContentType()
+    {
+        return "text";
+    }
+
+    private static class PlainTextDirectoryRenderer implements IDirectoryRenderer
+    {
+        private PrintWriter writer;
+
+        public String getContentType()
+        {
+            return "text";
+        }
+
+        public void printDirectory(String name, String relativePath)
+        {
+            writer.print(name + "\n");
+        }
+
+        public void printFile(String name, String relativePath, long size)
+        {
+            writer.format("%s\t%s\n", name, FileUtils.byteCountToDisplaySize(size));
+        }
+
+        public void printFooter()
+        {
+        }
+
+        public void printHeader(ExternalDataPE dataSet)
+        {
+            writer.println("Directory content:");
+        }
+
+        public void printLinkToParentDirectory(String relativePath)
+        {
+        }
+
+        public void setWriter(PrintWriter writer)
+        {
+            this.writer = writer;
+        }
+
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/RenderingContext.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/RenderingContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..46309f1a4fdc76131251a0aa30f8e90f5bf7e62b
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/RenderingContext.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import java.io.File;
+
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+
+final class RenderingContext
+{
+    private final File rootDir;
+
+    private final String relativePathOrNull;
+
+    private File file;
+
+    private final String urlPrefix;
+
+    private String relativeParentPath;
+
+    RenderingContext(File rootDir, String urlPrefix, String relativePathOrNull)
+    {
+        this.rootDir = rootDir;
+        this.relativePathOrNull = relativePathOrNull;
+        this.file = rootDir;
+        this.urlPrefix = urlPrefix;
+        if (relativePathOrNull != null && relativePathOrNull.length() > 0)
+        {
+            file = new File(rootDir, relativePathOrNull);
+            relativeParentPath = FileUtilities.getRelativeFile(rootDir, file.getParentFile());
+            if (relativeParentPath == null)
+            {
+                relativeParentPath = "";
+            }
+        }
+    }
+
+    public final File getRootDir()
+    {
+        return rootDir;
+    }
+
+    public final String getRelativePathOrNull()
+    {
+        return relativePathOrNull;
+    }
+
+    public final File getFile()
+    {
+        return file;
+    }
+
+    public final String getUrlPrefix()
+    {
+        return urlPrefix;
+    }
+
+    public final String getRelativeParentPath()
+    {
+        return relativeParentPath;
+    }
+
+}
\ No newline at end of file
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/.gitignore b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServletTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServletTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a565747ea6231e64511a127e033fceffc9c74c0
--- /dev/null
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/DatasetDownloadServletTest.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2008 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.dss.generic.server;
+
+import static org.testng.AssertJUnit.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.log4j.Level;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.exceptions.CheckedExceptionTunnel;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.logging.BufferedAppender;
+import ch.systemsx.cisd.common.utilities.OSUtilities;
+import ch.systemsx.cisd.openbis.dss.generic.server.ApplicationContext;
+import ch.systemsx.cisd.openbis.dss.generic.server.ConfigParameters;
+import ch.systemsx.cisd.openbis.dss.generic.server.DatasetDownloadServlet;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ExternalDataPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.GroupPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorType;
+import ch.systemsx.cisd.openbis.generic.shared.dto.LocatorTypePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProcedurePE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePE;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class DatasetDownloadServletTest
+{
+    private static final String APPLICATION_NAME = "download";
+
+    private static final String REQUEST_URI_PREFIX = "/" + APPLICATION_NAME + "/";
+
+    private static final String EXPIRATION_MESSAGE =
+            "<html><body>Download session expired.</body></html>";
+
+    private static final String LOGGER_NAME = "OPERATION.DatasetDownloadServlet";
+
+    private static final String LOG_INFO = "INFO  " + LOGGER_NAME + " - ";
+
+    private static final String LOG_ERROR = "ERROR " + LOGGER_NAME + " - ";
+
+    private static final File TEST_FOLDER = new File("targets/unit-test/store");
+
+    private static final String EXAMPLE_DATA_SET_FOLDER_NAME = "data set #123";
+
+    private static final File EXAMPLE_DATA_SET_FOLDER =
+            new File(TEST_FOLDER, EXAMPLE_DATA_SET_FOLDER_NAME);
+
+    private static final String EXAMPLE_FILE_NAME = "read me @home.txt";
+
+    private static final File EXAMPLE_FILE = new File(EXAMPLE_DATA_SET_FOLDER, EXAMPLE_FILE_NAME);
+
+    private static final String EXAMPLE_FILE_CONTENT = "Hello world!";
+
+    private static final String EXAMPLE_DATA_SET_SUB_FOLDER_NAME = "+ s % ! # @";
+
+    private static final String ESCAPED_EXAMPLE_DATA_SET_SUB_FOLDER_NAME =
+            encode(EXAMPLE_DATA_SET_SUB_FOLDER_NAME);
+
+    private static final File EXAMPLE_DATA_SET_SUB_FOLDER =
+            new File(EXAMPLE_DATA_SET_FOLDER, EXAMPLE_DATA_SET_SUB_FOLDER_NAME);
+
+    private static final String EXAMPLE_SESSION_ID = "AV76CF";
+
+    private static final String EXAMPLE_DATA_SET_CODE = "1234-1";
+
+    private static final String SAMPLE_CODE = "SAMPLE-S";
+
+    private static final String EXPERIMENT_CODE = "EPERIMENT-E";
+
+    private static final String GROUP_CODE = "GROUP-G";
+
+    private static final String PROJECT_CODE = "PROJECT-P";
+
+    private static String encode(String url)
+    {
+        try
+        {
+            return URLEncoder.encode(url, "UTF-8");
+        } catch (UnsupportedEncodingException ex)
+        {
+            throw CheckedExceptionTunnel.wrapIfNecessary(ex);
+        }
+    }
+
+    private BufferedAppender logRecorder;
+
+    private Mockery context;
+
+    private HttpServletRequest request;
+
+    private HttpServletResponse response;
+
+    private IETLLIMSService dataSetService;
+
+    private HttpSession httpSession;
+
+    @BeforeMethod
+    public void setUp()
+    {
+        logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.DEBUG);
+        context = new Mockery();
+        request = context.mock(HttpServletRequest.class);
+        response = context.mock(HttpServletResponse.class);
+        dataSetService = context.mock(IETLLIMSService.class);
+        httpSession = context.mock(HttpSession.class);
+        TEST_FOLDER.mkdirs();
+        EXAMPLE_DATA_SET_FOLDER.mkdir();
+        FileUtilities.writeToFile(EXAMPLE_FILE, EXAMPLE_FILE_CONTENT);
+        EXAMPLE_DATA_SET_SUB_FOLDER.mkdir();
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        logRecorder.reset();
+        FileUtilities.deleteRecursively(TEST_FOLDER);
+        // To following line of code should also be called at the end of each test method.
+        // Otherwise one do not known which test failed.
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testInitialDoGet() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        final ExternalDataPE externalData = createExternalData();
+        prepareParseRequestURL();
+        prepareForObtainingDataSetFromServer(externalData);
+        prepareForGettingDataSetFromSession(externalData, "");
+        prepareForCreatingHTML(writer);
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals(
+                "<html><head><title> Data Set Download Service: GROUP-G/PROJECT-P/EPERIMENT-E/SAMPLE-S/1234-1</title><style type=\'text/css\'> * { margin: 3px; }html { height: 100%;  }body { height: 100%; font-family: verdana, tahoma, helvetica; font-size: 11px; text-align:left; }h1 { text-align: center; padding: 1em; color: #1E4E8F;}.td_hd { border: 1px solid #FFFFFF; padding 3px; background-color: #DDDDDD; height: 1.5em; }.div_hd { background-color: #1E4E8F; color: white; font-weight: bold; padding: 3px; }table { border-collapse: collapse; padding: 1em; }tr, td { font-family: verdana, tahoma, helvetica; font-size: 11px; }.td_file { font-family: verdana, tahoma, helvetica; font-size: 11px; height: 1.5em }.wrapper { min-height: 100%; height: auto !important; height: 100%; margin: 0em auto -4em; }.footer { height: 4em; text-align: center; }</style></head><body><div class=\'wrapper\'><h1>Data Set Download Service</h1><div class=\'div_hd\'>Information about data set</div><table><tr><td class=\'td_hd\'>Group:</td><td>GROUP-G</td></tr><tr><td class=\'td_hd\'>Project:</td><td>PROJECT-P</td></tr><tr><td class=\'td_hd\'>Experiment:</td><td>EPERIMENT-E</td></tr><tr><td class=\'td_hd\'>Sample:</td><td>SAMPLE-S</td></tr><tr><td class=\'td_hd\'>Data Set Code:</td><td>1234-1</td></tr></table> <div class=\'div_hd\'>Files</div><table> "
+                        + OSUtilities.LINE_SEPARATOR
+                        + "<tr><td class=\'td_file\'><a href=\'/download/1234-1/%2B+s+%25+%21+%23+%40\'>+ s % ! # @</td><td></td></tr>"
+                        + OSUtilities.LINE_SEPARATOR
+                        + "<tr><td class=\'td_file\'><a href=\'/download/1234-1/read+me+%40home.txt\'>read me @home.txt</td><td>12 bytes</td></tr>"
+                        + OSUtilities.LINE_SEPARATOR
+                        + "</table> </div> <div class=\'footer\'>Copyright &copy; 2008 ETHZ - <a href=\'http://www.cisd.systemsx.ethz.ch/\'>CISD</a> </div> </body></html>"
+                        + OSUtilities.LINE_SEPARATOR + "", writer.toString());
+        assertEquals(LOG_INFO + "Data set '1234-1' obtained from openBIS server."
+                + OSUtilities.LINE_SEPARATOR + LOG_INFO
+                + "For data set '1234-1' show directory <wd>/data set #123",
+                getNormalizedLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    private void prepareParseRequestURL()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getParameter(DatasetDownloadServlet.SESSION_ID_KEY);
+                    will(returnValue(EXAMPLE_SESSION_ID));
+
+                    one(request).getParameter(DatasetDownloadServlet.DISPLAY_MODE_KEY);
+                    will(returnValue(null));
+                }
+            });
+    }
+
+    @Test
+    public void testInitialDoGetButDataSetNotFoundInStore() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        final ExternalDataPE externalData = createExternalData();
+        LocatorTypePE locatorType = new LocatorTypePE();
+        locatorType.setCode("unknown");
+        externalData.setLocatorType(locatorType);
+        prepareParseRequestURL();
+        prepareForObtainingDataSetFromServer(externalData);
+        prepareForGettingDataSetFromSession(externalData, "blabla");
+        context.checking(new Expectations()
+            {
+                {
+                    one(response).setContentType("text/html");
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        String pageContent = writer.toString();
+        String snippet = "Data set '1234-1' not found in store";
+        assertEquals("Text snippet >" + snippet + "< not found in following page content: "
+                + pageContent, true, pageContent.indexOf(snippet) > 0);
+        String logContent = logRecorder.getLogContent();
+        assertEquals("Text snippet >" + snippet + "< not found in following page content: "
+                + logContent, true, logContent.indexOf(snippet) > 0);
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetButUnknownDataSetCode() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        final ExternalDataPE externalData = createExternalData();
+        prepareParseRequestURL();
+        prepareForObtainingDataSetFromServer(externalData);
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getSession(false);
+                    will(returnValue(httpSession));
+
+                    one(httpSession).getAttribute(DatasetDownloadServlet.DATA_SET_KEY);
+                    Map<String, ExternalData> map = new HashMap<String, ExternalData>();
+                    will(returnValue(map));
+
+                    one(request).getRequestURI();
+                    String codeAndPath = externalData.getCode();
+                    will(returnValue(REQUEST_URI_PREFIX + codeAndPath));
+
+                    one(response).setContentType("text/html");
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals("<html><body><h1>Error</h1>" + OSUtilities.LINE_SEPARATOR
+                + "Unknown data set '1234-1'." + OSUtilities.LINE_SEPARATOR + "</body></html>"
+                + OSUtilities.LINE_SEPARATOR, writer.toString());
+        String logContent = logRecorder.getLogContent();
+        assertEquals(LOG_INFO + "Data set '1234-1' obtained from openBIS server."
+                + OSUtilities.LINE_SEPARATOR + LOG_INFO
+                + "User failure: Unknown data set '1234-1'.", logContent);
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetSubFolder() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        final ExternalDataPE externalData = createExternalData();
+        prepareParseRequestURLNoSession();
+        prepareForGettingDataSetFromSession(externalData, ESCAPED_EXAMPLE_DATA_SET_SUB_FOLDER_NAME);
+        prepareForCreatingHTML(writer);
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals(
+                "<html><head><title> Data Set Download Service: GROUP-G/PROJECT-P/EPERIMENT-E/SAMPLE-S/1234-1</title><style type=\'text/css\'> * { margin: 3px; }html { height: 100%;  }body { height: 100%; font-family: verdana, tahoma, helvetica; font-size: 11px; text-align:left; }h1 { text-align: center; padding: 1em; color: #1E4E8F;}.td_hd { border: 1px solid #FFFFFF; padding 3px; background-color: #DDDDDD; height: 1.5em; }.div_hd { background-color: #1E4E8F; color: white; font-weight: bold; padding: 3px; }table { border-collapse: collapse; padding: 1em; }tr, td { font-family: verdana, tahoma, helvetica; font-size: 11px; }.td_file { font-family: verdana, tahoma, helvetica; font-size: 11px; height: 1.5em }.wrapper { min-height: 100%; height: auto !important; height: 100%; margin: 0em auto -4em; }.footer { height: 4em; text-align: center; }</style></head><body><div class=\'wrapper\'><h1>Data Set Download Service</h1><div class=\'div_hd\'>Information about data set</div><table><tr><td class=\'td_hd\'>Group:</td><td>GROUP-G</td></tr><tr><td class=\'td_hd\'>Project:</td><td>PROJECT-P</td></tr><tr><td class=\'td_hd\'>Experiment:</td><td>EPERIMENT-E</td></tr><tr><td class=\'td_hd\'>Sample:</td><td>SAMPLE-S</td></tr><tr><td class=\'td_hd\'>Data Set Code:</td><td>1234-1</td></tr></table> <div class=\'div_hd\'>Files</div><table> <tr><td class=\'td_hd\'>Folder:</td><td>+ s % ! # @</td></tr>"
+                        + OSUtilities.LINE_SEPARATOR
+                        + "<tr><td class=\'td_file\'><a href=\'/download/1234-1/\'>..</td><td></td></tr>"
+                        + OSUtilities.LINE_SEPARATOR
+                        + "</table> </div> <div class=\'footer\'>Copyright &copy; 2008 ETHZ - <a href=\'http://www.cisd.systemsx.ethz.ch/\'>CISD</a> </div> </body></html>"
+                        + OSUtilities.LINE_SEPARATOR, writer.toString());
+        assertEquals(LOG_INFO + "For data set '1234-1' show directory <wd>/data set #123/"
+                + EXAMPLE_DATA_SET_SUB_FOLDER_NAME, getNormalizedLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetFile() throws Exception
+    {
+        final ExternalDataPE externalData = createExternalData();
+        prepareParseRequestURLNoSession();
+        prepareForGettingDataSetFromSession(externalData, EXAMPLE_FILE_NAME);
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        context.checking(new Expectations()
+            {
+                {
+                    one(response).setContentType(DatasetDownloadServlet.BINARY_CONTENT_TYPE);
+                    one(response).setContentLength(EXAMPLE_FILE_CONTENT.length());
+                    one(response).setHeader("Content-Disposition",
+                            "inline; filename=" + EXAMPLE_FILE_NAME);
+                    one(response).getOutputStream();
+                    will(returnValue(new ServletOutputStream()
+                        {
+                            @Override
+                            public void write(int b) throws IOException
+                            {
+                                outputStream.write(b);
+                            }
+                        }));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals("Hello world!", outputStream.toString());
+        assertEquals(LOG_INFO + "For data set '1234-1' deliver file "
+                + "<wd>/data set #123/read me @home.txt (12 bytes).", getNormalizedLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetNonExistingFile() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        final ExternalDataPE externalData = createExternalData();
+        prepareParseRequestURLNoSession();
+        prepareForGettingDataSetFromSession(externalData, "blabla");
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getRequestURL();
+                    will(returnValue(new StringBuffer("requestURL")));
+
+                    one(request).getQueryString();
+                    will(returnValue("queryString"));
+
+                    one(response).setContentType("text/html");
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals("<html><body><h1>Error</h1>" + OSUtilities.LINE_SEPARATOR
+                + "File 'blabla' does not exist." + OSUtilities.LINE_SEPARATOR + "</body></html>"
+                + OSUtilities.LINE_SEPARATOR, writer.toString());
+        String logContent = getNormalizedLogContent();
+        assertEquals("The following string does not start as expected: " + logContent, true,
+                logContent.startsWith(LOG_ERROR
+                        + "Request requestURL?queryString caused an exception:"));
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetForExpiredSession() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        prepareParseRequestURLNoSession();
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getRequestURI();
+                    will(returnValue(REQUEST_URI_PREFIX + EXAMPLE_DATA_SET_CODE));
+
+                    one(request).getSession(false);
+                    will(returnValue(null));
+
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals(EXPIRATION_MESSAGE, writer.toString());
+        assertEquals("", getNormalizedLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetRequestURINotStartingWithApplicationName() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getRequestURI();
+                    will(returnValue("blabla"));
+
+                    one(request).getRequestURL();
+                    will(returnValue(new StringBuffer("requestURL")));
+
+                    one(request).getQueryString();
+                    will(returnValue("query"));
+
+                    one(response).setContentType("text");
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals("Error:" + OSUtilities.LINE_SEPARATOR
+                + "Request URI 'blabla' expected to start with '/download/'."
+                + OSUtilities.LINE_SEPARATOR, writer.toString());
+        String logContent = getNormalizedLogContent();
+        assertEquals("The following string does not start as expected: " + logContent, true,
+                logContent.startsWith(LOG_ERROR + "Request requestURL?query caused an exception:"));
+
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testDoGetForPathInfoStartingWithSeparator() throws Exception
+    {
+        final StringWriter writer = new StringWriter();
+        final ExternalDataPE externalData = createExternalData();
+        prepareParseRequestURL();
+        prepareForObtainingDataSetFromServer(externalData);
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getRequestURI();
+                    will(returnValue(REQUEST_URI_PREFIX + EXAMPLE_DATA_SET_CODE));
+
+                    one(request).getSession(false);
+                    will(returnValue(null));
+
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+                }
+            });
+
+        DatasetDownloadServlet servlet = createServlet();
+        servlet.doGet(request, response);
+        assertEquals(EXPIRATION_MESSAGE, writer.toString());
+        assertEquals(LOG_INFO + "Data set '1234-1' obtained from openBIS server.",
+                getNormalizedLogContent());
+
+        context.assertIsSatisfied();
+    }
+
+    private void prepareForGettingDataSetFromSession(final ExternalDataPE externalData,
+            final String path)
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getSession(false);
+                    will(returnValue(httpSession));
+
+                    one(httpSession).getAttribute(DatasetDownloadServlet.DATA_SET_KEY);
+                    Map<String, ExternalDataPE> map = new HashMap<String, ExternalDataPE>();
+                    map.put(externalData.getCode(), externalData);
+                    will(returnValue(map));
+
+                    one(request).getRequestURI();
+                    String codeAndPath = REQUEST_URI_PREFIX + externalData.getCode() + "/" + path;
+                    will(returnValue(codeAndPath));
+
+                    allowing(request).getRequestURI();
+                    will(returnValue(APPLICATION_NAME + "/" + codeAndPath));
+
+                }
+            });
+    }
+
+    private void prepareParseRequestURLNoSession()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(request).getParameter(DatasetDownloadServlet.SESSION_ID_KEY);
+                    will(returnValue(null));
+
+                    one(request).getParameter(DatasetDownloadServlet.DISPLAY_MODE_KEY);
+                    will(returnValue("html"));
+                }
+            });
+    }
+
+    private void prepareForObtainingDataSetFromServer(final ExternalDataPE externalData)
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(dataSetService).tryGetDataSet(EXAMPLE_SESSION_ID, EXAMPLE_DATA_SET_CODE);
+                    will(returnValue(externalData));
+
+                    one(request).getSession(true);
+                    will(returnValue(httpSession));
+
+                    one(httpSession).setMaxInactiveInterval(120);
+                    one(httpSession).getAttribute(DatasetDownloadServlet.DATA_SET_KEY);
+                    will(returnValue(null));
+
+                    one(httpSession).setAttribute(DatasetDownloadServlet.DATA_SET_KEY,
+                            new HashMap<String, ExternalDataPE>());
+                }
+            });
+    }
+
+    private void prepareForCreatingHTML(final StringWriter writer) throws IOException
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(response).getWriter();
+                    will(returnValue(new PrintWriter(writer)));
+
+                    one(response).setContentType("text/html");
+                }
+            });
+    }
+
+    private ExternalDataPE createExternalData()
+    {
+        GroupPE group = new GroupPE();
+        group.setCode(GROUP_CODE);
+        ProjectPE project = new ProjectPE();
+        project.setCode(PROJECT_CODE);
+        project.setGroup(group);
+        ProcedurePE procedure = new ProcedurePE();
+        ExperimentPE experiment = new ExperimentPE();
+        experiment.setCode(EXPERIMENT_CODE);
+        experiment.setProject(project);
+        procedure.setExperiment(experiment);
+        final ExternalDataPE externalData = new ExternalDataPE();
+        externalData.setProcedure(procedure);
+        externalData.setCode(EXAMPLE_DATA_SET_CODE);
+        SamplePE samplePE = new SamplePE();
+        samplePE.setCode(SAMPLE_CODE);
+        externalData.setSampleAcquiredFrom(samplePE);
+        LocatorTypePE locatorTypePE = new LocatorTypePE();
+        locatorTypePE.setCode(LocatorType.DEFAULT_LOCATOR_TYPE_CODE);
+        externalData.setLocatorType(locatorTypePE);
+        externalData.setLocation(EXAMPLE_DATA_SET_FOLDER_NAME);
+        return externalData;
+    }
+
+    private DatasetDownloadServlet createServlet()
+    {
+        Properties properties = new Properties();
+        properties.setProperty(ConfigParameters.STOREROOT_DIR_KEY, TEST_FOLDER.toString());
+        properties.setProperty(ConfigParameters.PORT_KEY, "8080");
+        properties.setProperty(ConfigParameters.SERVER_URL_KEY, "http://localhost");
+        properties.setProperty(ConfigParameters.SESSION_TIMEOUT_KEY, "2");
+        properties.setProperty(ConfigParameters.KEYSTORE_PATH_KEY, "/");
+        properties.setProperty(ConfigParameters.KEYSTORE_PASSWORD_KEY, "x");
+        properties.setProperty(ConfigParameters.KEYSTORE_KEY_PASSWORD_KEY, "y");
+        ConfigParameters configParameters = new ConfigParameters(properties);
+        return new DatasetDownloadServlet(new ApplicationContext(dataSetService, configParameters,
+                APPLICATION_NAME));
+    }
+
+    private String getNormalizedLogContent()
+    {
+        String logContent = logRecorder.getLogContent();
+        logContent = logContent.replace(TEST_FOLDER.getAbsolutePath(), "<wd>");
+        logContent = logContent.replace('\\', '/');
+        return logContent;
+    }
+
+}
diff --git a/datastore_server/sourceTest/java/tests.xml b/datastore_server/sourceTest/java/tests.xml
index e8c6f023e2b6fc1afa085f085c31e77836e50519..cdd9b5631f699efede5f768192417bf157c7b70d 100644
--- a/datastore_server/sourceTest/java/tests.xml
+++ b/datastore_server/sourceTest/java/tests.xml
@@ -9,6 +9,7 @@
         </groups>
         <packages>
             <package name="ch.systemsx.cisd.etlserver.*" />
+            <package name="ch.systemsx.cisd.openbis.dss.*" />
         </packages>
     </test>
 </suite>