From 7c8f30dbe860a8abd0de3e7e140953774d3f06c2 Mon Sep 17 00:00:00 2001
From: kaloyane <kaloyane>
Date: Tue, 26 Apr 2011 11:16:50 +0000
Subject: [PATCH] LMS-2197: create a simple FTP server showing
 space/projects/experiments/datasets (initial version).

SVN: 21038
---
 datastore_server/.classpath                   |   3 +
 .../dss/generic/server/ConfigParameters.java  |  12 +-
 .../generic/server/ftp/DSSFileSystemView.java | 124 ++++++++
 .../dss/generic/server/ftp/FtpConstants.java  |  33 +++
 .../server/ftp/FtpPathResolverContext.java    |  58 ++++
 .../server/ftp/FtpPathResolverRegistry.java   |  90 ++++++
 .../dss/generic/server/ftp/FtpServer.java     | 125 ++++++++
 .../generic/server/ftp/FtpServerConfig.java   | 139 +++++++++
 .../dss/generic/server/ftp/FtpUser.java       |  92 ++++++
 .../generic/server/ftp/FtpUserManager.java    | 106 +++++++
 .../generic/server/ftp/IFtpPathResolver.java  |  38 +++
 .../server/ftp/IFtpPathResolverRegistry.java  |  29 ++
 .../server/ftp/resolver/AbstractFtpFile.java  | 120 ++++++++
 .../ftp/resolver/AbstractFtpFolder.java       |  60 ++++
 .../resolver/ExperimentFolderResolver.java    | 100 +++++++
 .../HierarchicalContentToFtpFileAdapter.java  | 109 +++++++
 .../resolver/IExperimentChildrenLister.java   |  31 ++
 .../ftp/resolver/ProjectFolderResolver.java   |  85 ++++++
 .../ftp/resolver/RootFolderResolver.java      |  88 ++++++
 .../ftp/resolver/SpaceFolderResolver.java     | 102 +++++++
 .../TemplateBasedDataSetResourceResolver.java | 267 ++++++++++++++++++
 .../dss/generic/server/webdav/.gitignore      |   0
 .../dss/generic/shared/ServiceProvider.java   |  10 +-
 .../source/java/dssApplicationContext.xml     |  22 ++
 .../openbis/generic/server/ETLService.java    |  22 ++
 .../generic/server/ETLServiceLogger.java      |  12 +
 .../generic/shared/IETLLIMSService.java       |  22 +-
 27 files changed, 1889 insertions(+), 10 deletions(-)
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/DSSFileSystemView.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpConstants.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverContext.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverRegistry.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServer.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServerConfig.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUser.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUserManager.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolver.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolverRegistry.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFile.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFolder.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ExperimentFolderResolver.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/HierarchicalContentToFtpFileAdapter.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/IExperimentChildrenLister.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ProjectFolderResolver.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/RootFolderResolver.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/SpaceFolderResolver.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/TemplateBasedDataSetResourceResolver.java
 create mode 100644 datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/webdav/.gitignore

diff --git a/datastore_server/.classpath b/datastore_server/.classpath
index 61b8c11668f..5b7e6596399 100644
--- a/datastore_server/.classpath
+++ b/datastore_server/.classpath
@@ -62,5 +62,8 @@
 	<classpathentry kind="lib" path="/libraries/cisd-image_readers/cisd-image_readers-jai.jar"/>
 	<classpathentry kind="lib" path="/libraries/cisd-image_readers/cisd-image_readers.jar"/>
 	<classpathentry kind="lib" path="/libraries/cisd-image_readers/cisd-image_readers-imagej.jar"/>
+	<classpathentry kind="lib" path="/libraries/ftpserver/ftplet-api.jar"/>
+	<classpathentry kind="lib" path="/libraries/ftpserver/ftpserver-core.jar"/>
+	<classpathentry kind="lib" path="/libraries/mina/mina-core.jar"/>
 	<classpathentry kind="output" path="targets/classes"/>
 </classpath>
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
index 13cdf8f4412..3077eb0d03b 100644
--- 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
@@ -35,7 +35,7 @@ import ch.systemsx.cisd.common.utilities.PropertyUtils;
  * 
  * @author Franz-Josef Elmer
  */
-final class ConfigParameters
+public final class ConfigParameters
 {
 
     private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
@@ -68,11 +68,11 @@ final class ConfigParameters
 
     private static final int DEFAULT_AUTH_CACHE_CLEANUP_TIMER_PERIOD_MINS = 3 * 60;
 
-    static final String KEYSTORE_PATH_KEY = KEYSTORE + "path";
+    public static final String KEYSTORE_PATH_KEY = KEYSTORE + "path";
 
-    static final String KEYSTORE_PASSWORD_KEY = KEYSTORE + "password";
+    public static final String KEYSTORE_PASSWORD_KEY = KEYSTORE + "password";
 
-    static final String KEYSTORE_KEY_PASSWORD_KEY = KEYSTORE + "key-password";
+    public static final String KEYSTORE_KEY_PASSWORD_KEY = KEYSTORE + "key-password";
 
     static final String PLUGIN_SERVICES_LIST_KEY = "plugin-services";
 
@@ -124,7 +124,7 @@ final class ConfigParameters
 
     private final String webstartJarPath;
 
-    public static final class PluginServlet
+    static final class PluginServlet
     {
         private final String servletClass;
 
@@ -169,7 +169,7 @@ final class ConfigParameters
      * 
      * @throws ConfigurationFailureException if a property is missed or has an invalid value.
      */
-    public ConfigParameters(final Properties properties)
+    ConfigParameters(final Properties properties)
     {
         this.properties = properties;
         storePath = new File(PropertyUtils.getMandatoryProperty(properties, STOREROOT_DIR_KEY));
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/DSSFileSystemView.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/DSSFileSystemView.java
new file mode 100644
index 00000000000..6672b0ec781
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/DSSFileSystemView.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import org.apache.ftpserver.ftplet.FileSystemView;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.cifex.client.application.utils.StringUtils;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+
+/**
+ * A central class that manages the movement of a user up and down the exposed hierarchical
+ * structure.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class DSSFileSystemView implements FileSystemView
+{
+    private final String sessionToken;
+
+    private final IETLLIMSService service;
+
+    private FtpFile workingDirectory;
+
+    private final IFtpPathResolverRegistry pathResolverRegistry;
+
+    DSSFileSystemView(String sessionToken, IETLLIMSService service,
+            IFtpPathResolverRegistry pathResolverRegistry) throws FtpException
+    {
+        this.sessionToken = sessionToken;
+        this.service = service;
+        this.pathResolverRegistry = pathResolverRegistry;
+        this.workingDirectory = getHomeDirectory();
+    }
+
+    public boolean changeWorkingDirectory(String path) throws FtpException
+    {
+        FtpFile ftpFile = getFile(path);
+        if (ftpFile != null && ftpFile.isDirectory())
+        {
+            workingDirectory = ftpFile;
+            return true;
+        }
+        return false;
+    }
+
+    public void dispose()
+    {
+    }
+
+    public FtpFile getFile(String path) throws FtpException
+    {
+        String normalizedPath = normalizePath(path);
+
+        // this check speeds directory listings in the LFTP console client
+        if (workingDirectory != null && workingDirectory.getAbsolutePath().equals(normalizedPath))
+        {
+            return workingDirectory;
+        }
+
+        FtpPathResolverContext context =
+                new FtpPathResolverContext(sessionToken, service, pathResolverRegistry);
+        return pathResolverRegistry.tryResolve(normalizedPath, context);
+    }
+
+    private String normalizePath(String path) throws FtpException
+    {
+        String result = path.trim();
+        if (result.startsWith(".."))
+        {
+            String currentPath = workingDirectory.getAbsolutePath();
+            int idx = currentPath.lastIndexOf(FtpConstants.FILE_SEPARATOR);
+            result = currentPath.substring(0, idx) + result.substring(2);
+        } else if (result.startsWith("."))
+        {
+            result = workingDirectory.getAbsolutePath() + result.substring(1);
+        } else if (false == result.startsWith(FtpConstants.ROOT_DIRECTORY))
+        {
+            result = workingDirectory.getAbsolutePath() + FtpConstants.FILE_SEPARATOR + result;
+        }
+        // remove trailing slashes
+        result = result.replaceAll("/*$", "");
+        // replace multiple adjacent slashes with a single slash
+        result = result.replaceAll("/+", "/");
+
+        if (StringUtils.isBlank(result))
+        {
+            return FtpConstants.ROOT_DIRECTORY;
+        }
+
+        return result;
+    }
+
+    public FtpFile getHomeDirectory() throws FtpException
+    {
+        return getFile(FtpConstants.ROOT_DIRECTORY);
+    }
+
+    public FtpFile getWorkingDirectory() throws FtpException
+    {
+        return workingDirectory;
+    }
+
+    public boolean isRandomAccessible() throws FtpException
+    {
+        return true;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpConstants.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpConstants.java
new file mode 100644
index 00000000000..8616dc1be02
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpConstants.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2011 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.ftp;
+
+/**
+ * @author Kaloyan Enimanev
+ */
+public class FtpConstants
+{
+
+    public static final String FTP_USER_GROUP_NAME = "dss";
+
+    public static final String FTP_USER_NAME = "dss";
+
+    public static final String ROOT_DIRECTORY = "/";
+
+    public static final String FILE_SEPARATOR = "/";
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverContext.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverContext.java
new file mode 100644
index 00000000000..465e4cce345
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverContext.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+
+/**
+ * An object holding all necessary context information for ftp path resolution.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class FtpPathResolverContext
+{
+
+    private final String sessionToken;
+
+    private final IETLLIMSService service;
+
+    private final IFtpPathResolverRegistry resolverRegistry;
+
+    public FtpPathResolverContext(String sessionToken, IETLLIMSService service,
+            IFtpPathResolverRegistry resolverRegistry)
+    {
+        this.sessionToken = sessionToken;
+        this.service = service;
+        this.resolverRegistry = resolverRegistry;
+    }
+
+    public String getSessionToken()
+    {
+        return sessionToken;
+    }
+
+    public IETLLIMSService getService()
+    {
+        return service;
+    }
+
+    public IFtpPathResolverRegistry getResolverRegistry()
+    {
+        return resolverRegistry;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverRegistry.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverRegistry.java
new file mode 100644
index 00000000000..31b862e0fd4
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpPathResolverRegistry.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.resolver.ExperimentFolderResolver;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.resolver.ProjectFolderResolver;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.resolver.RootFolderResolver;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.resolver.SpaceFolderResolver;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.resolver.TemplateBasedDataSetResourceResolver;
+
+/**
+ * A registry class keeping references to all known {@link IFtpPathResolver} instances.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class FtpPathResolverRegistry implements IFtpPathResolverRegistry
+{
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            FtpPathResolverRegistry.class);
+
+    private List<IFtpPathResolver> pathResolvers = new ArrayList<IFtpPathResolver>();
+
+    /**
+     * initializes the registry with all known {@link IFtpPathResolver}-s.
+     */
+    public FtpPathResolverRegistry(FtpServerConfig ftpServerConfig)
+    {
+        pathResolvers.add(new RootFolderResolver());
+        pathResolvers.add(new SpaceFolderResolver());
+        pathResolvers.add(new ProjectFolderResolver());
+        TemplateBasedDataSetResourceResolver dataSetResolver =
+                new TemplateBasedDataSetResourceResolver(ftpServerConfig);
+        pathResolvers.add(new ExperimentFolderResolver(dataSetResolver));
+        pathResolvers.add(dataSetResolver);
+    }
+
+    /**
+     * tries for find an {@link IFtpPathResolver} instance that can resolve a given path.
+     */
+    IFtpPathResolver tryFindResolver(String path)
+    {
+        for (IFtpPathResolver ftpFileCreator : pathResolvers)
+        {
+            if (ftpFileCreator.canResolve(path))
+            {
+                return ftpFileCreator;
+            }
+        }
+        return null;
+    }
+
+    public FtpFile tryResolve(String path, FtpPathResolverContext resolverContext)
+    {
+        IFtpPathResolver resolver = tryFindResolver(path);
+        if (resolver != null)
+        {
+            return resolver.resolve(path, resolverContext);
+        } else
+        {
+            String message =
+                    String.format("Cannot find resolver for path '%s'. Wrong user input ?", path);
+            operationLog.warn(message);
+            return null;
+        }
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServer.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServer.java
new file mode 100644
index 00000000000..1687baa36e4
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServer.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import java.util.Properties;
+
+import org.apache.ftpserver.ConnectionConfigFactory;
+import org.apache.ftpserver.FtpServerFactory;
+import org.apache.ftpserver.ftplet.FileSystemFactory;
+import org.apache.ftpserver.ftplet.FileSystemView;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.ftplet.User;
+import org.apache.ftpserver.ftplet.UserManager;
+import org.apache.ftpserver.listener.ListenerFactory;
+import org.apache.ftpserver.ssl.SslConfigurationFactory;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+
+/**
+ * Controls the lifecycle of an FTP server built into DSS.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class FtpServer implements FileSystemFactory
+{
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            FtpServer.class);
+
+    private final IETLLIMSService openBisService;
+
+    private final UserManager userManager;
+
+    private final FtpServerConfig config;
+
+    private final IFtpPathResolverRegistry pathResolverRegistry;
+
+    private org.apache.ftpserver.FtpServer server;
+
+    public FtpServer(IETLLIMSService openBisService, UserManager userManager, Properties configProps)
+            throws Exception
+    {
+        this.openBisService = openBisService;
+        this.userManager = userManager;
+        this.config = new FtpServerConfig(configProps);
+        this.pathResolverRegistry = new FtpPathResolverRegistry(config);
+
+        if (config.isStartServer())
+        {
+            start();
+        }
+    }
+
+    private void start() throws Exception
+    {
+        FtpServerFactory serverFactory = new FtpServerFactory();
+
+        ListenerFactory factory = new ListenerFactory();
+        factory.setPort(config.getPort());
+        if (config.isUseSSL())
+        {
+            SslConfigurationFactory sslConfigFactory = new SslConfigurationFactory();
+            sslConfigFactory.setKeystoreFile(config.getKeyStore());
+            sslConfigFactory.setKeystorePassword(config.getKeyStorePassword());
+            sslConfigFactory.setKeyPassword(config.getKeyPassword());
+            factory.setSslConfiguration(sslConfigFactory.createSslConfiguration());
+        }
+        serverFactory.addListener("default", factory.createListener());
+
+        ConnectionConfigFactory connectionConfigFactory = new ConnectionConfigFactory();
+        connectionConfigFactory.setMaxThreads(config.getMaxThreads());
+        serverFactory.setConnectionConfig(connectionConfigFactory.createConnectionConfig());
+
+        serverFactory.setFileSystem(this);
+        serverFactory.setUserManager(userManager);
+
+        server = serverFactory.createServer();
+
+        String startingMessage =
+                String.format("Starting FTP server on port %d ...", config.getPort());
+        operationLog.info(startingMessage);
+        server.start();
+
+    }
+
+    /**
+     * called by spring IoC container when the application shuts down.
+     */
+    public void stop()
+    {
+        if (server != null)
+        {
+            server.stop();
+        }
+    }
+
+    public FileSystemView createFileSystemView(User user) throws FtpException
+    {
+        if (user instanceof FtpUser)
+        {
+            String sessionToken = ((FtpUser) user).getSessionToken();
+            return new DSSFileSystemView(sessionToken, openBisService, pathResolverRegistry);
+        } else
+        {
+            throw new FtpException("Unsupported user type.");
+        }
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServerConfig.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServerConfig.java
new file mode 100644
index 00000000000..9f3ebd8c765
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpServerConfig.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import java.io.File;
+import java.util.Properties;
+
+import ch.systemsx.cisd.common.utilities.PropertyUtils;
+import ch.systemsx.cisd.openbis.dss.generic.server.ConfigParameters;
+
+/**
+ * @author Kaloyan Enimanev
+ */
+public class FtpServerConfig
+{
+    private final static String PREFIX = "ftp.server.";
+
+    private final static String ENABLE_KEY = PREFIX + "enable";
+
+    private final static String PORT_KEY = PREFIX + "port";
+
+    private final static String USE_SSL_KEY = PREFIX + "use-ssl";
+
+    private final static String MAX_THREADS_KEY = PREFIX + "maxThreads";
+
+    private final static String DATASET_DISPLAY_TEMPLATE_KEY = PREFIX + "dataset.display.template";
+
+    private static final int DEFAULT_PORT = 2121;
+
+    private static final boolean DEFAULT_USE_SSL = false;
+
+    private static final int DEFAULT_MAX_THREADS = 25;
+
+    private static final String DEFAULT_DATASET_TEMPLATE = "${dataSetCode}";
+
+    private boolean startServer;
+
+    private int port;
+
+    private boolean useSSL;
+
+    private File keyStore;
+
+    private String keyPassword;
+
+    private String keyStorePassword;
+
+    private String dataSetDisplayTemplate;
+
+    private int maxThreads;
+
+    public FtpServerConfig(Properties props) {
+        this.startServer = PropertyUtils.getBoolean(props, ENABLE_KEY, false);
+        if (startServer)
+        {
+            initializeProperties(props);
+        }
+    }
+
+    private void initializeProperties(Properties props)
+    {
+        this.port = PropertyUtils.getPosInt(props, PORT_KEY, DEFAULT_PORT);
+        this.useSSL = PropertyUtils.getBoolean(props, USE_SSL_KEY, DEFAULT_USE_SSL);
+        if (useSSL)
+        {
+            initializeSSLProperties(props);
+        }
+        maxThreads = PropertyUtils.getPosInt(props, MAX_THREADS_KEY, DEFAULT_MAX_THREADS);
+        dataSetDisplayTemplate =
+                PropertyUtils.getProperty(props, DATASET_DISPLAY_TEMPLATE_KEY, DEFAULT_DATASET_TEMPLATE);
+    }
+
+    private void initializeSSLProperties(Properties props)
+    {
+        String keyStoreFileName =
+                PropertyUtils.getMandatoryProperty(props, ConfigParameters.KEYSTORE_PATH_KEY);
+        keyStore = new File(keyStoreFileName);
+        keyStorePassword =
+                PropertyUtils.getMandatoryProperty(props, ConfigParameters.KEYSTORE_PASSWORD_KEY);
+        keyPassword =
+                PropertyUtils.getMandatoryProperty(props,
+                        ConfigParameters.KEYSTORE_KEY_PASSWORD_KEY);
+    }
+
+    public boolean isStartServer()
+    {
+        return startServer;
+    }
+
+    public int getPort()
+    {
+        return port;
+    }
+
+    public boolean isUseSSL()
+    {
+        return useSSL;
+    }
+
+    public File getKeyStore()
+    {
+        return keyStore;
+    }
+
+    public String getKeyPassword()
+    {
+        return keyPassword;
+    }
+
+    public String getKeyStorePassword()
+    {
+        return keyStorePassword;
+    }
+
+    public Integer getMaxThreads()
+    {
+        return maxThreads;
+    }
+
+    public String getDataSetDisplayTemplate()
+    {
+        return dataSetDisplayTemplate;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUser.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUser.java
new file mode 100644
index 00000000000..d3fe821cf75
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUser.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.ftpserver.ftplet.Authority;
+import org.apache.ftpserver.ftplet.AuthorizationRequest;
+import org.apache.ftpserver.ftplet.User;
+
+/**
+ * An implementation of the Apache {@link User} interface, additionally holding an authenticated
+ * openBIS session token.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class FtpUser implements User
+{
+    private static final int SECONDS_PER_HOUR = 60 * 60;
+
+    private final String name;
+
+    private final String sessionToken;
+
+    public FtpUser(String userName, String sessionToken)
+    {
+        this.name = userName;
+        this.sessionToken = sessionToken;
+    }
+
+    public boolean getEnabled()
+    {
+        return true;
+    }
+
+    public String getHomeDirectory()
+    {
+        return "/";
+    }
+
+    public int getMaxIdleTime()
+    {
+        return SECONDS_PER_HOUR;
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public String getPassword()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getSessionToken()
+    {
+        return sessionToken;
+    }
+
+    public AuthorizationRequest authorize(AuthorizationRequest request)
+    {
+        // authorization is implemented by not showing the users datasets they cannot see.
+        return request;
+    }
+
+    public List<Authority> getAuthorities()
+    {
+        return Collections.emptyList();
+    }
+
+    public List<Authority> getAuthorities(Class<? extends Authority> arg0)
+    {
+        return Collections.emptyList();
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUserManager.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUserManager.java
new file mode 100644
index 00000000000..40671c09966
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/FtpUserManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import org.apache.ftpserver.ftplet.Authentication;
+import org.apache.ftpserver.ftplet.AuthenticationFailedException;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.ftplet.User;
+import org.apache.ftpserver.ftplet.UserManager;
+import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication;
+import org.apache.log4j.Logger;
+
+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.SessionContextDTO;
+
+/**
+ * An implementation of the Apache {@link UserManager} interface, adapting openBIS users to FTP
+ * users.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class FtpUserManager implements UserManager
+{
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            FtpUserManager.class);
+
+    
+    private final IETLLIMSService service;
+
+    public FtpUserManager(IETLLIMSService service)
+    {
+        this.service = service;
+    }
+
+    public User authenticate(Authentication authentication) throws AuthenticationFailedException
+    {
+        if (authentication instanceof UsernamePasswordAuthentication)
+        {
+            UsernamePasswordAuthentication upa = (UsernamePasswordAuthentication) authentication;
+            String user = upa.getUsername();
+            String password = upa.getPassword();
+            SessionContextDTO session = service.tryToAuthenticate(user, password);
+            if (session != null && session.getSessionToken() != null)
+            {
+                return new FtpUser(user, session.getSessionToken());
+            }
+        } else {
+            operationLog.warn("Unsupported authentication type :" + authentication.getClass());
+        }
+        
+        throw new AuthenticationFailedException();
+    }
+
+    public void delete(String arg0) throws FtpException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean doesExist(String arg0) throws FtpException
+    {
+        return false;
+    }
+
+    public String getAdminName() throws FtpException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getAllUserNames() throws FtpException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public User getUserByName(String userName) throws FtpException
+    {
+        return new FtpUser(userName, null);
+    }
+
+    public boolean isAdmin(String arg0) throws FtpException
+    {
+        return false;
+    }
+
+    public void save(User arg0) throws FtpException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolver.java
new file mode 100644
index 00000000000..d771ada9e38
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolver.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+
+/**
+ * {@link IFtpPathResolver}-s can translate String paths to {@link FtpFile} objects.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public interface IFtpPathResolver
+{
+    /**
+     * @param path a normalized path, containing no trailing slashes.
+     */
+    boolean canResolve(String path);
+
+    /**
+     * @param path a normalized path, containing no trailing slashes.
+     */
+    FtpFile resolve(String path, FtpPathResolverContext resolverContext);
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolverRegistry.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolverRegistry.java
new file mode 100644
index 00000000000..ab79a831eeb
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/IFtpPathResolverRegistry.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 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.ftp;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+
+/**
+ * @author Kaloyan Enimanev
+ */
+public interface IFtpPathResolverRegistry
+{
+
+    FtpFile tryResolve(String path, FtpPathResolverContext resolverContext);
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFile.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFile.java
new file mode 100644
index 00000000000..a02ece45c54
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFile.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+
+/**
+ * A convenience abstract implementation for an ftp folder.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public abstract class AbstractFtpFile implements FtpFile
+{
+    protected final String absolutePath;
+
+    public AbstractFtpFile(String absolutePath)
+    {
+        this.absolutePath = absolutePath;
+    }
+
+    public boolean doesExist()
+    {
+        return true;
+    }
+
+    public String getAbsolutePath()
+    {
+        return absolutePath;
+    }
+
+    public String getName()
+    {
+        return new File(absolutePath).getName();
+    }
+
+    public String getOwnerName()
+    {
+        return FtpConstants.FTP_USER_NAME;
+    }
+
+    public String getGroupName()
+    {
+        return FtpConstants.FTP_USER_GROUP_NAME;
+    }
+
+    public boolean isHidden()
+    {
+        return false;
+    }
+
+    public boolean isReadable()
+    {
+        return true;
+    }
+
+    public boolean isRemovable()
+    {
+        return false;
+    }
+
+    public boolean isWritable()
+    {
+        return false;
+    }
+
+    // =================================
+    // Unsupported operations
+    // =================================
+
+    public OutputStream createOutputStream(long arg0) throws IOException
+    {
+        return null;
+    }
+
+    public boolean delete()
+    {
+        return false;
+    }
+
+    public boolean mkdir()
+    {
+        return false;
+    }
+
+    public boolean move(FtpFile arg0)
+    {
+        return false;
+    }
+
+    public boolean setLastModified(long arg0)
+    {
+        return false;
+    }
+
+    public int getLinkCount()
+    {
+        return 0;
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFolder.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFolder.java
new file mode 100644
index 00000000000..3b567621009
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/AbstractFtpFolder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A convenience abstract implementation for an ftp folder.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public abstract class AbstractFtpFolder extends AbstractFtpFile
+{
+
+    public AbstractFtpFolder(String absolutePath)
+    {
+        super(absolutePath);
+    }
+
+    public boolean isDirectory()
+    {
+        return true;
+    }
+
+    public boolean isFile()
+    {
+        return false;
+    }
+
+    public InputStream createInputStream(long arg0) throws IOException
+    {
+        return null;
+    }
+
+    public long getSize()
+    {
+        return 0;
+    }
+
+    public long getLastModified()
+    {
+        return System.currentTimeMillis();
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ExperimentFolderResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ExperimentFolderResolver.java
new file mode 100644
index 00000000000..4741ea8c53b
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ExperimentFolderResolver.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpPathResolverContext;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.IFtpPathResolver;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory;
+
+/**
+ * Resolves experiment folders with path "/<space-code>/<project-code>/<experiment-code>" to
+ * {@link FtpFile}-s.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class ExperimentFolderResolver implements IFtpPathResolver
+{
+    private final IExperimentChildrenLister childLister;
+
+    public ExperimentFolderResolver(IExperimentChildrenLister childLister)
+    {
+        this.childLister = childLister;
+    }
+
+    /**
+     * @return <code>true</code> for all paths containing 3 levels of nested folders,
+     *         <code>false</code> for all other paths.
+     */
+    public boolean canResolve(String path)
+    {
+        int nestedLevels = StringUtils.countMatches(path, FtpConstants.FILE_SEPARATOR);
+        return nestedLevels == 3;
+    }
+
+    public FtpFile resolve(final String path, final FtpPathResolverContext resolverContext)
+    {
+        return new AbstractFtpFolder(path)
+            {
+
+                public List<FtpFile> listFiles()
+                {
+                    List<FtpFile> result = new ArrayList<FtpFile>();
+
+                    for (String childName : listChildrenNames(path, resolverContext))
+                    {
+                        String childPath = path + FtpConstants.FILE_SEPARATOR + childName;
+                        FtpFile childFile =
+                                resolverContext.getResolverRegistry().tryResolve(childPath,
+                                        resolverContext);
+                        result.add(childFile);
+                    }
+
+                    return result;
+                }
+
+            };
+    }
+
+    private List<String> listChildrenNames(String expIdentifier, FtpPathResolverContext context)
+    {
+        IETLLIMSService service = context.getService();
+        String sessionToken = context.getSessionToken();
+
+        ExperimentIdentifier identifier =
+                new ExperimentIdentifierFactory(expIdentifier).createIdentifier();
+
+        Experiment exp = service.tryToGetExperiment(sessionToken, identifier);
+        if (exp == null)
+        {
+            return Collections.emptyList();
+        } else
+        {
+            return childLister.listExperimentChildrenPaths(exp, context);
+        }
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/HierarchicalContentToFtpFileAdapter.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/HierarchicalContentToFtpFileAdapter.java
new file mode 100644
index 00000000000..9f3c3e6c346
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/HierarchicalContentToFtpFileAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.common.io.IHierarchicalContentNode;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+
+/**
+ * An {@link FtpFile} implementation adapting an underlying {@link IHierarchicalContentNode}.
+ * <p>
+ * The resources represented by {@link HierarchicalContentToFtpFileAdapter} exist in the data store.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class HierarchicalContentToFtpFileAdapter extends AbstractFtpFile
+{
+    private final IHierarchicalContentNode contentNode;
+
+    public HierarchicalContentToFtpFileAdapter(String path, IHierarchicalContentNode contentNode)
+    {
+        super(path);
+        this.contentNode = contentNode;
+    }
+
+    public InputStream createInputStream(long offset) throws IOException
+    {
+        InputStream result = contentNode.getInputStream();
+        if (offset > 0)
+        {
+            result.skip(offset);
+        }
+        return result;
+    }
+
+    public long getLastModified()
+    {
+        try
+        {
+            return contentNode.getFile().lastModified();
+        } catch (UnsupportedOperationException uoe)
+        {
+            return 0;
+        }
+    }
+
+
+    public long getSize()
+    {
+        if (isFile())
+        {
+            return contentNode.getFileLength();
+        }
+        return 0;
+    }
+
+    public boolean isDirectory()
+    {
+        return contentNode.isDirectory();
+    }
+
+    public boolean isFile()
+    {
+        return isDirectory() == false;
+    }
+
+    public List<org.apache.ftpserver.ftplet.FtpFile> listFiles()
+    {
+        if (isDirectory())
+        {
+            List<IHierarchicalContentNode> children = contentNode.getChildNodes();
+            List<org.apache.ftpserver.ftplet.FtpFile> result =
+                    new ArrayList<org.apache.ftpserver.ftplet.FtpFile>();
+            for (IHierarchicalContentNode childNode : children)
+            {
+                String childPath = absolutePath + FtpConstants.FILE_SEPARATOR + childNode.getName();
+                HierarchicalContentToFtpFileAdapter childFile =
+                        new HierarchicalContentToFtpFileAdapter(childPath, childNode);
+                result.add(childFile);
+            }
+            return result;
+
+        } else
+        {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/IExperimentChildrenLister.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/IExperimentChildrenLister.java
new file mode 100644
index 00000000000..f0df672104a
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/IExperimentChildrenLister.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpPathResolverContext;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+
+/**
+ * @author Kaloyan Enimanev
+ */
+public interface IExperimentChildrenLister
+{
+    List<String> listExperimentChildrenPaths(Experiment experiment, FtpPathResolverContext context);
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ProjectFolderResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ProjectFolderResolver.java
new file mode 100644
index 00000000000..846dc597107
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/ProjectFolderResolver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpPathResolverContext;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.IFtpPathResolver;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifierFactory;
+
+/**
+ * Resolves project folders with path "/<space-code>/<project-code>" to {@link FtpFile}-s.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class ProjectFolderResolver implements IFtpPathResolver
+{
+
+    /**
+     * @return <code>true</code> for all paths containing 2 levels of nested folders,
+     *         <code>false</code> for all other paths.
+     */
+    public boolean canResolve(String path)
+    {
+        int nestedLevels = StringUtils.countMatches(path, FtpConstants.FILE_SEPARATOR);
+        return nestedLevels == 2;
+    }
+
+    public FtpFile resolve(final String path, final FtpPathResolverContext resolverContext)
+    {
+        return new AbstractFtpFolder(path)
+            {
+
+                public List<FtpFile> listFiles()
+                {
+                    List<Experiment> experiments = listExperiments(path, resolverContext);
+                    List<FtpFile> result = new ArrayList<FtpFile>();
+
+                    for (Experiment experiment : experiments)
+                    {
+                        String childPath =
+                                path + FtpConstants.FILE_SEPARATOR + experiment.getCode();
+                        FtpFile childFile =
+                                resolverContext.getResolverRegistry().tryResolve(childPath,
+                                        resolverContext);
+                        result.add(childFile);
+                    }
+
+                    return result;
+                }
+            };
+    }
+
+    private List<Experiment> listExperiments(String projectIdentifier,
+            FtpPathResolverContext context)
+    {
+        IETLLIMSService service = context.getService();
+        String sessionToken = context.getSessionToken();
+        ProjectIdentifier identifier =
+                new ProjectIdentifierFactory(projectIdentifier).createIdentifier();
+        return service.listExperiments(sessionToken, identifier);
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/RootFolderResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/RootFolderResolver.java
new file mode 100644
index 00000000000..90697ee047c
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/RootFolderResolver.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpPathResolverContext;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.IFtpPathResolver;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Project;
+
+/**
+ * Creates a root folder {@link FtpFile} listing all existing spaces as its children.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class RootFolderResolver implements IFtpPathResolver
+{
+
+    public boolean canResolve(String path)
+    {
+        return FtpConstants.ROOT_DIRECTORY.equals(path);
+    }
+
+    public FtpFile resolve(String path, final FtpPathResolverContext resolverContext)
+    {
+        return new AbstractFtpFolder(path)
+            {
+
+                public List<FtpFile> listFiles()
+                {
+
+                    List<Project> projects = listProjects(resolverContext);
+                    List<FtpFile> result = new ArrayList<FtpFile>();
+                    Set<String> spacesSeen = new TreeSet<String>();
+
+                    for (Project project : projects)
+                    {
+                        String spaceCode = project.getSpace().getCode();
+                        spacesSeen.add(spaceCode);
+                    }
+
+                    for (String spaceCode : spacesSeen)
+                    {
+                        String childPath = FtpConstants.ROOT_DIRECTORY + spaceCode;
+                        FtpFile child =
+                                resolverContext.getResolverRegistry().tryResolve(childPath,
+                                        resolverContext);
+                        if (child != null)
+                        {
+                            result.add(child);
+                        }
+                    }
+
+                    return result;
+                }
+
+                private List<Project> listProjects(FtpPathResolverContext context)
+                {
+                    IETLLIMSService service = context.getService();
+                    String sessionToken = context.getSessionToken();
+                    List<Project> projects = service.listProjects(sessionToken);
+                    return projects;
+                }
+            };
+    }
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/SpaceFolderResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/SpaceFolderResolver.java
new file mode 100644
index 00000000000..edbefcbdaa1
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/SpaceFolderResolver.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpPathResolverContext;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.IFtpPathResolver;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Project;
+
+/**
+ * Resolves experiment folders with path "/<space-code>" to {@link FtpFile}-s.
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class SpaceFolderResolver implements IFtpPathResolver
+{
+
+    /**
+     * @return <code>true</code> for all paths containing single folder, <code>false</code> for all
+     *         other paths.
+     */
+    public boolean canResolve(String path)
+    {
+        return false == removeStartingSlash(path).contains(FtpConstants.FILE_SEPARATOR);
+    }
+
+    public FtpFile resolve(final String path, final FtpPathResolverContext resolverContext)
+    {
+        return new AbstractFtpFolder(path)
+            {
+
+                public List<FtpFile> listFiles()
+                {
+                    List<Project> projects = listProjects(resolverContext);
+
+                    String pathSpaceCode = removeStartingSlash(path);
+                    List<FtpFile> result = new ArrayList<FtpFile>();
+                    List<String> childProjects = new ArrayList<String>();
+
+                    for (Project project : projects)
+                    {
+                        String projectSpaceCode = project.getSpace().getCode();
+                        if (projectSpaceCode.equals(pathSpaceCode))
+                        {
+                            childProjects.add(project.getCode());
+                        }
+                    }
+
+                    for (String childProject : childProjects)
+                    {
+                        String childPath = path + FtpConstants.FILE_SEPARATOR + childProject;
+                        FtpFile childFile =
+                                resolverContext.getResolverRegistry().tryResolve(childPath,
+                                        resolverContext);
+                        result.add(childFile);
+                    }
+
+                    return result;
+                }
+
+            };
+    }
+
+    private List<Project> listProjects(FtpPathResolverContext context)
+    {
+        IETLLIMSService service = context.getService();
+        String sessionToken = context.getSessionToken();
+        List<Project> projects = service.listProjects(sessionToken);
+        return projects;
+    }
+
+    private String removeStartingSlash(String path)
+    {
+        if (path.startsWith(FtpConstants.FILE_SEPARATOR))
+        {
+            return path.substring(1);
+        } else
+        {
+            return path;
+        }
+    }
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/TemplateBasedDataSetResourceResolver.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/TemplateBasedDataSetResourceResolver.java
new file mode 100644
index 00000000000..3cd9de5630b
--- /dev/null
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/ftp/resolver/TemplateBasedDataSetResourceResolver.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2011 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.ftp.resolver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.apache.ftpserver.ftplet.FtpFile;
+
+import ch.systemsx.cisd.common.io.IHierarchicalContent;
+import ch.systemsx.cisd.common.io.IHierarchicalContentNode;
+import ch.systemsx.cisd.common.utilities.ExtendedProperties;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpConstants;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpPathResolverContext;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpServerConfig;
+import ch.systemsx.cisd.openbis.dss.generic.server.ftp.IFtpPathResolver;
+import ch.systemsx.cisd.openbis.dss.generic.shared.IHierarchicalContentProvider;
+import ch.systemsx.cisd.openbis.dss.generic.shared.ServiceProvider;
+import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExternalData;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory;
+
+/**
+ * Resolves paths like
+ * /<space-code>/<project-code>/<experiment-code>/<dataset-template>[/<sub-path>]*
+ * <p>
+ * Subpaths are resolved as a relative paths starting from the root of a dataset.
+ * <p>
+ * TODO KE: add disambiguation checks - append an uniqueness factor to (?another variable) to the
+ * path if no "dataSetCode" variable is used in the template
+ * 
+ * @author Kaloyan Enimanev
+ */
+public class TemplateBasedDataSetResourceResolver implements IFtpPathResolver, IExperimentChildrenLister
+{
+    private static final String DATA_SET_CODE_VARNAME = "dataSetCode";
+
+    private static final String DATA_SET_TYPE_VARNAME = "dataSetType";
+
+    private static final String DATA_SET_DATE_VARNAME = "dataSetDate";
+
+    private static final String FILE_NAME_VARNAME = "fileName";
+
+    private static final String DATA_SET_DATE_FORMAT = "yyyy-MM-dd-HH-mm";
+
+    /**
+     * a template, that can contain special variables.
+     * 
+     * @see #evaluateTemplate(ExternalData, String) to find out what variables are understood and
+     *      interpreted.
+     */
+    private final String template;
+
+    private static class DataSetAndFileName
+    {
+        ExternalData dataSet;
+
+        // will only be filled when the ${fileName} variable
+        // is used in the template
+        String fileName = StringUtils.EMPTY;
+    }
+
+    public TemplateBasedDataSetResourceResolver(FtpServerConfig ftpServerConfig)
+    {
+        this.template = ftpServerConfig.getDataSetDisplayTemplate();
+    }
+
+    /**
+     * @return <code>true</code> for paths containing at least 4 nested directory levels.
+     */
+    public boolean canResolve(String path)
+    {
+        int nestedLevels = StringUtils.countMatches(path, FtpConstants.FILE_SEPARATOR);
+        return nestedLevels >= 4;
+    }
+
+    public FtpFile resolve(String path, final FtpPathResolverContext resolverContext)
+    {
+        IETLLIMSService service = resolverContext.getService();
+        String sessionToken = resolverContext.getSessionToken();
+
+        DataSetAndFileName dataSetAndFileName =
+                extractDataSetAndFileName(path, service, sessionToken);
+        if (dataSetAndFileName == null)
+        {
+            return null;
+        }
+        
+        String nestedSubPath = extractNestedSubPath(path);
+        String relativePath =
+                dataSetAndFileName.fileName + FtpConstants.FILE_SEPARATOR + nestedSubPath;
+
+        IHierarchicalContentProvider provider = ServiceProvider.getHierarchicalContentProvider();
+        IHierarchicalContent content =
+                provider.asContent(dataSetAndFileName.dataSet.getDataSetCode());
+        IHierarchicalContentNode contentNode =
+                StringUtils.isBlank(relativePath) ? content.getRootNode() : content
+                        .getNode(relativePath);
+        return new HierarchicalContentToFtpFileAdapter(path, contentNode);
+    }
+
+    private DataSetAndFileName extractDataSetAndFileName(String path, IETLLIMSService service,
+            String sessionToken)
+    {
+        String experimentId = extractExperimentIdentifier(path);
+        Experiment experiment = extractExperiment(experimentId, service, sessionToken);
+        if (experiment == null)
+        {
+            // cannot resolve an existing experiment from the specified path
+            return null;
+        }
+        List<ExternalData> dataSets =
+                service.listDataSetsByExperimentID(sessionToken, new TechId(experiment));
+        String pathWithEndSlash = path + FtpConstants.FILE_SEPARATOR;
+        for (ExternalData dataSet : dataSets)
+        {
+            Map<String /* fileName */, String /* evaluated */> evaluatedPaths =
+                    getEvaluatedDataSetPaths(dataSet);
+            for (Entry<String, String> entry : evaluatedPaths.entrySet())
+            {
+                String fullEvaluatedPath =
+                        experimentId + FtpConstants.FILE_SEPARATOR + entry.getValue()
+                                + FtpConstants.FILE_SEPARATOR;
+                if (pathWithEndSlash.startsWith(fullEvaluatedPath))
+                {
+                    DataSetAndFileName result = new DataSetAndFileName();
+                    result.dataSet = dataSet;
+                    result.fileName = entry.getKey();
+                    return result;
+                }
+            }
+
+        }
+
+        return null;
+    }
+
+    /**
+     * @return the nested path within the data set (i.e. under the dataset directory level)
+     */
+    private String extractNestedSubPath(String path)
+    {
+        String[] levels = StringUtils.split(path, FtpConstants.FILE_SEPARATOR);
+        if (levels.length > 4) {
+            return StringUtils.join(levels, FtpConstants.FILE_SEPARATOR, 4, levels.length);
+        } else {
+            return StringUtils.EMPTY;
+        }
+    }
+
+    private Experiment extractExperiment(String experimentId, IETLLIMSService service,
+            String sessionToken)
+    {
+        ExperimentIdentifier experimentIdentifier =
+                new ExperimentIdentifierFactory(experimentId).createIdentifier();
+
+        return service.tryToGetExperiment(sessionToken, experimentIdentifier);
+    }
+
+    private String extractExperimentIdentifier(String path)
+    {
+        String[] levels = StringUtils.split(path, FtpConstants.FILE_SEPARATOR);
+        String experimentId =
+                FtpConstants.ROOT_DIRECTORY
+                        + StringUtils.join(levels, FtpConstants.FILE_SEPARATOR, 0, 3);
+        return experimentId;
+    }
+
+    public List<String> listExperimentChildrenPaths(Experiment experiment, FtpPathResolverContext context)
+    {
+        IETLLIMSService service = context.getService();
+        String sessionToken = context.getSessionToken();
+
+        List<ExternalData> dataSets = service.listDataSetsByExperimentID(sessionToken, new TechId(experiment));
+        List<String> result = new ArrayList<String>();
+        for (ExternalData dataSet : dataSets)
+        {
+            Map<String, String> pathsForDataSet = getEvaluatedDataSetPaths(dataSet);
+            result.addAll(pathsForDataSet.values());
+        }
+        return result;
+    }
+
+    private Map<String /* fileName */, String /* evaluated template */> getEvaluatedDataSetPaths(
+            ExternalData dataSet)
+    {
+        Map<String, String> result = new HashMap<String, String>();
+        
+        IHierarchicalContentProvider provider = ServiceProvider.getHierarchicalContentProvider();
+        IHierarchicalContent content = provider.asContent(dataSet.getCode());
+        IHierarchicalContentNode rootNode = content.getRootNode();
+
+        for (String fileName : extractFileNames(rootNode))
+        {
+            String path = evaluateTemplate(dataSet, fileName);
+            result.put(fileName, path);
+        }
+        
+        return result;
+    }
+
+    /**
+     * @return all values to be used when evaluating the template "${fileName}" variable.
+     */
+    private List<String> extractFileNames(IHierarchicalContentNode rootNode)
+    {
+        if (rootNode.isDirectory())
+        {
+            List<String> childrenFileNames = new ArrayList<String>();
+            for (IHierarchicalContentNode child : rootNode.getChildNodes())
+            {
+                childrenFileNames.add(child.getName());
+            }
+            return childrenFileNames;
+        } else
+        {
+            return Collections.singletonList(StringUtils.EMPTY);
+        }
+    }
+
+    public String evaluateTemplate(ExternalData dataSet,
+            String fileName)
+    {
+        ExtendedProperties properties = new ExtendedProperties();
+        properties.put(DATA_SET_CODE_VARNAME, dataSet.getCode());
+        properties.put(DATA_SET_TYPE_VARNAME, dataSet.getDataSetType().getCode());
+        String dataSetDate = extractDateValue(dataSet.getRegistrationDate());
+        properties.put(DATA_SET_DATE_VARNAME, dataSetDate);
+        properties.put(FILE_NAME_VARNAME, fileName);
+
+        String templatePropName = "template";
+        properties.put(templatePropName, template);
+        return properties.getProperty(templatePropName);
+    }
+
+
+    private String extractDateValue(Date dataSetDate)
+    {
+        return DateFormatUtils.format(dataSetDate, DATA_SET_DATE_FORMAT);
+    }
+
+
+}
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/webdav/.gitignore b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/webdav/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java
index 002916febdb..8acc1bc783d 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/ServiceProvider.java
@@ -60,8 +60,14 @@ public class ServiceProvider
     {
         if (create && applicationContext == null)
         {
-            applicationContext = new ClassPathXmlApplicationContext(new String[]
-                { "dssApplicationContext.xml" }, true);
+            synchronized (ServiceProvider.class)
+            {
+                if (applicationContext == null)
+                {
+                    applicationContext = new ClassPathXmlApplicationContext(new String[]
+                        { "dssApplicationContext.xml" }, true);
+                }
+            }
         }
         return applicationContext;
     }
diff --git a/datastore_server/source/java/dssApplicationContext.xml b/datastore_server/source/java/dssApplicationContext.xml
index 370deea1160..258a04722f9 100644
--- a/datastore_server/source/java/dssApplicationContext.xml
+++ b/datastore_server/source/java/dssApplicationContext.xml
@@ -18,6 +18,10 @@
         <property name="ignoreUnresolvablePlaceholders" value="true" />
     </bean>
     
+    <bean id="configProperties" factory-bean="propertyConfigurer"   
+        factory-method="getResolvedProps">
+    </bean>
+    
     <bean id="data-set-path-infos-provider" class="ch.systemsx.cisd.openbis.dss.generic.server.DatabaseBasedDataSetPathInfoProvider"/>
 
   <bean id="plugin-tasks" class="ch.systemsx.cisd.openbis.dss.generic.server.plugins.tasks.PluginTaskProviders"
@@ -142,5 +146,23 @@
     -->
 
     <bean class="ch.systemsx.cisd.common.spring.LogAdvisor" />
+  
+    <!-- 
+        // FTP server
+     -->
+    
+    <!-- Adapts the openBIS users for the FTP server -->
+    <bean id="adapted-ftp-user-manager" class="ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpUserManager">
+        <constructor-arg ref="etl-lims-service"/>
+    </bean>
+
+    <!-- 
+        Optionally starts an FTP server.
+    -->
+    <bean id="ftp-server" class="ch.systemsx.cisd.openbis.dss.generic.server.ftp.FtpServer" destroy-method="stop">
+        <constructor-arg ref="etl-lims-service"/>
+        <constructor-arg ref="adapted-ftp-user-manager"/>
+        <constructor-arg ref="configProperties" />
+    </bean>
     
 </beans>
\ No newline at end of file
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
index c8657c640d4..43877b3dcb2 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLService.java
@@ -35,6 +35,7 @@ import ch.systemsx.cisd.openbis.generic.server.business.IDataStoreServiceFactory
 import ch.systemsx.cisd.openbis.generic.server.business.IPropertiesBatchManager;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.ICommonBusinessObjectFactory;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.IExperimentBO;
+import ch.systemsx.cisd.openbis.generic.server.business.bo.IExperimentTable;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.IExternalDataBO;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.IExternalDataTable;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.IGroupBO;
@@ -62,6 +63,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataStoreServiceKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseInstance;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatastoreServiceDescription;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DeletedDataSet;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityType;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentType;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExternalData;
@@ -528,6 +530,26 @@ public class ETLService extends AbstractCommonServer<IETLService> implements IET
         return datasetLister.listByDatasetCode(dataSetCodes);
     }
 
+    public List<Project> listProjects(String sessionToken)
+    {
+        checkSession(sessionToken);
+        final List<ProjectPE> projects = getDAOFactory().getProjectDAO().listProjects();
+        Collections.sort(projects);
+        return ProjectTranslator.translate(projects);
+    }
+
+    public List<Experiment> listExperiments(String sessionToken, ProjectIdentifier projectIdentifier)
+    {
+        final Session session = getSession(sessionToken);
+        final IExperimentTable experimentTable =
+                businessObjectFactory.createExperimentTable(session);
+        experimentTable.load(EntityType.ALL_TYPES_CODE, projectIdentifier);
+        final List<ExperimentPE> experiments = experimentTable.getExperiments();
+        Collections.sort(experiments);
+        return ExperimentTranslator.translate(experiments, session.getBaseIndexURL(),
+                ExperimentTranslator.LoadableFields.PROPERTIES);
+    }
+
     public IEntityProperty[] tryToGetPropertiesOfTopSampleRegisteredFor(String sessionToken,
             SampleIdentifier sampleIdentifier) throws UserFailureException
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
index fcbcbb36ca8..31c61ec419a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/ETLServiceLogger.java
@@ -463,4 +463,16 @@ public class ETLServiceLogger extends AbstractServerLogger implements IETLServic
         return null;
     }
 
+    public List<Experiment> listExperiments(String sessionToken, ProjectIdentifier projectIdentifier)
+    {
+        logAccess(sessionToken, "listExperiments", "%s", projectIdentifier);
+        return null;
+    }
+
+    public List<Project> listProjects(String sessionToken)
+    {
+        logAccess(sessionToken, "listProjects");
+        return null;
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
index 1c8539c0c2d..5c887f26405 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/IETLLIMSService.java
@@ -389,7 +389,8 @@ public interface IETLLIMSService extends IServer, ISessionProvider
             throws UserFailureException;
 
     /**
-     * Creates and returns a unique code for a new data set.
+     * Creates and returns a unique code for a new data set. TODO KE: 2011-04-19 remove this method.
+     * It is equivalent to "createPermId()"
      */
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
@@ -459,6 +460,24 @@ public interface IETLLIMSService extends IServer, ISessionProvider
     @Transactional(readOnly = true)
     @RolesAllowed(RoleWithHierarchy.SPACE_ETL_SERVER)
     public List<ExternalData> listDataSets(String sessionToken, String dataStoreCode, TrackingDataSetCriteria criteria);
+
+    /**
+     * List all experiments for a given project identifier.
+     */
+    @Transactional(readOnly = true)
+    @RolesAllowed(value =
+        { RoleWithHierarchy.SPACE_OBSERVER })
+    public List<Experiment> listExperiments(
+            String sessionToken,
+            @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class) ProjectIdentifier projectIdentifier);
+
+    /**
+     * List all projects that the user can see.
+     */
+    @Transactional(readOnly = true)
+    @RolesAllowed(value =
+        { RoleWithHierarchy.SPACE_OBSERVER })
+    public List<Project> listProjects(String sessionToken);
     
     /**
      * Adds specified properties of given data set. Properties defined before will not be updated.
@@ -656,5 +675,4 @@ public interface IETLLIMSService extends IServer, ISessionProvider
     public Project tryGetProject(
             String sessionToken,
             @AuthorizationGuard(guardClass = ExistingSpaceIdentifierPredicate.class) ProjectIdentifier projectIdentifier);
-
 }
-- 
GitLab