diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java
index 50a3642462e5464aaf62625acb1b36a1482c5fe5..bd1ec54d8cec68848cb6ffe19e7017edaf66d141 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/IApplicationServerApi.java
@@ -75,6 +75,9 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentSear
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentTypeSearchCriteria;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentTypeUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentUpdate;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.ExportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.ExternalDms;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.create.ExternalDmsCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.delete.ExternalDmsDeletionOptions;
@@ -2261,6 +2264,6 @@ public interface IApplicationServerApi extends IRpcService
 
     public void executeImport(String sessionToken, IImportData importData, ImportOptions importOptions);
 
-//    public ExportResult executeExport(String sessionToken, ExportData exportData, ExportOptions exportOptions);
+    public ExportResult executeExport(String sessionToken, ExportData exportData, ExportOptions exportOptions);
 
 }
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/ExportOperation.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/ExportOperation.java
new file mode 100644
index 0000000000000000000000000000000000000000..dde0e692ed685aeff446be4245ba581a36b7ff56
--- /dev/null
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/ExportOperation.java
@@ -0,0 +1,90 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.ObjectToString;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.operation.IOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.ExportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportOptions;
+import ch.systemsx.cisd.base.annotation.JsonObject;
+
+@JsonObject("as.dto.exporter.ExportOperation")
+public class ExportOperation implements Serializable, IOperation
+{
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty
+    private ExportData exportData;
+
+    @JsonProperty
+    private ExportOptions exportOptions;
+
+    @SuppressWarnings("unused")
+    public ExportOperation()
+    {
+    }
+
+    public ExportOperation(final ExportData exportData, final ExportOptions exportOptions)
+    {
+        this.exportData = exportData;
+        this.exportOptions = exportOptions;
+    }
+
+    @Override
+    public String getMessage()
+    {
+        return toString();
+    }
+
+    @JsonIgnore
+    public ExportData getExportData()
+    {
+        return exportData;
+    }
+
+    @JsonIgnore
+    public void setExportData(final ExportData exportData)
+    {
+        this.exportData = exportData;
+    }
+
+    @JsonIgnore
+    public ExportOptions getExportOptions()
+    {
+        return exportOptions;
+    }
+
+    @JsonIgnore
+    public void setExportOptions(final ExportOptions exportOptions)
+    {
+        this.exportOptions = exportOptions;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ObjectToString(this).append("exportData", exportData).append("exportOptions", exportOptions).toString();
+    }
+
+}
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/ExportOperationResult.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/ExportOperationResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2a86cb7f60102452af62b93c5b256a28b332130
--- /dev/null
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/ExportOperationResult.java
@@ -0,0 +1,64 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.operation.IOperationResult;
+import ch.systemsx.cisd.base.annotation.JsonObject;
+
+@JsonObject("as.dto.exporter.ExportOperationResult")
+public class ExportOperationResult implements Serializable, IOperationResult
+{
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty
+    private ExportResult exportResult;
+
+    public ExportOperationResult()
+    {
+    }
+
+    public ExportOperationResult(final ExportResult exportResult)
+    {
+        this.exportResult = exportResult;
+    }
+
+    @JsonIgnore
+    public ExportResult getExportResult()
+    {
+        return exportResult;
+    }
+
+    @Override
+    public String getMessage()
+    {
+        return toString();
+    }
+
+    @Override
+    public String toString()
+    {
+        return this.getClass().getSimpleName();
+    }
+
+}
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/data/Attribute.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/data/Attribute.java
index 5b5828e5b37c4fa8c1691d192332b03096180484..9539425e10207be472e15d9d0f68aa75c25d0493 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/data/Attribute.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/data/Attribute.java
@@ -22,10 +22,70 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
 public enum Attribute
 {
 
+    ARCHIVING_STATUS,
+
+    AUTO_GENERATE_CODES,
+
+    AUTO_GENERATE_CODE,
+
+    CHILDREN,
+
+    CODE,
+
+    DESCRIPTION,
+
+    DISALLOW_DELETION,
+
+    EXPERIMENT,
+
+    GENERATE_CODES,
+
+    GENERATED_CODE_PREFIX,
+
+    IDENTIFIER,
+
+    LABEL,
+
+    MAIN_DATA_SET_PATH,
+
+    MAIN_DATA_SET_PATTERN,
+
+    MODIFICATION_DATE,
+
+    MODIFIER,
+
+    ONTOLOGY_ID,
+
+    ONTOLOGY_VERSION,
+
+    ONTOLOGY_ANNOTATION_ID,
+
+    PARENTS,
+
+    PERM_ID,
+
+    PRESENT_IN_ARCHIVE,
+
+    PROJECT,
+
+    REGISTRATION_DATE,
+
+    REGISTRATOR,
+
+    SIZE,
+
+    SAMPLE,
+
     SPACE,
 
-    SAMPLE_TYPE,
+    STORAGE_CONFIRMATION,
+
+    UNIQUE_SUBCODES,
+
+    URL_TEMPLATE,
+
+    VALIDATION_SCRIPT,
 
-    EXPERIMENT_TYPE,
+    VERSION
 
 }
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/options/ExportFormat.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/options/ExportFormat.java
index 23e2fefd4cf0704ee945583514e92c629957431b..64d679274c1a3ceff76ee5f12641e485d7057619 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/options/ExportFormat.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/exporter/options/ExportFormat.java
@@ -22,10 +22,12 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
 public enum ExportFormat
 {
 
-    XLS,
+    XLSX,
 
     PDF,
 
+    HTML,
+
     DATA
 
 }
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/ExportOperation.js b/api-openbis-javascript/src/v3/as/dto/exporter/ExportOperation.js
new file mode 100644
index 0000000000000000000000000000000000000000..d66b58e30002c482dd288f3233256825450d6adc
--- /dev/null
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/ExportOperation.js
@@ -0,0 +1,55 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  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.
+ *
+ */
+
+define(["stjs", "as/dto/common/operation/IOperation"],
+  function (stjs, IOperation) {
+    var ExportOperation = function(exportData, exportOptions) {
+      this.exportData = exportData;
+      this.exportOptions = exportOptions;
+    }
+
+    stjs.extend(
+      ExportOperation,
+      IOperation,
+      [IOperation],
+      function (constructor, prototype) {
+        prototype["@type"] = "as.dto.exporter.ExportOperation";
+
+        constructor.serialVersionUID = 1;
+        prototype.exportData = null;
+        prototype.exportOptions = null;
+
+        prototype.getMessage = function() {
+          return "ExportOperation";
+        };
+
+        prototype.getExportData = function() {
+          return this.exportData;
+        };
+
+        prototype.getExportOptions = function() {
+          return this.exportOptions;
+        };
+      },
+      {
+        exportData: "ExportData",
+        exportOptions: "ExportOptions"
+      }
+    );
+
+    return ExportOperation;
+  });
\ No newline at end of file
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/ExportOperationResult.js b/api-openbis-javascript/src/v3/as/dto/exporter/ExportOperationResult.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2035e175022e6877105ab7cb32c4bdb06d12768
--- /dev/null
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/ExportOperationResult.js
@@ -0,0 +1,49 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  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.
+ *
+ */
+
+define(["stjs", "as/dto/common/operation/IOperationResult"],
+  function (stjs, IOperationResult) {
+    var ExportOperationResult = function(exportResult) {
+      this.exportResult = exportResult;
+    }
+
+    stjs.extend(
+      ExportOperationResult,
+      IOperationResult,
+      [IOperationResult],
+      function (constructor, prototype) {
+        prototype["@type"] = "as.dto.exporter.ExportOperationResult";
+
+        constructor.serialVersionUID = 1;
+
+        prototype.exportResult = null;
+
+        prototype.getMessage = function() {
+          return "ExportOperationResult";
+        };
+
+        prototype.getExportResult = function() {
+          return this.exportResult;
+        }
+      },
+      {
+        exportResult: "ExportResult"
+      }
+    );
+
+    return ExportOperationResult;
+  });
\ No newline at end of file
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/data/Attribute.js b/api-openbis-javascript/src/v3/as/dto/exporter/data/Attribute.js
index 3314038e7d360f76689d01a9776d8d64c0c54dd5..9c85ba3bc40903146ae98757969df9e5d0b089f4 100644
--- a/api-openbis-javascript/src/v3/as/dto/exporter/data/Attribute.js
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/data/Attribute.js
@@ -17,7 +17,41 @@
 
 define(["stjs", "as/dto/common/Enum"], function (stjs, Enum) {
   var Attribute = function() {
-    Enum.call(this, ["SPACE", "SAMPLE_TYPE", "EXPERIMENT_TYPE"]);
+    Enum.call(this, [
+      "ARCHIVING_STATUS",
+      "AUTO_GENERATE_CODES",
+      "AUTO_GENERATE_CODE",
+      "CHILDREN",
+      "CODE",
+      "DESCRIPTION",
+      "DISALLOW_DELETION",
+      "EXPERIMENT",
+      "GENERATE_CODES",
+      "GENERATED_CODE_PREFIX",
+      "IDENTIFIER",
+      "LABEL",
+      "MAIN_DATA_SET_PATH",
+      "MAIN_DATA_SET_PATTERN",
+      "MODIFICATION_DATE",
+      "MODIFIER",
+      "ONTOLOGY_ID",
+      "ONTOLOGY_VERSION",
+      "ONTOLOGY_ANNOTATION_ID",
+      "PARENTS",
+      "PERM_ID",
+      "PRESENT_IN_ARCHIVE",
+      "PROJECT",
+      "REGISTRATION_DATE",
+      "REGISTRATOR",
+      "SIZE",
+      "SAMPLE",
+      "SPACE",
+      "STORAGE_CONFIRMATION",
+      "UNIQUE_SUBCODES",
+      "URL_TEMPLATE",
+      "VALIDATION_SCRIPT",
+      "VERSION"
+    ]);
   }
 
   stjs.extend(
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportData.js b/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportData.js
index 81ad031587966c1c54d217b31bb035dc1547e7e1..1ced9cc1096125bf38e4db64fec3c9965475a5bb 100644
--- a/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportData.js
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportData.js
@@ -16,7 +16,9 @@
  */
 
 define(["stjs"], function (stjs) {
-  var ExportData = function() {
+  var ExportData = function(permIds, fields) {
+    this.permIds = permIds;
+    this.fields = fields;
   }
 
   stjs.extend(
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportablePermId.js b/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportablePermId.js
index df6affab0faf7dadc9e7ad9cedf8c80e2e3bd6d6..4a0569dca3d067f42745c9960ed48fb5cf2b4991 100644
--- a/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportablePermId.js
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/data/ExportablePermId.js
@@ -16,7 +16,9 @@
  */
 
 define(["stjs"], function (stjs) {
-  var ExportablePermId = function() {
+  var ExportablePermId = function(exportableKind, permId) {
+    this.exportableKind = exportableKind;
+    this.permId = permId;
   }
 
   stjs.extend(
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportFormat.js b/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportFormat.js
index 75baed9a5c4b1fbc873e3c2802f07b724e75d9ba..3c45af05f3d2948b427aa4c1bfd0c2f62393205a 100644
--- a/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportFormat.js
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportFormat.js
@@ -17,7 +17,7 @@
 
 define(["stjs", "as/dto/common/Enum"], function (stjs, Enum) {
   var ExportFormat = function() {
-    Enum.call(this, ["XLS", "PDF", "DATA"]);
+    Enum.call(this, ["XLSX", "PDF", "HTML", "DATA"]);
   }
 
   stjs.extend(
diff --git a/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportOptions.js b/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportOptions.js
index 005f53ddb45bb934b447fbe55375891f8ecefdcd..586119a7948febdeca546ed852b8efd6419d7bff 100644
--- a/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportOptions.js
+++ b/api-openbis-javascript/src/v3/as/dto/exporter/options/ExportOptions.js
@@ -16,7 +16,11 @@
  */
 
 define(["stjs"], function (stjs) {
-  var ExportOptions = function() {
+  var ExportOptions = function(formats, xlsTextFormat, withReferredTypes, withImportCompatibility) {
+    this.formats = formats;
+    this.xlsTextFormat = xlsTextFormat;
+    this.withReferredTypes = withReferredTypes;
+    this.withImportCompatibility = withImportCompatibility;
   }
 
   stjs.extend(
diff --git a/api-openbis-javascript/src/v3/openbis.js b/api-openbis-javascript/src/v3/openbis.js
index 1f4aa3c7b00f30dbc7aaa836fa7b4b32ba1d51b8..f7b04ec5da23c81b264ce065f74219d5761a1c17 100644
--- a/api-openbis-javascript/src/v3/openbis.js
+++ b/api-openbis-javascript/src/v3/openbis.js
@@ -2347,16 +2347,27 @@ define([ 'jquery', 'util/Json', 'as/dto/datastore/search/DataStoreSearchCriteria
 			});
 		}
 
-        this.isSessionActive = function() {
-            var thisFacade = this;
-            return thisFacade._private.ajaxRequest({
-                url : openbisUrl,
-                data : {
-                    "method" : "isSessionActive",
-                    "params" : [ thisFacade._private.sessionToken ]
-                }
-            });
-        }
+		this.executeExport = function(exportData, exportOptions) {
+			var thisFacade = this;
+			return thisFacade._private.ajaxRequest({
+				url : openbisUrl,
+				data : {
+					"method" : "executeExport",
+					"params" : [ thisFacade._private.sessionToken, exportData, exportOptions ]
+				}
+			});
+		}
+
+		this.isSessionActive = function() {
+				var thisFacade = this;
+				return thisFacade._private.ajaxRequest({
+						url : openbisUrl,
+						data : {
+								"method" : "isSessionActive",
+								"params" : [ thisFacade._private.sessionToken ]
+						}
+				});
+		}
 
 		this.getDataStoreFacade = function() {
 			var dataStoreCodes = [];
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/FileServiceServlet.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/FileServiceServlet.java
index 24b3e41fd8aad42f09c4f7f48fceadc29890c974..3118f94025ed81c8da07989980f4e81fab39f747 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/FileServiceServlet.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/FileServiceServlet.java
@@ -45,7 +45,6 @@ import ch.systemsx.cisd.common.properties.PropertyUtils;
 import ch.systemsx.cisd.common.spring.ExposablePropertyPlaceholderConfigurer;
 import ch.systemsx.cisd.common.string.Template;
 import ch.systemsx.cisd.openbis.generic.client.web.server.AbstractServlet;
-import ch.systemsx.cisd.openbis.generic.shared.basic.GenericSharedConstants;
 
 
 /**
@@ -59,12 +58,12 @@ public class FileServiceServlet extends AbstractServlet
     public static final String FILE_SERVICE_PATH = "file-service";
     public static final String FILE_SERVICE_PATH_MAPPING = FILE_SERVICE_PATH + "/**/*";
 
-    private static final String APP_PREFIX = "/" + FILE_SERVICE_PATH + "/";
     private static final String KEY_PREFIX = "file-server.";
-    
-    private static final String REPO_PATH_KEY = KEY_PREFIX + "repository-path";
-    private static final String DEFAULT_REPO_PATH = "../../../data/file-server";
-    
+    public static final String REPO_PATH_KEY = KEY_PREFIX + "repository-path";
+
+    public static final String DEFAULT_REPO_PATH = "../../../data/file-server";
+    private static final String APP_PREFIX = "/" + FILE_SERVICE_PATH + "/";
+
     private static final String MAX_SIZE_KEY = KEY_PREFIX + "maximum-file-size-in-MB";
     private static final int DEFAULT_MAX_SIZE = 10;
     
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
index 6aa3bd93402d5181749d60db27da33d92d192d48..df54c5679c00ae347859bbca0e5318e136d8713e 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
@@ -146,6 +146,11 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentType
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.UpdateExperimentTypesOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.UpdateExperimentsOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportOperationResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.ExportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.ExternalDms;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.create.CreateExternalDmsOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.create.CreateExternalDmsOperationResult;
@@ -1818,7 +1823,15 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
         executeOperation(sessionToken, new ImportOperation(importData, importOptions));
     }
 
-    @Override public IApplicationServerApi createPersonalAccessTokenInvocationHandler(final IPersonalAccessTokenInvocation invocation)
+    @Override
+    public ExportResult executeExport(final String sessionToken, final ExportData exportData, final ExportOptions exportOptions)
+    {
+        final ExportOperationResult operationResult = executeOperation(sessionToken, new ExportOperation(exportData, exportOptions));
+        return operationResult.getExportResult();
+    }
+
+    @Override
+    public IApplicationServerApi createPersonalAccessTokenInvocationHandler(final IPersonalAccessTokenInvocation invocation)
     {
         return new ApplicationServerApiPersonalAccessTokenInvocationHandler(invocation);
     }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java
index e2c20fbece7bfb0645e0994a6d8d615ca93178ac..f1ec87277d6c59b0716a50ffe6c854d93cfd047e 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiLogger.java
@@ -76,6 +76,9 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentSear
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentTypeSearchCriteria;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentTypeUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentUpdate;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.ExportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.ExternalDms;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.create.ExternalDmsCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.delete.ExternalDmsDeletionOptions;
@@ -1371,4 +1374,11 @@ public class ApplicationServerApiLogger extends AbstractServerLogger implements
         logAccess(sessionToken, "execute-import", "IImportData(%s) ImportOptions(%s)", importData, importOptions);
     }
 
+    @Override
+    public ExportResult executeExport(final String sessionToken, final ExportData exportData, final ExportOptions exportOptions)
+    {
+        logAccess(sessionToken, "execute-export", "ExportData(%s) ExportOptions(%s)", exportData, exportOptions);
+        return null;
+    }
+
 }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiPersonalAccessTokenInvocationHandler.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiPersonalAccessTokenInvocationHandler.java
index d7d792f889e393c67da51b5066328d4bdc43ec4b..1eba23cd4650c3f3bbda3dff28015498fc1281e3 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiPersonalAccessTokenInvocationHandler.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApiPersonalAccessTokenInvocationHandler.java
@@ -77,6 +77,9 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentSear
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentTypeSearchCriteria;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentTypeUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update.ExperimentUpdate;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.ExportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.ExternalDms;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.create.ExternalDmsCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.delete.ExternalDmsDeletionOptions;
@@ -1260,6 +1263,12 @@ public class ApplicationServerApiPersonalAccessTokenInvocationHandler implements
         invocation.proceedWithNewFirstArgument(converter.convert(sessionToken));
     }
 
+    @Override
+    public ExportResult executeExport(final String sessionToken, final ExportData exportData, final ExportOptions exportOptions)
+    {
+        return invocation.proceedWithNewFirstArgument(converter.convert(sessionToken));
+    }
+
     private void checkPersonalAccessTokensEnabled()
     {
         if (!config.arePersonalAccessTokensEnabled())
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/AbstractExportFieldsFinder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/AbstractExportFieldsFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..bb307a1f2735c9784389ed83e1fabbfb7d806d23
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/AbstractExportFieldsFinder.java
@@ -0,0 +1,98 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IEntityType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IPermIdHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.Attribute;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.SelectedFields;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.PropertyAssignment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.PropertyType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.IPropertyTypeId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.PropertyTypePermId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.ethz.sis.openbis.generic.server.xls.export.FieldType;
+
+public abstract class AbstractExportFieldsFinder<ENTITY_TYPE extends IEntityType & IPermIdHolder> implements IExportFieldsFinder
+{
+
+    @Override
+    public Map<String, List<Map<String, String>>> findExportFields(final Set<IPropertyTypeId> properties,
+            final IApplicationServerInternalApi applicationServerApi, final String sessionToken, final SelectedFields selectedFields)
+    {
+        final SearchResult<ENTITY_TYPE> entityTypeSearchResult = findEntityTypes(properties, applicationServerApi, sessionToken);
+
+        final List<ENTITY_TYPE> entityTypes = entityTypeSearchResult.getObjects();
+        final Collector<ENTITY_TYPE, ?, Map<String, List<Map<String, String>>>> entityTypeToMapCollector =
+                getEntityTypeMapCollector(selectedFields, entityTypes);
+        return entityTypes.stream().collect(entityTypeToMapCollector);
+    }
+
+    private Collector<ENTITY_TYPE, ?, Map<String, List<Map<String, String>>>> getEntityTypeMapCollector(final SelectedFields selectedFields,
+            final List<ENTITY_TYPE> entityTypes)
+    {
+        final Map<String, Map<PropertyTypePermId, String>> propertyTypePermIdsByEntityType =
+                entityTypes.stream().collect(Collectors.toMap(this::getPermId,
+                        entityType -> entityType.getPropertyAssignments().stream()
+                                .map(PropertyAssignment::getPropertyType)
+                                .collect(Collectors.toMap(PropertyType::getPermId, PropertyType::getCode))));
+
+        return Collectors.toMap(this::getPermId,
+                entityType ->
+                {
+                    final Map<PropertyTypePermId, String> propertyTypePermIds =
+                            propertyTypePermIdsByEntityType.get(getPermId(entityType));
+                    final List<String> selectedPropertyTypeCodes =
+                            selectedFields.getProperties().stream().flatMap(
+                                            propertyTypePermId ->
+                                            {
+                                                final String propertyTypeCode = propertyTypePermIds.get(propertyTypePermId);
+                                                return propertyTypeCode != null ? Stream.of(propertyTypeCode) : Stream.empty();
+                                            })
+                                    .collect(Collectors.toList());
+                    return mergePropertiesAndAttributes(selectedPropertyTypeCodes, selectedFields.getAttributes());
+                });
+    }
+
+    private static List<Map<String, String>> mergePropertiesAndAttributes(final List<String> selectedPropertyTypeCodes,
+            final Collection<Attribute> attributes)
+    {
+        final Stream<Map<String, String>> attributesStream = attributes.stream()
+                .map(attribute -> Map.of(TYPE, FieldType.ATTRIBUTE.name(), ID, attribute.name()));
+
+        final Stream<Map<String, String>> propertiesStream = selectedPropertyTypeCodes.stream()
+                .map(propertyTypeCode -> Map.of(TYPE, FieldType.PROPERTY.name(), ID, propertyTypeCode));
+
+        return Stream.concat(attributesStream, propertiesStream).collect(Collectors.toList());
+    }
+
+    public abstract SearchResult<ENTITY_TYPE> findEntityTypes(Set<IPropertyTypeId> properties,
+            IApplicationServerInternalApi applicationServerApi, String sessionToken);
+
+    public abstract String getPermId(ENTITY_TYPE entityType);
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/DataSetExportFieldsFinder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/DataSetExportFieldsFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..f60cd6a239d0a712dc55a898f4c0b4883c6e249e
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/DataSetExportFieldsFinder.java
@@ -0,0 +1,51 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.Set;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.search.DataSetTypeSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.IPropertyTypeId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+
+public class DataSetExportFieldsFinder extends AbstractExportFieldsFinder<DataSetType>
+{
+
+    @Override
+    public SearchResult<DataSetType> findEntityTypes(final Set<IPropertyTypeId> properties,
+            final IApplicationServerInternalApi applicationServerApi, final String sessionToken)
+    {
+        final DataSetTypeSearchCriteria typeSearchCriteria = new DataSetTypeSearchCriteria();
+        typeSearchCriteria.withPropertyAssignments().withPropertyType().withIds().thatIn(properties);
+
+        final DataSetTypeFetchOptions fetchOptions = new DataSetTypeFetchOptions();
+        fetchOptions.withPropertyAssignments().withPropertyType();
+
+        return applicationServerApi.searchDataSetTypes(sessionToken, typeSearchCriteria, fetchOptions);
+    }
+
+    @Override
+    public String getPermId(final DataSetType dataSetType)
+    {
+        return dataSetType.getPermId().getPermId();
+    }
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/DocumentBuilder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/DocumentBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..da12409d5487c2d09dc2e7fb93f9987821799bbf
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/DocumentBuilder.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright ETH 2021 - 2023 Zürich, Scientific IT Services
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.Base64;
+
+import org.apache.log4j.Logger;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import ch.systemsx.cisd.common.http.JettyHttpClientFactory;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+
+class DocumentBuilder
+{
+
+    private static final Logger LOG = LogFactory.getLogger(LogCategory.OPERATION, DocumentBuilder.class);
+
+    private static final String START_RICH_TEXT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<html><head></head><body>";
+
+    private static final String END_RICH_TEXT = "</body></html>";
+
+    private StringBuffer doc = new StringBuffer();
+
+    private String closedDoc;
+
+    private boolean closed = false;
+
+    public DocumentBuilder()
+    {
+        System.setProperty("javax.xml.transform.TransformerFactory", "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
+        startDoc();
+    }
+
+    public void setDocument(final String doc)
+    {
+        this.doc = new StringBuffer(doc);
+        closed = true;
+    }
+
+    private void startDoc()
+    {
+        if (!closed)
+        {
+            doc.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
+            doc.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
+            doc.append("<head></head>");
+            doc.append("<body>");
+        }
+    }
+
+    private void endDoc()
+    {
+        if (!closed)
+        {
+            doc.append("</body>");
+            doc.append("</html>");
+            closed = true;
+            closedDoc = fixImages(doc);
+        }
+    }
+
+    public void addProperty(final String key, final String value)
+    {
+        if (!closed)
+        {
+            doc.append("<p>").append("<b>").append(key).append(": ").append("</b>").append("</p>");
+            addParagraph(value);
+        }
+    }
+
+    public void addParagraph(final String value)
+    {
+        if (!closed)
+        {
+            doc.append("<p>").append(cleanXMLEnvelope(value)).append("</p>");
+        }
+    }
+
+    public void addTitle(final String title)
+    {
+        if (!closed)
+        {
+            doc.append("<h1>").append(title).append("</h1>");
+        }
+    }
+
+    public void addHeader(final String header)
+    {
+        if (!closed)
+        {
+            doc.append("<h2>").append(header).append("</h2>");
+        }
+    }
+
+    public String getHtml()
+    {
+        if (!closed)
+        {
+            endDoc();
+        }
+        return closedDoc;
+    }
+
+    private String cleanXMLEnvelope(final String value)
+    {
+        if (value.startsWith(START_RICH_TEXT) && value.endsWith(END_RICH_TEXT))
+        {
+            return value.substring(START_RICH_TEXT.length(), value.length() - END_RICH_TEXT.length());
+        } else
+        {
+            return value;
+        }
+    }
+
+    private String fixImages(StringBuffer buffer)
+    {
+        final Document jsoupDoc = Jsoup.parse(buffer.toString());
+        jsoupDoc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+        final Elements elements = jsoupDoc.select("img");
+
+        // Fixes images sizes
+        for (final Element element : elements)
+        {
+            final String style = element.attr("style");
+            final String[] rules = style.split(";");
+            for (final String rule : rules)
+            {
+                final String[] ruleElements = rule.split(":");
+                if (ruleElements.length == 2)
+                {
+                    final String ruleKey = ruleElements[0].trim();
+                    final String ruleValue = ruleElements[1].trim();
+                    if ((ruleKey.equalsIgnoreCase("width") || ruleKey.equalsIgnoreCase("height")) && ruleValue.endsWith("px"))
+                    {
+                        element.attr(ruleKey, ruleValue.substring(0, ruleValue.length() - 2));
+                    }
+                }
+            }
+        }
+
+        return jsoupDoc.html();
+    }
+
+    private static String getDataUriFromUri(final String url) throws Exception
+    {
+        final HttpClient client = JettyHttpClientFactory.getHttpClient();
+        final Request requestEntity = client.newRequest(url).method("GET");
+        final ContentResponse contentResponse = requestEntity.send();
+        return "data:" + contentResponse.getMediaType() + ";base64," + Base64.getEncoder().encodeToString(contentResponse.getContent());
+    }
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/EntitiesFinder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/EntitiesFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d24e7d70337f7e00d99546d24d54c201ce749c7
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/EntitiesFinder.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.ICodeHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.EntityKind;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.IEntityTypeId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.ExperimentType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.fetchoptions.ProjectFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.id.ProjectPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.fetchoptions.PropertyAssignmentFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.Vocabulary;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.IVocabularyId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyPermId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind;
+import ch.ethz.sis.openbis.generic.server.xls.export.ExportablePermId;
+import ch.systemsx.cisd.openbis.generic.server.CommonServiceProvider;
+
+class EntitiesFinder
+{
+
+    public static Collection<ICodeHolder> getEntities(final String sessionToken, final Collection<ExportablePermId> permIds)
+    {
+        final Map<ExportableKind, List<ExportablePermId>> groupedExportables =
+                permIds.stream().collect(Collectors.groupingBy(ExportablePermId::getExportableKind));
+
+        return groupedExportables.entrySet().stream().flatMap(entry ->
+        {
+            final Collection<String> stringPermIds = entry.getValue().stream().map(permId -> permId.getPermId().getPermId())
+                    .collect(Collectors.toList());
+            switch (entry.getKey())
+            {
+                case SAMPLE_TYPE:
+                {
+                    return Stream.of(getSampleTypes(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case EXPERIMENT_TYPE:
+                {
+                    return Stream.of(getExperimentTypes(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case DATASET_TYPE:
+                {
+                    return Stream.of(getDataSetTypes(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case VOCABULARY_TYPE:
+                {
+                    return Stream.of(getVocabularies(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case SPACE:
+                {
+                    return Stream.of(getSpaces(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case PROJECT:
+                {
+                    return Stream.of(getProjects(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case SAMPLE:
+                {
+                    return Stream.of(getSamples(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case EXPERIMENT:
+                {
+                    return Stream.of(getExperiments(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                case DATASET:
+                {
+                    return Stream.of(getDataSets(sessionToken, stringPermIds)).map(value -> (ICodeHolder) value);
+                }
+                default:
+                {
+                    throw new IllegalArgumentException();
+                }
+            }
+        }).collect(Collectors.toList());
+    }
+
+    public static Collection<DataSetType> getDataSetTypes(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final DataSetTypeFetchOptions fetchOptions = new DataSetTypeFetchOptions();
+        fetchOptions.withValidationPlugin().withScript();
+        final PropertyAssignmentFetchOptions propertyAssignmentFetchOptions = fetchOptions.withPropertyAssignments();
+        propertyAssignmentFetchOptions.withPropertyType().withVocabulary();
+        propertyAssignmentFetchOptions.withPropertyType().withSampleType();
+        propertyAssignmentFetchOptions.withPropertyType().withMaterialType();
+        propertyAssignmentFetchOptions.withPlugin().withScript();
+        final Map<IEntityTypeId, DataSetType> dataSetTypes = api.getDataSetTypes(sessionToken,
+                permIds.stream().map(permId -> new EntityTypePermId(permId, EntityKind.DATA_SET)).collect(Collectors.toList()), fetchOptions);
+
+        assert dataSetTypes.size() <= 1;
+
+        return dataSetTypes.values();
+    }
+
+    public static Collection<DataSet> getDataSets(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final List<DataSetPermId> dataSetPermIds = permIds.stream().map(DataSetPermId::new)
+                .collect(Collectors.toList());
+        final DataSetFetchOptions fetchOptions = new DataSetFetchOptions();
+        fetchOptions.withSample();
+        fetchOptions.withExperiment().withProject().withSpace();
+        fetchOptions.withType().withPropertyAssignments().withPropertyType();
+        fetchOptions.withProperties();
+        fetchOptions.withRegistrator();
+        fetchOptions.withModifier();
+        fetchOptions.withPhysicalData();
+        fetchOptions.withParents().withProperties();
+        fetchOptions.withChildren().withProperties();
+        return api.getDataSets(sessionToken, dataSetPermIds, fetchOptions).values();
+    }
+
+    public static Collection<Experiment> getExperiments(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final List<ExperimentPermId> experimentPermIds = permIds.stream().map(ExperimentPermId::new)
+                .collect(Collectors.toList());
+        final ExperimentFetchOptions fetchOptions = new ExperimentFetchOptions();
+        final ProjectFetchOptions projectFetchOptions = fetchOptions.withProject();
+        projectFetchOptions.withSpace();
+        projectFetchOptions.withRegistrator();
+        projectFetchOptions.withModifier();
+
+        fetchOptions.withType().withPropertyAssignments().withPropertyType();
+        fetchOptions.withProperties();
+        fetchOptions.withRegistrator();
+        fetchOptions.withModifier();
+        fetchOptions.withDataSets().withType();
+        return api.getExperiments(sessionToken, experimentPermIds, fetchOptions).values();
+    }
+
+    public static Collection<ExperimentType> getExperimentTypes(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final ExperimentTypeFetchOptions fetchOptions = new ExperimentTypeFetchOptions();
+        fetchOptions.withValidationPlugin().withScript();
+        final PropertyAssignmentFetchOptions propertyAssignmentFetchOptions = fetchOptions.withPropertyAssignments();
+        propertyAssignmentFetchOptions.withPropertyType().withVocabulary();
+        propertyAssignmentFetchOptions.withPropertyType().withSampleType();
+        propertyAssignmentFetchOptions.withPropertyType().withMaterialType();
+        propertyAssignmentFetchOptions.withPlugin().withScript();
+        final Map<IEntityTypeId, ExperimentType> experimentTypes = api.getExperimentTypes(sessionToken,
+                permIds.stream().map(permId -> new EntityTypePermId(permId, EntityKind.EXPERIMENT)).collect(Collectors.toList()), fetchOptions);
+
+        assert experimentTypes.size() <= 1;
+
+        return experimentTypes.values();
+    }
+
+    public static Collection<Project> getProjects(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final List<ProjectPermId> projectPermIds = permIds.stream().map(ProjectPermId::new)
+                .collect(Collectors.toList());
+        final ProjectFetchOptions fetchOptions = new ProjectFetchOptions();
+        fetchOptions.withSpace();
+        fetchOptions.withRegistrator();
+        fetchOptions.withModifier();
+        return api.getProjects(sessionToken, projectPermIds, fetchOptions).values();
+    }
+
+    public static Collection<Sample> getSamples(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final List<SamplePermId> samplePermIds = permIds.stream().map(SamplePermId::new)
+                .collect(Collectors.toList());
+        final SampleFetchOptions fetchOptions = new SampleFetchOptions();
+        final ExperimentFetchOptions experimentFetchOptions = fetchOptions.withExperiment();
+        experimentFetchOptions.withProperties();
+        experimentFetchOptions.withProject().withSpace();
+        fetchOptions.withSpace();
+        fetchOptions.withProject().withSpace();
+        fetchOptions.withParents().withProperties();
+        fetchOptions.withChildren().withProperties();
+        fetchOptions.withType().withPropertyAssignments().withPropertyType();
+        fetchOptions.withProperties();
+        fetchOptions.withRegistrator();
+        fetchOptions.withModifier();
+        fetchOptions.withDataSets().withType();
+        fetchOptions.withContainer();
+        return api.getSamples(sessionToken, samplePermIds, fetchOptions).values();
+    }
+
+    public static Collection<SampleType> getSampleTypes(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final SampleTypeFetchOptions fetchOptions = new SampleTypeFetchOptions();
+        fetchOptions.withValidationPlugin().withScript();
+        final PropertyAssignmentFetchOptions propertyAssignmentFetchOptions = fetchOptions.withPropertyAssignments();
+        propertyAssignmentFetchOptions.withPropertyType().withVocabulary();
+        propertyAssignmentFetchOptions.withPropertyType().withSampleType();
+        propertyAssignmentFetchOptions.withPropertyType().withMaterialType();
+        propertyAssignmentFetchOptions.withPlugin().withScript();
+        final Map<IEntityTypeId, SampleType> sampleTypes = api.getSampleTypes(sessionToken,
+                permIds.stream().map(permId -> new EntityTypePermId(permId, EntityKind.SAMPLE)).collect(Collectors.toList()), fetchOptions);
+
+        assert sampleTypes.size() <= 1;
+
+        return sampleTypes.values();
+    }
+
+    public static Collection<Space> getSpaces(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final List<SpacePermId> spacePermIds = permIds.stream().map(SpacePermId::new).collect(Collectors.toList());
+        final SpaceFetchOptions fetchOptions = new SpaceFetchOptions();
+        fetchOptions.withRegistrator();
+        return api.getSpaces(sessionToken, spacePermIds, fetchOptions).values();
+    }
+
+    public static Collection<Vocabulary> getVocabularies(final String sessionToken, final Collection<String> permIds)
+    {
+        final IApplicationServerInternalApi api = CommonServiceProvider.getApplicationServerApi();
+        final VocabularyFetchOptions fetchOptions = new VocabularyFetchOptions();
+        fetchOptions.withTerms();
+        fetchOptions.withRegistrator();
+        final Map<IVocabularyId, Vocabulary> vocabularies = api.getVocabularies(sessionToken,
+                permIds.stream().map(VocabularyPermId::new).collect(Collectors.toList()), fetchOptions);
+
+        assert vocabularies.size() <= 1;
+
+        return vocabularies.values();
+    }
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExperimentExportFieldsFinder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExperimentExportFieldsFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..0404d54dbd986676ead458449e166c29814a283f
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExperimentExportFieldsFinder.java
@@ -0,0 +1,51 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.Set;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.ExperimentType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.IPropertyTypeId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentTypeSearchCriteria;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+
+public class ExperimentExportFieldsFinder extends AbstractExportFieldsFinder<ExperimentType>
+{
+
+    @Override 
+    public SearchResult<ExperimentType> findEntityTypes(final Set<IPropertyTypeId> properties,
+            final IApplicationServerInternalApi applicationServerApi, final String sessionToken)
+    {
+        final ExperimentTypeSearchCriteria typeSearchCriteria = new ExperimentTypeSearchCriteria();
+        typeSearchCriteria.withPropertyAssignments().withPropertyType().withIds().thatIn(properties);
+
+        final ExperimentTypeFetchOptions fetchOptions = new ExperimentTypeFetchOptions();
+        fetchOptions.withPropertyAssignments().withPropertyType();
+
+        return applicationServerApi.searchExperimentTypes(sessionToken, typeSearchCriteria, fetchOptions);
+    }
+
+    @Override 
+    public String getPermId(final ExperimentType experimentType)
+    {
+        return experimentType.getPermId().getPermId();
+    }
+    
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExportExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExportExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..4731d9f7adb5320e51c7755f632a1674b52e6895
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExportExecutor.java
@@ -0,0 +1,1615 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import static ch.ethz.sis.openbis.generic.server.FileServiceServlet.DEFAULT_REPO_PATH;
+import static ch.ethz.sis.openbis.generic.server.FileServiceServlet.REPO_PATH_KEY;
+import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.DATASET;
+import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.EXPERIMENT;
+import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.MASTER_DATA_EXPORTABLE_KINDS;
+import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.PROJECT;
+import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.SAMPLE;
+import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.SPACE;
+import static ch.ethz.sis.openbis.generic.server.xls.export.FieldType.ATTRIBUTE;
+import static ch.ethz.sis.openbis.generic.server.xls.export.FieldType.PROPERTY;
+import static ch.ethz.sis.openbis.generic.server.xls.export.XLSExport.ExportResult;
+import static ch.ethz.sis.openbis.generic.server.xls.export.XLSExport.SCRIPTS_DIRECTORY;
+import static ch.ethz.sis.openbis.generic.server.xls.export.XLSExport.TextFormatting;
+import static ch.ethz.sis.openbis.generic.server.xls.export.XLSExport.ZIP_EXTENSION;
+import static ch.ethz.sis.openbis.generic.server.xls.export.helper.AbstractXLSExportHelper.FIELD_ID_KEY;
+import static ch.ethz.sis.openbis.generic.server.xls.export.helper.AbstractXLSExportHelper.FIELD_TYPE_KEY;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+import org.apache.commons.io.filefilter.NameFileFilter;
+import org.apache.log4j.Logger;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.id.ObjectIdentifier;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.ICodeHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IDescriptionHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IEntityType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IEntityTypeHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IExperimentHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IIdentifierHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IModificationDateHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IModifierHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IParentChildrenHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IPermIdHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IPropertiesHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IRegistrationDateHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IRegistratorHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.ISampleHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSet;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetKind;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.DataSetType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.fetchoptions.DataSetTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.search.DataSetTypeSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.Experiment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.ExperimentType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.fetchoptions.ExperimentTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.search.ExperimentTypeSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.Attribute;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.ExportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.IExportableFields;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.SelectedFields;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportFormat;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.options.ExportOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.DataType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.PropertyAssignment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.PropertyType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.IPropertyTypeId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.Sample;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.search.SampleTypeSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
+import ch.ethz.sis.openbis.generic.asapi.v3.exceptions.NotFetchedException;
+import ch.ethz.sis.openbis.generic.dssapi.v3.IDataStoreServerApi;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownload;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadOptions;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadReader;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.fetchoptions.DataSetFileFetchOptions;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.id.DataSetFilePermId;
+import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.search.DataSetFileSearchCriteria;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.sharedapi.v3.json.ObjectMapperResource;
+import ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind;
+import ch.ethz.sis.openbis.generic.server.xls.export.ExportablePermId;
+import ch.ethz.sis.openbis.generic.server.xls.export.XLSExport;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.spring.ExposablePropertyPlaceholderConfigurer;
+import ch.systemsx.cisd.openbis.generic.server.CommonServiceProvider;
+import ch.systemsx.cisd.openbis.generic.shared.ISessionWorkspaceProvider;
+
+@SuppressWarnings("SizeReplaceableByIsEmpty")
+@Component
+public class ExportExecutor implements IExportExecutor
+{
+
+    public static final String METADATA_FILE_PREFIX = "metadata";
+
+    public static final String EXPORT_FILE_PREFIX = "export";
+
+    public static final String METADATA_FILE_NAME = "metadata" + XLSExport.XLSX_EXTENSION;
+
+    public static final String XLSX_DIRECTORY = "xlsx";
+
+    public static final String PDF_DIRECTORY = "pdf";
+
+    public static final String DATA_DIRECTORY = "data";
+
+    public static final String META_FILE_NAME = "meta.json";
+
+    public static final String SHARED_SAMPLES_DIRECTORY = "(shared)";
+
+    public static final String HTML_EXTENSION = ".html";
+
+    public static final String PDF_EXTENSION = ".pdf";
+
+    public static final String JSON_EXTENSION = ".json";
+
+    static final String NAME_PROPERTY_NAME = "$NAME";
+
+    private static final String TYPE_EXPORT_FIELD_KEY = "TYPE";
+
+    private static final Map<ExportableKind, IExportFieldsFinder> FIELDS_FINDER_BY_EXPORTABLE_KIND =
+            Map.of(ExportableKind.SAMPLE, new SampleExportFieldsFinder(),
+                    ExportableKind.EXPERIMENT, new ExperimentExportFieldsFinder(),
+                    ExportableKind.DATASET, new DataSetExportFieldsFinder());
+
+    private static final Set<ExportableKind> TYPE_EXPORTABLE_KINDS = EnumSet.of(ExportableKind.SAMPLE_TYPE, ExportableKind.EXPERIMENT_TYPE,
+            ExportableKind.DATASET_TYPE, ExportableKind.VOCABULARY_TYPE, ExportableKind.SPACE, ExportableKind.PROJECT);
+
+    private static final String PYTHON_EXTENSION = ".py";
+
+    private static final String COMMON_STYLE = "border: 1px solid black;";
+
+    private static final String TABLE_STYLE = COMMON_STYLE + " border-collapse: collapse;";
+
+    private static final String DATA_TAG_START = "<DATA>";
+
+    private static final int DATA_TAG_START_LENGTH = DATA_TAG_START.length();
+
+    private static final String DATA_TAG_END = "</DATA>";
+
+    private static final int DATA_TAG_END_LENGTH = DATA_TAG_END.length();
+
+    private static final String PNG_MEDIA_TYPE = "image/png";
+
+    private static final String JPEG_MEDIA_TYPE = "image/jpeg";
+
+    /** Buffer size for the buffer stream for Base64 encoding. Should be a multiple of 3. */
+    private static final int BUFFER_SIZE = 3 * 1024;
+
+    private static final Map<String, String> MEDIA_TYPE_BY_EXTENSION = Map.of(
+            ".png", PNG_MEDIA_TYPE,
+            ".jpg", JPEG_MEDIA_TYPE,
+            ".jpeg", JPEG_MEDIA_TYPE,
+            ".jfif", JPEG_MEDIA_TYPE,
+            ".pjpeg", JPEG_MEDIA_TYPE,
+            ".pjp", JPEG_MEDIA_TYPE,
+            ".gif", "image/gif",
+            ".bmp", "image/bmp",
+            ".webp", "image/webp",
+            ".tiff", "image/tiff");
+
+    private static final String DEFAULT_MEDIA_TYPE = JPEG_MEDIA_TYPE;
+
+    private static final String DATA_PREFIX_TEMPLATE = "data:%s;base64,";
+
+    private static final String KIND_DOCUMENT_PROPERTY_ID = "Kind";
+
+    private static final Logger OPERATION_LOG = LogFactory.getLogger(LogCategory.OPERATION, ExportExecutor.class);
+
+    /** All characters except the ones we consider safe as a folder name. */
+    private static final String UNSAFE_CHARACTERS_REGEXP = "[^\\w $!#%'()+,\\-.;=@\\[\\]^{}_~]";
+
+    @Autowired
+    private ISessionWorkspaceProvider sessionWorkspaceProvider;
+
+    @Resource(name = ObjectMapperResource.NAME)
+    private ObjectMapper objectMapper;
+
+    @Resource(name = ExposablePropertyPlaceholderConfigurer.PROPERTY_CONFIGURER_BEAN_NAME)
+    private ExposablePropertyPlaceholderConfigurer configurer;
+
+    private ObjectWriter objectWriter;
+
+    @PostConstruct
+    private void postConstruct()
+    {
+        objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
+    }
+
+    @Override
+    public ExportResult doExport(final IOperationContext context, final ExportOperation operation)
+    {
+        try
+        {
+            final ExportData exportData = operation.getExportData();
+            final ExportOptions exportOptions = operation.getExportOptions();
+            final String sessionToken = context.getSession().getSessionToken();
+
+            return doExport(sessionToken, exportData, exportOptions);
+        } catch (final IOException e)
+        {
+            throw UserFailureException.fromTemplate(e, "IO exception exporting.");
+        }
+    }
+
+    private ExportResult doExport(final String sessionToken, final ExportData exportData, final ExportOptions exportOptions)
+            throws IOException
+    {
+        final IApplicationServerInternalApi applicationServerApi = CommonServiceProvider.getApplicationServerApi();
+
+        final List<ExportablePermId> exportablePermIds = exportData.getPermIds().stream()
+                .map(exportablePermIdDto -> new ExportablePermId(
+                        ExportableKind.valueOf(exportablePermIdDto.getExportableKind().name()), exportablePermIdDto.getPermId()))
+                .collect(Collectors.toList());
+        final Set<ExportableKind> exportableKinds = exportablePermIds.stream()
+                .map(ExportablePermId::getExportableKind)
+                .collect(Collectors.toSet());
+
+        final IExportableFields fields = exportData.getFields();
+        final Map<String, Map<String, List<Map<String, String>>>> exportFields;
+        if (fields instanceof SelectedFields)
+        {
+            final SelectedFields selectedFields = (SelectedFields) fields;
+            final Set<IPropertyTypeId> properties = new HashSet<>(selectedFields.getProperties());
+
+            exportFields = exportableKinds.stream().flatMap(exportableKind ->
+            {
+                final IExportFieldsFinder fieldsFinder = FIELDS_FINDER_BY_EXPORTABLE_KIND.get(exportableKind);
+                if (fieldsFinder != null)
+                {
+                    final Map<String, List<Map<String, String>>> selectedFieldMap =
+                            fieldsFinder.findExportFields(properties, applicationServerApi, sessionToken, selectedFields);
+                    return Stream.of(new AbstractMap.SimpleEntry<>(exportableKind.name(), selectedFieldMap));
+                } else if (TYPE_EXPORTABLE_KINDS.contains(exportableKind))
+                {
+                    final Map<String, List<Map<String, String>>> selectedAttributesMap = findExportAttributes(exportableKind, selectedFields);
+                    return Stream.of(new AbstractMap.SimpleEntry<>(TYPE_EXPORT_FIELD_KEY, selectedAttributesMap));
+                } else
+                {
+                    return Stream.empty();
+                }
+            }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        } else
+        {
+            exportFields = null;
+        }
+
+        return doExport(applicationServerApi, sessionToken,
+                exportablePermIds, exportOptions.isWithReferredTypes(), exportFields,
+                TextFormatting.valueOf(exportOptions.getXlsTextFormat().name()), exportOptions.isWithImportCompatibility(),
+                exportOptions.getFormats());
+    }
+
+    private ExportResult doExport(final IApplicationServerApi api,
+            final String sessionToken, final List<ExportablePermId> exportablePermIds,
+            final boolean exportReferredMasterData,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields,
+            final TextFormatting textFormatting, final boolean compatibleWithImport,
+            final Set<ExportFormat> exportFormats) throws IOException
+    {
+        final String zipFileName = String.format("%s.%s%s", EXPORT_FILE_PREFIX, new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS").format(new Date()),
+                ZIP_EXTENSION);
+        final Collection<String> warnings = new ArrayList<>();
+
+        final boolean hasXlsxFormat = exportFormats.contains(ExportFormat.XLSX);
+        final boolean hasHtmlFormat = exportFormats.contains(ExportFormat.HTML);
+        final boolean hasPdfFormat = exportFormats.contains(ExportFormat.PDF);
+        final boolean hasDataFormat = exportFormats.contains(ExportFormat.DATA);
+
+        if (hasXlsxFormat)
+        {
+            exportXlsx(api, sessionToken, exportablePermIds, exportReferredMasterData, exportFields, textFormatting, compatibleWithImport, warnings);
+        }
+
+        if (hasHtmlFormat || hasPdfFormat || hasDataFormat)
+        {
+            final EntitiesVo entitiesVo = new EntitiesVo(sessionToken, exportablePermIds);
+
+            if (hasPdfFormat || hasHtmlFormat)
+            {
+                final ISessionWorkspaceProvider sessionWorkspaceProvider = CommonServiceProvider.getSessionWorkspaceProvider();
+                final File sessionWorkspaceDirectory = sessionWorkspaceProvider.getSessionWorkspace(sessionToken).getCanonicalFile();
+                final File docDirectory = new File(sessionWorkspaceDirectory, PDF_DIRECTORY);
+                mkdirs(docDirectory);
+
+                exportSpacesDoc(sessionToken, exportFields, entitiesVo, exportFormats, docDirectory);
+                exportProjectsDoc(sessionToken, docDirectory, entitiesVo, exportFields, exportFormats);
+                exportExperimentsDoc(sessionToken, docDirectory, entitiesVo, exportFields, exportFormats);
+                exportSamplesDoc(sessionToken, docDirectory, entitiesVo, exportFields, exportFormats);
+                exportDataSetsDoc(sessionToken, docDirectory, entitiesVo, exportFields, exportFormats);
+            }
+
+            if (hasDataFormat)
+            {
+                exportData(sessionToken, entitiesVo);
+            }
+        }
+
+        final ISessionWorkspaceProvider sessionWorkspaceProvider = CommonServiceProvider.getSessionWorkspaceProvider();
+        final String sessionWorkspaceDirectoryPath = sessionWorkspaceProvider.getSessionWorkspace(sessionToken).getCanonicalPath();
+        zipDirectory(sessionWorkspaceDirectoryPath, new File(sessionWorkspaceDirectoryPath, zipFileName));
+
+        deleteDirectory(sessionWorkspaceDirectoryPath + '/' + XLSX_DIRECTORY);
+        deleteDirectory(sessionWorkspaceDirectoryPath + '/' + PDF_DIRECTORY);
+        deleteDirectory(sessionWorkspaceDirectoryPath + '/' + DATA_DIRECTORY);
+
+        return new ExportResult(zipFileName, warnings);
+    }
+
+    private static void exportXlsx(final IApplicationServerApi api, final String sessionToken, final List<ExportablePermId> exportablePermIds,
+            final boolean exportReferredMasterData, final Map<String, Map<String, List<Map<String, String>>>> exportFields,
+            final TextFormatting textFormatting, final boolean compatibleWithImport, final Collection<String> warnings) throws IOException
+    {
+        final ISessionWorkspaceProvider sessionWorkspaceProvider = CommonServiceProvider.getSessionWorkspaceProvider();
+        final File sessionWorkspaceDirectory = sessionWorkspaceProvider.getSessionWorkspace(sessionToken).getCanonicalFile();
+        final XLSExport.PrepareWorkbookResult xlsExportResult = XLSExport.prepareWorkbook(api, sessionToken, exportablePermIds,
+                exportReferredMasterData, exportFields, textFormatting, compatibleWithImport);
+
+        final File xlsxDirectory = new File(sessionWorkspaceDirectory, XLSX_DIRECTORY);
+        mkdirs(xlsxDirectory);
+
+        final File scriptsDirectory = new File(xlsxDirectory, SCRIPTS_DIRECTORY);
+
+        final Map<String, String> xlsExportScripts = xlsExportResult.getScripts();
+        if (!xlsExportScripts.isEmpty())
+        {
+            mkdirs(scriptsDirectory);
+
+            for (final Map.Entry<String, String> script : xlsExportScripts.entrySet())
+            {
+                final File scriptFile = new File(scriptsDirectory, script.getKey() + PYTHON_EXTENSION);
+                try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(scriptFile), BUFFER_SIZE))
+                {
+                    bos.write(script.getValue().getBytes());
+                    bos.flush();
+                }
+            }
+        }
+
+        try (
+                final Workbook wb = xlsExportResult.getWorkbook();
+                final BufferedOutputStream bos = new BufferedOutputStream(
+                        new FileOutputStream(new File(xlsxDirectory, METADATA_FILE_NAME)), BUFFER_SIZE);
+        )
+        {
+            wb.write(bos);
+        }
+
+        warnings.addAll(xlsExportResult.getWarnings());
+    }
+
+    private void exportData(final String sessionToken, final EntitiesVo entitiesVo) throws IOException
+    {
+        final Collection<Sample> samples = entitiesVo.getSamples();
+        for (final Sample sample : samples)
+        {
+            exportDatasetsData(sessionToken, 'O', sample.getDataSets(), sample, sample.getContainer());
+        }
+
+        final Collection<Experiment> experiments = entitiesVo.getExperiments();
+        for (final Experiment experiment : experiments)
+        {
+            exportDatasetsData(sessionToken, 'E', experiment.getDataSets(), experiment, null);
+        }
+    }
+
+    private void exportDatasetsData(final String sessionToken,
+            final char prefix, final List<DataSet> dataSets, final ICodeHolder codeHolder,
+            final Sample container) throws IOException
+    {
+        final String spaceCode = getSpaceCode(codeHolder);
+        final String projectCode = getProjectCode(codeHolder);
+        final String containerCode = container == null ? null : container.getCode();
+        final String code = codeHolder.getCode();
+        final String codeHolderJson = objectWriter.writeValueAsString(codeHolder);
+        final IDataStoreServerApi v3Dss = CommonServiceProvider.getDataStoreServerApi();
+
+        for (final DataSet dataSet : dataSets)
+        {
+            final String dataSetPermId = dataSet.getPermId().getPermId();
+            final String dataSetCode = dataSet.getCode();
+            final String dataSetTypeCode = dataSet.getType().getCode();
+            final String dataSetName = getEntityName(dataSet);
+
+            createMetadataJsonFile(sessionToken, prefix, spaceCode, projectCode, containerCode, code, dataSetTypeCode,
+                    dataSetCode, dataSetName, codeHolderJson);
+
+            if (dataSet.getKind() != DataSetKind.LINK)
+            {
+                final DataSetFileSearchCriteria criteria = new DataSetFileSearchCriteria();
+                criteria.withDataSet().withPermId().thatEquals(dataSetPermId);
+
+                final SearchResult<DataSetFile> results = v3Dss.searchFiles(sessionToken, criteria, new DataSetFileFetchOptions());
+
+                OPERATION_LOG.info(String.format("Found: %d files", results.getTotalCount()));
+
+                final List<DataSetFile> dataSetFiles = results.getObjects();
+                final List<DataSetFilePermId> fileIds = dataSetFiles.stream().map(DataSetFile::getPermId).collect(Collectors.toList());
+
+                final DataSetFileDownloadOptions options = new DataSetFileDownloadOptions();
+                options.setRecursive(true);
+
+                try (final InputStream is = v3Dss.downloadFiles(sessionToken, fileIds, options))
+                {
+                    final DataSetFileDownloadReader reader = new DataSetFileDownloadReader(is);
+                    DataSetFileDownload file;
+                    while ((file = reader.read()) != null)
+                    {
+                        createNextDataFile(sessionToken, prefix, spaceCode, projectCode,
+                                containerCode, code, dataSetTypeCode, dataSetCode, dataSetName, file);
+                    }
+                }
+            } else
+            {
+                OPERATION_LOG.info(String.format("Omitted data export for link dataset with permId: %s", dataSetPermId));
+            }
+        }
+    }
+
+    private static void createMetadataJsonFile(final String sessionToken, final char prefix, final String spaceCode, final String projectCode,
+            final String containerCode, final String code, final String dataSetTypeCode, final String dataSetCode, final String dataSetName,
+            final String codeHolderJson) throws IOException
+    {
+        final ISessionWorkspaceProvider sessionWorkspaceProvider = CommonServiceProvider.getSessionWorkspaceProvider();
+        final File sessionWorkspaceDirectory = sessionWorkspaceProvider.getSessionWorkspace(sessionToken).getCanonicalFile();
+        final File dataDirectory = new File(sessionWorkspaceDirectory, DATA_DIRECTORY);
+        mkdirs(dataDirectory);
+
+        final File metadataFile = new File(dataDirectory,
+                getDataDirectoryName(prefix, spaceCode, projectCode, containerCode, code, dataSetTypeCode, dataSetCode, dataSetName, META_FILE_NAME));
+
+        final File dataSubdirectory = metadataFile.getParentFile();
+        mkdirs(dataSubdirectory);
+
+        try (final OutputStream os = new BufferedOutputStream(new FileOutputStream(metadataFile)))
+        {
+            writeInChunks(os, codeHolderJson.getBytes(StandardCharsets.UTF_8));
+        }
+    }
+
+    private void exportSpacesDoc(final String sessionToken, final Map<String, Map<String, List<Map<String, String>>>> exportFields,
+            final EntitiesVo entitiesVo, final Set<ExportFormat> exportFormats, final File docDirectory) throws IOException
+    {
+        createFilesAndFoldersForSpacesOfEntities(sessionToken, docDirectory, entitiesVo.getSpaces(), exportFields, exportFormats);
+        createFilesAndFoldersForSpacesOfEntities(sessionToken, docDirectory, entitiesVo.getProjects(), exportFields, exportFormats);
+        createFilesAndFoldersForSpacesOfEntities(sessionToken, docDirectory, entitiesVo.getExperiments(), exportFields, exportFormats);
+        createFilesAndFoldersForSpacesOfEntities(sessionToken, docDirectory, entitiesVo.getSamples(), exportFields, exportFormats);
+    }
+
+    private void createFilesAndFoldersForSpacesOfEntities(final String sessionToken, final File docDirectory, final Collection<?> entities,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final Set<ExportFormat> exportFormats) throws IOException
+    {
+        final boolean hasHtmlFormat = exportFormats.contains(ExportFormat.HTML);
+        final boolean hasPdfFormat = exportFormats.contains(ExportFormat.PDF);
+
+        for (final Object entity : entities)
+        {
+            if (entity instanceof Space)
+            {
+                final Space space = (Space) entity;
+
+                final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = getEntityTypeExportFieldsMap(exportFields, SPACE);
+                final String html = getHtml(sessionToken, space, entityTypeExportFieldsMap);
+                final byte[] htmlBytes = html.getBytes(StandardCharsets.UTF_8);
+
+                if (hasHtmlFormat)
+                {
+                    final File htmlFile = createNextDocFile(docDirectory, space.getCode(), null, null, null, null, null, null, null, HTML_EXTENSION);
+                    try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(htmlFile), BUFFER_SIZE))
+                    {
+                        writeInChunks(bos, htmlBytes);
+                        bos.flush();
+                    }
+                }
+
+                if (hasPdfFormat)
+                {
+                    final File pdfFile = createNextDocFile(docDirectory, space.getCode(), null, null, null, null, null, null, null, PDF_EXTENSION);
+                    try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(pdfFile), BUFFER_SIZE))
+                    {
+                        final PdfRendererBuilder builder = new PdfRendererBuilder();
+                        builder.withHtmlContent(html, null);
+                        builder.toStream(bos);
+                        builder.run();
+                    }
+                }
+            } else
+            {
+                final String spaceCode = getSpaceCode(entity);
+                final String folderName = spaceCode == null && entity instanceof Sample ? SHARED_SAMPLES_DIRECTORY : spaceCode;
+                final File space = createNextDocFile(docDirectory, folderName, null, null, null, null, null, null, null, null);
+                mkdirs(space);
+            }
+        }
+    }
+
+    private void exportProjectsDoc(final String sessionToken, final File docDirectory, final EntitiesVo entitiesVo,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final Set<ExportFormat> exportFormats) throws IOException
+    {
+        createFilesAndFoldersForProjectsOfEntities(sessionToken, docDirectory, entitiesVo.getProjects(), exportFields, exportFormats);
+        createFilesAndFoldersForProjectsOfEntities(sessionToken, docDirectory, entitiesVo.getExperiments(), exportFields, exportFormats);
+        createFilesAndFoldersForProjectsOfEntities(sessionToken, docDirectory, entitiesVo.getSamples(), exportFields, exportFormats);
+    }
+
+    private void createFilesAndFoldersForProjectsOfEntities(final String sessionToken, final File docDirectory, final Collection<?> entities,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final Set<ExportFormat> exportFormats) throws IOException
+    {
+        for (final Object entity : entities)
+        {
+            if (entity instanceof Project)
+            {
+                final Project project = (Project) entity;
+                final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = getEntityTypeExportFieldsMap(exportFields, PROJECT);
+                createDocFilesForEntity(sessionToken, docDirectory, entityTypeExportFieldsMap, project,
+                        project.getSpace().getCode(), project.getCode(), null, null, null, null, null, null,
+                        exportFormats);
+            } else
+            {
+                final String projectCode = getProjectCode(entity);
+                if (projectCode != null)
+                {
+                    final File space = createNextDocFile(docDirectory, getSpaceCode(entity), projectCode, null, null, null, null, null, null, null);
+                    mkdirs(space);
+                }
+            }
+        }
+    }
+
+    private void exportExperimentsDoc(final String sessionToken, final File docDirectory, final EntitiesVo entitiesVo,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final Set<ExportFormat> exportFormats) throws IOException
+    {
+        createFilesAndFoldersForExperimentsOfEntities(sessionToken, docDirectory, entitiesVo.getExperiments(), exportFields, exportFormats);
+        createFilesAndFoldersForExperimentsOfEntities(sessionToken, docDirectory, entitiesVo.getSamples(), exportFields, exportFormats);
+        createFilesAndFoldersForExperimentsOfEntities(sessionToken, docDirectory, entitiesVo.getDataSets(), exportFields, exportFormats);
+    }
+
+    private void createFilesAndFoldersForExperimentsOfEntities(final String sessionToken, final File docDirectory,
+            final Collection<?> entities, final Map<String, Map<String, List<Map<String, String>>>> exportFields,
+            final Set<ExportFormat> exportFormats) throws IOException
+    {
+        for (final Object entity : entities)
+        {
+            if (entity instanceof IExperimentHolder)
+            {
+                final Experiment experiment = ((IExperimentHolder) entity).getExperiment();
+                if (experiment != null)
+                {
+                    final Project project = experiment.getProject();
+                    final File docFile = createNextDocFile(docDirectory, project.getSpace().getCode(), project.getCode(), experiment.getCode(),
+                            getEntityName(experiment), null, null, null, null, null);
+                    mkdirs(docFile);
+                }
+            }
+
+            if (entity instanceof Experiment)
+            {
+                final Experiment experiment = (Experiment) entity;
+                final Project project = experiment.getProject();
+                final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = getEntityTypeExportFieldsMap(exportFields, EXPERIMENT);
+                createDocFilesForEntity(sessionToken, docDirectory, entityTypeExportFieldsMap, experiment,
+                        project.getSpace().getCode(), project.getCode(), experiment.getCode(), getEntityName(experiment), null, null, null, null,
+                        exportFormats);
+            }
+        }
+    }
+
+    private void exportSamplesDoc(final String sessionToken, final File docDirectory, final EntitiesVo entitiesVo,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final Set<ExportFormat> exportFormats)
+            throws IOException
+    {
+        createFilesAndFoldersForSamplesOfEntities(sessionToken, docDirectory, entitiesVo.getSamples(), exportFields, exportFormats);
+    }
+
+    private static Map<String, List<Map<String, String>>> getEntityTypeExportFieldsMap(
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final ExportableKind exportableKind)
+    {
+        return exportFields == null
+                ? null
+                : exportFields.get(MASTER_DATA_EXPORTABLE_KINDS.contains(exportableKind) || exportableKind == SPACE || exportableKind == PROJECT
+                ? TYPE_EXPORT_FIELD_KEY : exportableKind.toString());
+    }
+
+    private void createFilesAndFoldersForSamplesOfEntities(final String sessionToken, final File docDirectory,
+            final Collection<?> entities, final Map<String, Map<String, List<Map<String, String>>>> exportFields,
+            final Set<ExportFormat> exportFormats) throws IOException
+    {
+        for (final Object entity : entities)
+        {
+            if (entity instanceof ISampleHolder)
+            {
+                final Sample sample = ((ISampleHolder) entity).getSample();
+                final Experiment experiment = sample.getExperiment();
+                final File docFile;
+
+                if (experiment != null)
+                {
+                    final Project project = experiment.getProject();
+                    docFile = createNextDocFile(docDirectory, project.getSpace().getCode(), project.getCode(), experiment.getCode(),
+                            getEntityName(experiment), null, null, null, null, null);
+                } else
+                {
+                    final Project project = sample.getProject();
+                    if (project != null)
+                    {
+                        docFile = createNextDocFile(docDirectory, project.getSpace().getCode(), project.getCode(), null,
+                                null, null, null, null, null, null);
+                    } else
+                    {
+                        final Space space = sample.getSpace();
+                        docFile = createNextDocFile(docDirectory, space != null ? space.getCode() : SHARED_SAMPLES_DIRECTORY, null, null,
+                                null, null, null, null, null, null);
+                    }
+                }
+
+                mkdirs(docFile);
+            }
+
+            if (entity instanceof Sample)
+            {
+                final Sample sample = (Sample) entity;
+                final Experiment experiment = sample.getExperiment();
+                final Sample container = sample.getContainer();
+
+                final String spaceCode = getSpaceCode(sample);
+                final String spaceFolder = spaceCode != null ? spaceCode : SHARED_SAMPLES_DIRECTORY;
+                final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = getEntityTypeExportFieldsMap(exportFields, SAMPLE);
+
+                createDocFilesForEntity(sessionToken, docDirectory, entityTypeExportFieldsMap, sample,
+                        spaceFolder, getProjectCode(sample), experiment != null ? experiment.getCode() : null,
+                        experiment != null ? getEntityName(experiment) : null, container != null ? container.getCode() : null, sample.getCode(),
+                        getEntityName(sample), null, exportFormats);
+            }
+        }
+    }
+
+    private void exportDataSetsDoc(final String sessionToken, final File docDirectory, final EntitiesVo entitiesVo,
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final Set<ExportFormat> exportFormats) throws IOException
+    {
+        createFilesAndFoldersForDataSetsOfEntities(sessionToken, docDirectory, entitiesVo.getDataSets(), exportFields, exportFormats);
+    }
+
+    private void createFilesAndFoldersForDataSetsOfEntities(final String sessionToken, final File docDirectory,
+            final Collection<?> entities, final Map<String, Map<String, List<Map<String, String>>>> exportFields,
+            final Set<ExportFormat> exportFormats) throws IOException
+    {
+        for (final Object entity : entities)
+        {
+            if (entity instanceof DataSet)
+            {
+                final DataSet dataSet = (DataSet) entity;
+                final Sample sample = dataSet.getSample();
+                final Sample container = sample != null ? sample.getContainer() : null;
+                final Experiment experiment = sample != null ? sample.getExperiment() : dataSet.getExperiment();
+                final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = getEntityTypeExportFieldsMap(exportFields, DATASET);
+
+                createDocFilesForEntity(sessionToken, docDirectory, entityTypeExportFieldsMap, dataSet,
+                        getSpaceCode(entity), getProjectCode(entity), experiment != null ? experiment.getCode() : null,
+                        experiment != null ? getEntityName(experiment) : null, container != null ? container.getCode() : null,
+                        sample != null ? sample.getCode() : null, sample != null ? getEntityName(sample) : null, dataSet.getCode(), exportFormats);
+            }
+        }
+    }
+
+    private static String getSpaceCode(final Object entity)
+    {
+        if (entity instanceof Space)
+        {
+            return getSpaceCode((Space) entity);
+        } else if (entity instanceof Project)
+        {
+            return getSpaceCode((Project) entity);
+        } else if (entity instanceof Experiment)
+        {
+            return getSpaceCode((Experiment) entity);
+        } else if (entity instanceof Sample)
+        {
+            return getSpaceCode((Sample) entity);
+        } else if (entity instanceof DataSet)
+        {
+            return getSpaceCode((DataSet) entity);
+        } else
+        {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private static String getSpaceCode(final Space entity)
+    {
+        return entity.getCode();
+    }
+
+    private static String getSpaceCode(final Project entity)
+    {
+        return entity.getSpace().getCode();
+    }
+
+    private static String getSpaceCode(final Experiment entity)
+    {
+        return entity.getProject().getSpace().getCode();
+    }
+
+    private static String getSpaceCode(final Sample sample)
+    {
+        final Space space = sample.getSpace();
+        if (space != null)
+        {
+            return sample.getSpace().getCode();
+        } else
+        {
+            final Experiment experiment = sample.getExperiment();
+            final Project project = sample.getProject();
+            if (experiment != null)
+            {
+                return experiment.getProject().getSpace().getCode();
+            } else if (project != null)
+            {
+                return project.getSpace().getCode();
+            } else
+            {
+                return null;
+            }
+        }
+    }
+
+    private static String getSpaceCode(final DataSet dataSet)
+    {
+        final Sample sample = dataSet.getSample();
+        return sample != null ? getSpaceCode(sample) :  getSpaceCode(dataSet.getExperiment());
+    }
+
+    private static String getProjectCode(final Object entity)
+    {
+        if (entity instanceof Project)
+        {
+            return getProjectCode((Project) entity);
+        } else if (entity instanceof Experiment)
+        {
+            return getProjectCode((Experiment) entity);
+        } else if (entity instanceof Sample)
+        {
+            return getProjectCode((Sample) entity);
+        } else if (entity instanceof DataSet)
+        {
+            return getProjectCode((DataSet) entity);
+        } else
+        {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private static String getProjectCode(final Project project)
+    {
+        return project.getCode();
+    }
+
+    private static String getProjectCode(final Experiment experiment)
+    {
+        return experiment.getProject().getCode();
+    }
+
+    private static String getProjectCode(final Sample sample)
+    {
+        final Project project = getProjectForSample(sample);
+        return project != null ? project.getCode() : null;
+    }
+
+    private static String getProjectCode(final DataSet dataSet)
+    {
+        final Sample sample = dataSet.getSample();
+        return sample != null ? getProjectCode(sample) : getProjectCode(dataSet.getExperiment());
+    }
+
+    private static Project getProjectForSample(final Sample sample)
+    {
+        final Experiment experiment = sample.getExperiment();
+        if (experiment != null)
+        {
+            return experiment.getProject();
+        } else
+        {
+            return sample.getProject();
+        }
+    }
+
+    private static void writeInChunks(final OutputStream os, final byte[] bytes) throws IOException
+    {
+        final int length = bytes.length;
+        for (int pos = 0; pos < length; pos += BUFFER_SIZE)
+        {
+            os.write(Arrays.copyOfRange(bytes, pos, Math.min(pos + BUFFER_SIZE, length)));
+        }
+        os.flush();
+    }
+
+    private static void writeInChunks(final OutputStream os, final InputStream is) throws IOException
+    {
+        final byte[] buffer = new byte[BUFFER_SIZE];
+        int length;
+        while ((length = is.read(buffer)) > 0)
+        {
+            os.write(buffer, 0, length);
+        }
+        os.flush();
+    }
+
+    private static File createNextDocFile(final File docDirectory, final String spaceCode, final String projectCode, final String experimentCode,
+            final String experimentName, final String containerCode, final String sampleCode, final String sampleName, final String dataSetCode,
+            final String extension)
+    {
+        return new File(docDirectory, getNextDocDirectoryName(spaceCode, projectCode, experimentCode, experimentName, containerCode, sampleCode,
+                sampleName, dataSetCode, extension));
+    }
+
+    private void createDocFilesForEntity(final String sessionToken, final File docDirectory,
+            final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap,
+            final ICodeHolder entity, final String spaceCode, final String projectCode, final String experimentCode,
+            final String experimentName, final String containerCode, final String sampleCode, final String sampleName, final String dataSetCode,
+            final Set<ExportFormat> exportFormats) throws IOException
+    {
+        final boolean hasHtmlFormat = exportFormats.contains(ExportFormat.HTML);
+        final boolean hasPdfFormat = exportFormats.contains(ExportFormat.PDF);
+        final String html = getHtml(sessionToken, entity, entityTypeExportFieldsMap);
+        final byte[] htmlBytes = html.getBytes(StandardCharsets.UTF_8);
+
+        if (hasHtmlFormat)
+        {
+            final File htmlFile = createNextDocFile(docDirectory, spaceCode, projectCode, experimentCode, experimentName, containerCode, sampleCode,
+                    sampleName, dataSetCode, HTML_EXTENSION);
+            try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(htmlFile), BUFFER_SIZE))
+            {
+                writeInChunks(bos, htmlBytes);
+                bos.flush();
+            }
+        }
+
+        if (hasPdfFormat)
+        {
+            final File pdfFile = createNextDocFile(docDirectory, spaceCode, projectCode, experimentCode, experimentName, containerCode, sampleCode,
+                    sampleName, dataSetCode, PDF_EXTENSION);
+            try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(pdfFile), BUFFER_SIZE))
+            {
+                final PdfRendererBuilder builder = new PdfRendererBuilder();
+                builder.withHtmlContent(html, null);
+                builder.toStream(bos);
+                builder.run();
+            }
+        }
+    }
+
+    static String getNextDocDirectoryName(final String spaceCode, final String projectCode, final String experimentCode, final String experimentName,
+            final String containerCode, final String sampleCode, final String sampleName, final String dataSetCode, final String extension)
+    {
+        final StringBuilder entryBuilder = new StringBuilder();
+
+        if (spaceCode == null && (projectCode != null || experimentCode != null || dataSetCode != null || (sampleCode == null && extension != null)))
+        {
+            throw new IllegalArgumentException();
+        } else if (spaceCode != null)
+        {
+            entryBuilder.append(spaceCode);
+        }
+
+        if (projectCode != null)
+        {
+            entryBuilder.append('/').append(projectCode);
+            if (experimentCode != null)
+            {
+                entryBuilder.append('/');
+                addFullEntityName(entryBuilder, null, experimentCode, experimentName);
+
+                if (sampleCode == null && dataSetCode != null)
+                {
+                    // Experiment data set
+                    entryBuilder.append('/').append(dataSetCode);
+                }
+            } else if (sampleCode == null && dataSetCode != null)
+            {
+                throw new IllegalArgumentException();
+            }
+        } else if (experimentCode != null || (dataSetCode != null && sampleCode == null))
+        {
+            throw new IllegalArgumentException();
+        }
+
+        if (sampleCode != null)
+        {
+            if (spaceCode != null)
+            {
+                entryBuilder.append('/');
+            }
+            addFullEntityName(entryBuilder, containerCode, sampleCode, sampleName);
+
+            if (dataSetCode != null)
+            {
+                // Sample data set
+                entryBuilder.append('/').append(dataSetCode);
+            }
+        }
+
+        entryBuilder.append(extension != null ? extension : '/');
+        return entryBuilder.toString();
+    }
+
+    private static void createNextDataFile(final String sessionToken, final char prefix, final String spaceCode, final String projectCode,
+            final String containerCode, final String entityCode, final String dataSetTypeCode, final String dataSetCode,
+            final String dataSetName, final DataSetFileDownload dataSetFileDownload) throws IOException
+    {
+        final ISessionWorkspaceProvider sessionWorkspaceProvider = CommonServiceProvider.getSessionWorkspaceProvider();
+        final File sessionWorkspaceDirectory = sessionWorkspaceProvider.getSessionWorkspace(sessionToken).getCanonicalFile();
+
+        final DataSetFile dataSetFile = dataSetFileDownload.getDataSetFile();
+        final String filePath = dataSetFile.getPath();
+        final boolean isDirectory = dataSetFile.isDirectory();
+
+        final File dataDirectory = new File(sessionWorkspaceDirectory, DATA_DIRECTORY);
+        mkdirs(dataDirectory);
+
+        final File dataSetFsEntry = new File(dataDirectory, getDataDirectoryName(prefix, spaceCode, projectCode, containerCode, entityCode,
+                dataSetTypeCode, dataSetCode, dataSetName, filePath) + (isDirectory ? "/" : ""));
+
+        final File dataSubdirectory = dataSetFsEntry.getParentFile();
+        mkdirs(dataSubdirectory);
+
+        if (!isDirectory)
+        {
+            try (
+                    final InputStream is = dataSetFileDownload.getInputStream();
+                    final BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(dataSetFsEntry))
+            )
+            {
+                writeInChunks(os, is);
+            }
+        } else
+        {
+            mkdirs(dataSetFsEntry);
+        }
+    }
+
+    static String getDataDirectoryName(final char prefix, final String spaceCode, final String projectCode,
+            final String containerCode, final String entityCode, final String dataSetTypeCode,
+            final String dataSetCode, final String dataSetName, final String fileName)
+    {
+        if (prefix != 'O' && prefix != 'E')
+        {
+            throw new IllegalArgumentException(String.format("Only 'O' and 'E' can be used as prefix got '%c' instead.", prefix));
+        }
+
+        if (containerCode != null && prefix != 'O')
+        {
+            throw new IllegalArgumentException("Only objects can have containers.");
+        }
+
+        final StringBuilder entryBuilder = new StringBuilder(String.valueOf(prefix));
+
+        if (spaceCode != null)
+        {
+            entryBuilder.append('+').append(spaceCode);
+        } else if (prefix == 'E')
+        {
+            throw new IllegalArgumentException("Space code cannot be null for experiments.");
+        } else if (projectCode != null)
+        {
+            throw new IllegalArgumentException("If space code is null project code should be also null.");
+        }
+
+        if (projectCode != null)
+        {
+            entryBuilder.append('+').append(projectCode);
+        } else if (prefix == 'E')
+        {
+            throw new IllegalArgumentException("Project code cannot be null for experiments.");
+        }
+
+        if (entityCode != null)
+        {
+            entryBuilder.append('+');
+            addFullEntityCode(entryBuilder, containerCode, entityCode);
+        } else
+        {
+            throw new IllegalArgumentException("Entity code is mandatory");
+        }
+
+        if (dataSetTypeCode != null)
+        {
+            entryBuilder.append('+').append(dataSetTypeCode);
+        } else
+        {
+            throw new IllegalArgumentException("Data set type code is mandatory");
+        }
+
+        if (dataSetCode != null)
+        {
+            entryBuilder.append('+');
+            addFullEntityName(entryBuilder, null, dataSetCode, dataSetName);
+        } else
+        {
+            throw new IllegalArgumentException("Data set code is mandatory");
+        }
+
+        if (fileName != null)
+        {
+            entryBuilder.append('/').append(fileName);
+        }
+
+        return entryBuilder.toString();
+    }
+
+    private static void addFullEntityName(final StringBuilder entryBuilder, final String containerCode, final String entityCode,
+            final String entityName)
+    {
+        if (entityName == null || entityName.isEmpty())
+        {
+            addFullEntityCode(entryBuilder, containerCode, entityCode);
+        } else
+        {
+            entryBuilder.append(entityName).append(" (");
+            addFullEntityCode(entryBuilder, containerCode, entityCode);
+            entryBuilder.append(")");
+        }
+    }
+
+    private static void addFullEntityCode(final StringBuilder entryBuilder, final String containerCode, final String entityCode)
+    {
+        if (containerCode != null)
+        {
+            entryBuilder.append(containerCode).append('*');
+        }
+
+        entryBuilder.append(entityCode);
+    }
+
+    private static String getEntityName(final IPropertiesHolder entity)
+    {
+        try
+        {
+            return escapeUnsafeCharacters(entity.getVarcharProperty(NAME_PROPERTY_NAME));
+        } catch (final NotFetchedException e)
+        {
+            return null;
+        }
+    }
+
+    static String escapeUnsafeCharacters(final String name)
+    {
+        return name != null ? name.replaceAll(UNSAFE_CHARACTERS_REGEXP, "_") : null;
+    }
+
+    private String getHtml(final String sessionToken, final ICodeHolder entityObj,
+            final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap) throws IOException
+    {
+        final IApplicationServerInternalApi v3 = CommonServiceProvider.getApplicationServerApi();
+
+        final DocumentBuilder documentBuilder = new DocumentBuilder();
+        documentBuilder.addTitle(entityObj.getCode());
+        documentBuilder.addHeader("Identification Info");
+
+        final IEntityType typeObj;
+        if (entityObj instanceof Experiment)
+        {
+            documentBuilder.addProperty(KIND_DOCUMENT_PROPERTY_ID, "Experiment");
+            final ExperimentTypeSearchCriteria searchCriteria = new ExperimentTypeSearchCriteria();
+            searchCriteria.withCode().thatEquals(((Experiment) entityObj).getType().getCode());
+            final ExperimentTypeFetchOptions fetchOptions = new ExperimentTypeFetchOptions();
+            fetchOptions.withPropertyAssignments().withPropertyType();
+            final SearchResult<ExperimentType> results = v3.searchExperimentTypes(sessionToken, searchCriteria, fetchOptions);
+            typeObj = results.getObjects().get(0);
+        } else if (entityObj instanceof Sample)
+        {
+            documentBuilder.addProperty(KIND_DOCUMENT_PROPERTY_ID, "Sample");
+            final SampleTypeSearchCriteria searchCriteria = new SampleTypeSearchCriteria();
+            searchCriteria.withCode().thatEquals(((Sample) entityObj).getType().getCode());
+            final SampleTypeFetchOptions fetchOptions = new SampleTypeFetchOptions();
+            fetchOptions.withPropertyAssignments().withPropertyType();
+            final SearchResult<SampleType> results = v3.searchSampleTypes(sessionToken, searchCriteria, fetchOptions);
+            typeObj = results.getObjects().get(0);
+        } else if (entityObj instanceof DataSet)
+        {
+            final DataSet dataSet = (DataSet) entityObj;
+            documentBuilder.addProperty(KIND_DOCUMENT_PROPERTY_ID, "DataSet");
+            final DataSetTypeSearchCriteria searchCriteria = new DataSetTypeSearchCriteria();
+            searchCriteria.withCode().thatEquals(dataSet.getType().getCode());
+            final DataSetTypeFetchOptions fetchOptions = new DataSetTypeFetchOptions();
+            fetchOptions.withPropertyAssignments().withPropertyType();
+            final SearchResult<DataSetType> results = v3.searchDataSetTypes(sessionToken, searchCriteria, fetchOptions);
+            typeObj = results.getObjects().get(0);
+        } else
+        {
+            typeObj = null;
+        }
+
+        if (entityObj instanceof Project)
+        {
+            documentBuilder.addProperty(KIND_DOCUMENT_PROPERTY_ID, "Project");
+        } else if (entityObj instanceof Space)
+        {
+            documentBuilder.addProperty(KIND_DOCUMENT_PROPERTY_ID, "Space");
+        } else
+        {
+            documentBuilder.addProperty("Type", ((IEntityTypeHolder) entityObj).getType().getCode());
+        }
+
+        final List<Map<String, String>> selectedExportFields;
+        if (entityTypeExportFieldsMap == null || entityTypeExportFieldsMap.isEmpty())
+        {
+            selectedExportFields = null;
+        } else if (typeObj != null)
+        {
+            selectedExportFields = entityTypeExportFieldsMap.get(typeObj.getCode());
+        } else if (entityObj instanceof Space)
+        {
+            selectedExportFields = entityTypeExportFieldsMap.get(SPACE.name());
+        } else if (entityObj instanceof Project)
+        {
+            selectedExportFields = entityTypeExportFieldsMap.get(PROJECT.name());
+        } else
+        {
+            selectedExportFields = null;
+        }
+
+        final Set<String> selectedExportAttributes = selectedExportFields != null
+                ? selectedExportFields.stream().filter(map -> Objects.equals(map.get(FIELD_TYPE_KEY), ATTRIBUTE.name()))
+                .map(map -> map.get(FIELD_ID_KEY)).collect(Collectors.toSet())
+                : null;
+
+        final Set<String> selectedExportProperties = selectedExportFields != null
+                ? selectedExportFields.stream().filter(map -> Objects.equals(map.get(FIELD_TYPE_KEY), PROPERTY.name()))
+                .map(map -> map.get(FIELD_ID_KEY)).collect(Collectors.toSet())
+                : null;
+
+        if (allowsValue(selectedExportAttributes, Attribute.CODE.name()))
+        {
+            documentBuilder.addProperty("Code", entityObj.getCode());
+        }
+
+        if (entityObj instanceof IPermIdHolder && allowsValue(selectedExportAttributes, Attribute.PERM_ID.name()))
+        {
+            documentBuilder.addProperty("Perm ID", ((IPermIdHolder) entityObj).getPermId().toString());
+        }
+
+        if (entityObj instanceof IIdentifierHolder && allowsValue(selectedExportAttributes, Attribute.IDENTIFIER.name()))
+        {
+            final ObjectIdentifier identifier = ((IIdentifierHolder) entityObj).getIdentifier();
+            if (identifier != null)
+            {
+                documentBuilder.addProperty("Identifier", identifier.getIdentifier());
+            }
+        }
+
+        if (entityObj instanceof IRegistratorHolder && allowsValue(selectedExportAttributes, Attribute.REGISTRATOR.name()))
+        {
+            final Person registrator = ((IRegistratorHolder) entityObj).getRegistrator();
+            if (registrator != null)
+            {
+                documentBuilder.addProperty("Registrator", registrator.getUserId());
+            }
+        }
+
+        if (entityObj instanceof IRegistrationDateHolder && allowsValue(selectedExportAttributes, Attribute.REGISTRATION_DATE.name()))
+        {
+            final Date registrationDate = ((IRegistrationDateHolder) entityObj).getRegistrationDate();
+            if (registrationDate != null)
+            {
+                documentBuilder.addProperty("Registration Date", String.valueOf(registrationDate));
+            }
+        }
+
+        if (entityObj instanceof IModifierHolder && allowsValue(selectedExportAttributes, Attribute.MODIFIER.name()))
+        {
+            final Person modifier = ((IModifierHolder) entityObj).getModifier();
+            if (modifier != null)
+            {
+                documentBuilder.addProperty("Modifier", modifier.getUserId());
+            }
+        }
+
+        if (entityObj instanceof IModificationDateHolder && allowsValue(selectedExportAttributes, Attribute.MODIFICATION_DATE.name()))
+        {
+            final Date modificationDate = ((IModificationDateHolder) entityObj).getModificationDate();
+            if (modificationDate != null)
+            {
+                documentBuilder.addProperty("Modification Date", String.valueOf(modificationDate));
+            }
+        }
+
+        if (entityObj instanceof IDescriptionHolder && allowsValue(selectedExportAttributes, Attribute.DESCRIPTION.name()))
+        {
+            final String description = ((IDescriptionHolder) entityObj).getDescription();
+            if (description != null)
+            {
+                documentBuilder.addHeader("Description");
+                documentBuilder.addParagraph(description);
+            }
+        }
+
+        if (entityObj instanceof IParentChildrenHolder<?>)
+        {
+            final IParentChildrenHolder<?> parentChildrenHolder = (IParentChildrenHolder<?>) entityObj;
+            if (allowsValue(selectedExportAttributes, Attribute.PARENTS.name()))
+            {
+                documentBuilder.addHeader("Parents");
+                final List<?> parents = parentChildrenHolder.getParents();
+                for (final Object parent : parents)
+                {
+                    final String relCodeName = ((ICodeHolder) parent).getCode();
+                    final Map<String, Serializable> properties = ((IPropertiesHolder) parent).getProperties();
+                    final String name = getEntityName((IPropertiesHolder) parent);
+                    documentBuilder.addParagraph(relCodeName + (name != null ? " (" + properties.get("NAME") + ")" : ""));
+                }
+            }
+
+            if (allowsValue(selectedExportAttributes, Attribute.CHILDREN.name()))
+            {
+                documentBuilder.addHeader("Children");
+                final List<?> children = parentChildrenHolder.getChildren();
+                for (final Object child : children)
+                {
+                    final String relCodeName = ((ICodeHolder) child).getCode();
+                    final Map<String, Serializable> properties = ((IPropertiesHolder) child).getProperties();
+                    final String name = getEntityName((IPropertiesHolder) child);
+                    documentBuilder.addParagraph(relCodeName + (name != null ? " (" + properties.get("NAME") + ")" : ""));
+                }
+            }
+        }
+
+        if (entityObj instanceof IPropertiesHolder)
+        {
+            documentBuilder.addHeader("Properties");
+            if (typeObj != null)
+            {
+                final List<PropertyAssignment> propertyAssignments = typeObj.getPropertyAssignments();
+                if (propertyAssignments != null)
+                {
+                    final Map<String, Serializable> properties = ((IPropertiesHolder) entityObj).getProperties();
+                    for (final PropertyAssignment propertyAssignment : propertyAssignments)
+                    {
+                        System.out.println(selectedExportFields);
+
+                        final PropertyType propertyType = propertyAssignment.getPropertyType();
+                        final String propertyTypeCode = propertyType.getCode();
+                        final Object rawPropertyValue = properties.get(propertyTypeCode);
+
+                        if (rawPropertyValue != null && allowsValue(selectedExportProperties, propertyTypeCode))
+                        {
+                            final String initialPropertyValue = String.valueOf(rawPropertyValue);
+                            final String propertyValue;
+
+                            if (propertyType.getDataType() == DataType.MULTILINE_VARCHAR &&
+                                    Objects.equals(propertyType.getMetaData().get("custom_widget"), "Word Processor"))
+                            {
+                                final StringBuilder propertyValueBuilder = new StringBuilder(initialPropertyValue);
+                                final Document doc = Jsoup.parse(initialPropertyValue);
+                                final Elements imageElements = doc.select("img");
+                                for (final Element imageElement : imageElements)
+                                {
+                                    final String imageSrc = imageElement.attr("src");
+                                    replaceAll(propertyValueBuilder, imageSrc, encodeImageContentToString(imageSrc));
+                                }
+                                propertyValue = propertyValueBuilder.toString();
+                            } else if (propertyType.getDataType() == DataType.XML
+                                    && Objects.equals(propertyType.getMetaData().get("custom_widget"), "Spreadsheet")
+                                    && initialPropertyValue.toUpperCase().startsWith(DATA_TAG_START) && initialPropertyValue.toUpperCase()
+                                    .endsWith(DATA_TAG_END))
+                            {
+                                final String subString = initialPropertyValue.substring(DATA_TAG_START_LENGTH,
+                                        initialPropertyValue.length() - DATA_TAG_END_LENGTH);
+                                final String decodedString = new String(Base64.getDecoder().decode(subString), StandardCharsets.UTF_8);
+                                final ObjectMapper objectMapper = new ObjectMapper();
+                                final JsonNode jsonNode = objectMapper.readTree(decodedString);
+                                propertyValue = convertJsonToHtml(jsonNode);
+                            } else
+                            {
+                                propertyValue = initialPropertyValue;
+                            }
+
+                            if (!Objects.equals(propertyValue, "\uFFFD(undefined)"))
+                            {
+                                documentBuilder.addProperty(propertyType.getLabel(), propertyValue);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return documentBuilder.getHtml();
+    }
+
+    private String encodeImageContentToString(final String imageSrc) throws IOException
+    {
+        final Base64.Encoder encoder = Base64.getEncoder();
+        final String extension = imageSrc.substring(imageSrc.lastIndexOf('.'));
+        final String mediaType = MEDIA_TYPE_BY_EXTENSION.getOrDefault(extension, DEFAULT_MEDIA_TYPE);
+        final String dataPrefix = String.format(DATA_PREFIX_TEMPLATE, mediaType);
+        final String filePath = getFilesRepository().getCanonicalPath() + "/" + imageSrc;
+
+        final StringBuilder result = new StringBuilder(dataPrefix);
+        final FileInputStream fileInputStream = new FileInputStream(filePath);
+        try (final BufferedInputStream in = new BufferedInputStream(fileInputStream, BUFFER_SIZE))
+        {
+            byte[] chunk = new byte[BUFFER_SIZE];
+            int len;
+            while ((len = in.read(chunk)) == BUFFER_SIZE) {
+                result.append(encoder.encodeToString(chunk));
+            }
+
+            if (len > 0) {
+                chunk = Arrays.copyOf(chunk, len);
+                result.append(encoder.encodeToString(chunk));
+            }
+        }
+
+        return result.toString();
+    }
+
+    /**
+     * Whether the set does not forbid a value.
+     *
+     * @param set the set to look in
+     * @param value the value to be found
+     * @return <code>true</code> if set is <code>null</code> or value is in the set
+     */
+    private boolean allowsValue(final Set<String> set, final String value)
+    {
+        return set == null || set.contains(value);
+    }
+
+    private static String convertJsonToHtml(final TreeNode node) throws IOException
+    {
+        final TreeNode data = node.get("data");
+        final TreeNode styles = node.get("style");
+
+        final StringBuilder tableBody = new StringBuilder();
+        for (int i = 0; i < data.size(); i++)
+        {
+            final TreeNode dataRow = data.get(i);
+            tableBody.append("<tr>\n");
+            for (int j = 0; j < dataRow.size(); j++)
+            {
+                final String stylesKey = convertNumericToAlphanumeric(i, j);
+                final String style = ((TextNode) styles.get(stylesKey)).textValue();
+                final TextNode cell = (TextNode) dataRow.get(j);
+                tableBody.append("  <td style='").append(COMMON_STYLE).append(" ").append(style).append("'> ").append(cell.textValue())
+                        .append(" </td>\n");
+            }
+            tableBody.append("</tr>\n");
+        }
+        return String.format("<table style='%s'>\n%s\n%s", TABLE_STYLE, tableBody, "</table>");
+    }
+
+    private static String convertNumericToAlphanumeric(final int row, final int col)
+    {
+        final int aCharCode = (int) 'A';
+        final int ord0 = col % 26;
+        final int ord1 = col / 26;
+        final char char0 = (char) (aCharCode + ord0);
+        final char char1 = (char) (aCharCode + ord1 - 1);
+        return String.valueOf(ord1 > 0 ? char1 : "") + char0 + (row + 1);
+    }
+
+    private static void replaceAll(final StringBuilder sb, final String target, final String replacement)
+    {
+        // Start index for the first search
+        int startIndex = sb.indexOf(target);
+        while (startIndex != -1)
+        {
+            final int endIndex = startIndex + target.length();
+            sb.replace(startIndex, endIndex, replacement);
+            // Update the start index for the next search
+            startIndex = sb.indexOf(target, startIndex + replacement.length());
+        }
+    }
+
+    private File getActualFile(final String sessionToken, final String actualResultFilePath)
+    {
+        final File sessionWorkspace = sessionWorkspaceProvider.getSessionWorkspace(sessionToken);
+        final File[] files = sessionWorkspace.listFiles((FilenameFilter) new NameFileFilter(actualResultFilePath));
+
+        assertNotNull(files);
+        assertEquals(1, files.length, String.format("Session workspace should contain only one file with the download URL '%s'.",
+                actualResultFilePath));
+
+        final File file = files[0];
+
+        assertTrue(file.getName().startsWith(METADATA_FILE_PREFIX + "."));
+        return file;
+    }
+
+    private static Map<String, List<Map<String, String>>> findExportAttributes(final ExportableKind exportableKind, final SelectedFields selectedFields)
+    {
+        final List<Map<String, String>> attributes = selectedFields.getAttributes().stream()
+                .map(attribute -> Map.of(IExportFieldsFinder.TYPE, ATTRIBUTE.name(), IExportFieldsFinder.ID, attribute.name()))
+                .collect(Collectors.toList());
+        return Map.of(exportableKind.name(), attributes);
+    }
+
+    private File getFilesRepository()
+    {
+        return new File(configurer.getResolvedProps().getProperty(REPO_PATH_KEY, DEFAULT_REPO_PATH));
+    }
+
+    /**
+     * Safely tries to create a directory if it does not exist. If it could not be created throws an exception.
+     *
+     * @param dir the directory to be created.
+     */
+    private static void mkdirs(final File dir)
+    {
+        if (!dir.isDirectory())
+        {
+            final boolean created = dir.mkdirs();
+            if (!created)
+            {
+                throw new RuntimeException(String.format("Cannot create directory '%s'.", dir.getPath()));
+            }
+        }
+    }
+
+    private static void zipDirectory(final String sourceDirectory, final File targetZipFile) throws IOException {
+        final Path sourceDir = Paths.get(sourceDirectory);
+        try (final ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(targetZipFile)))
+        {
+            try(final Stream<Path> stream = Files.walk(sourceDir))
+            {
+                stream.filter(path -> !path.equals(sourceDir) && !path.toFile().equals(targetZipFile))
+                        .forEach(path ->
+                        {
+                            final boolean isDirectory = Files.isDirectory(path);
+                            final String entryName = sourceDir.relativize(path).toString();
+                            final ZipEntry zipEntry = new ZipEntry(entryName + (isDirectory ? "/" : ""));
+                            try
+                            {
+                                zipOutputStream.putNextEntry(zipEntry);
+                                if (!isDirectory)
+                                {
+                                    Files.copy(path, zipOutputStream);
+                                }
+                                zipOutputStream.closeEntry();
+                            } catch (final IOException e)
+                            {
+                                throw new RuntimeException(e);
+                            }
+                        });
+            }
+        }
+    }
+
+    public static void deleteDirectory(final String directoryPath) throws IOException {
+        final Path path = Paths.get(directoryPath);
+        if (Files.exists(path))
+        {
+            try (final Stream<Path> walkStream = Files.walk(path))
+            {
+                walkStream
+                        .sorted(Comparator.reverseOrder())
+                        .map(Path::toFile)
+                        .forEach(File::delete);
+            }
+        }
+    }
+
+    private static class EntitiesVo
+    {
+
+        private final String sessionToken;
+
+        private final Map<ExportableKind, List<String>> groupedExportablePermIds;
+
+        private Collection<Space> spaces;
+
+        private Collection<Project> projects;
+
+        private Collection<Experiment> experiments;
+
+        private Collection<Sample> samples;
+
+        private Collection<DataSet> dataSets;
+
+        private EntitiesVo(final String sessionToken, final List<ExportablePermId> exportablePermIds)
+        {
+            this.sessionToken = sessionToken;
+            groupedExportablePermIds = getGroupedExportablePermIds(exportablePermIds);
+        }
+
+        private static Map<ExportableKind, List<String>> getGroupedExportablePermIds(final List<ExportablePermId> exportablePermIds)
+        {
+            final Collector<ExportablePermId, List<String>, List<String>> downstreamCollector = Collector.of(ArrayList::new,
+                    (stringPermIds, exportablePermId) -> stringPermIds.add(exportablePermId.getPermId().getPermId()),
+                    (left, right) ->
+                    {
+                        left.addAll(right);
+                        return left;
+                    });
+
+            return exportablePermIds.stream().collect(Collectors.groupingBy(ExportablePermId::getExportableKind, downstreamCollector));
+        }
+
+        public Collection<Space> getSpaces()
+        {
+            if (spaces == null)
+            {
+                spaces = EntitiesFinder.getSpaces(sessionToken, groupedExportablePermIds.getOrDefault(ExportableKind.SPACE, List.of()));
+            }
+            return spaces;
+        }
+
+        public Collection<Project> getProjects()
+        {
+            if (projects == null)
+            {
+                projects = EntitiesFinder.getProjects(sessionToken, groupedExportablePermIds.getOrDefault(ExportableKind.PROJECT, List.of()));
+            }
+
+            return projects;
+        }
+
+        public Collection<Experiment> getExperiments()
+        {
+            if (experiments == null)
+            {
+                experiments = EntitiesFinder.getExperiments(sessionToken, groupedExportablePermIds.getOrDefault(ExportableKind.EXPERIMENT, List.of()));
+            }
+            return experiments;
+        }
+
+        public Collection<Sample> getSamples()
+        {
+            if (samples == null)
+            {
+                samples = EntitiesFinder.getSamples(sessionToken, groupedExportablePermIds.getOrDefault(ExportableKind.SAMPLE, List.of()));
+            }
+            return samples;
+        }
+
+        public Collection<DataSet> getDataSets()
+        {
+            if (dataSets == null)
+            {
+                dataSets = EntitiesFinder.getDataSets(sessionToken, groupedExportablePermIds.getOrDefault(DATASET, List.of()));
+            }
+            return dataSets;
+        }
+
+    }
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExportOperationExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExportOperationExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ba9b6fc68aedb03fdbc668eeb2017e4da0e4c64
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/ExportOperationExecutor.java
@@ -0,0 +1,50 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportOperationResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportResult;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.common.OperationExecutor;
+import ch.ethz.sis.openbis.generic.server.xls.export.XLSExport;
+
+@Component
+public class ExportOperationExecutor extends OperationExecutor<ExportOperation, ExportOperationResult>
+{
+
+    @Autowired
+    private IExportExecutor executor;
+
+    @Override
+    protected Class<? extends ExportOperation> getOperationClass()
+    {
+        return ExportOperation.class;
+    }
+
+    @Override
+    protected ExportOperationResult doExecute(final IOperationContext context, final ExportOperation operation)
+    {
+        final XLSExport.ExportResult exportResult = executor.doExport(context, operation);
+        return new ExportOperationResult(new ExportResult(exportResult.getFileName(), exportResult.getWarnings()));
+    }
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/IExportExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/IExportExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3088cbad138e8a34334e52a82915bf7e18f4969
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/IExportExecutor.java
@@ -0,0 +1,29 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.ExportOperation;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.xls.export.XLSExport.ExportResult;
+
+public interface IExportExecutor
+{
+
+    ExportResult doExport(final IOperationContext context, final ExportOperation operation);
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/IExportFieldsFinder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/IExportFieldsFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..746c4521150731ee7312179008e1b1ee7a89ee53
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/IExportFieldsFinder.java
@@ -0,0 +1,38 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.exporter.data.SelectedFields;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.IPropertyTypeId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+
+public interface IExportFieldsFinder
+{
+
+    String TYPE = "type";
+
+    String ID = "id";
+
+    Map<String, List<Map<String, String>>> findExportFields(Set<IPropertyTypeId> properties,
+            IApplicationServerInternalApi applicationServerApi, String sessionToken, SelectedFields selectedFields);
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/SampleExportFieldsFinder.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/SampleExportFieldsFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..59ceb769f16d7587f960008d62774a22d385ad23
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/exporter/SampleExportFieldsFinder.java
@@ -0,0 +1,51 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter;
+
+import java.util.Set;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.property.id.IPropertyTypeId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.SampleType;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleTypeFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.search.SampleTypeSearchCriteria;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+
+public class SampleExportFieldsFinder extends AbstractExportFieldsFinder<SampleType>
+{
+
+    @Override
+    public SearchResult<SampleType> findEntityTypes(final Set<IPropertyTypeId> properties,
+            final IApplicationServerInternalApi applicationServerApi, final String sessionToken)
+    {
+        final SampleTypeSearchCriteria typeSearchCriteria = new SampleTypeSearchCriteria();
+        typeSearchCriteria.withPropertyAssignments().withPropertyType().withIds().thatIn(properties);
+
+        final SampleTypeFetchOptions fetchOptions = new SampleTypeFetchOptions();
+        fetchOptions.withPropertyAssignments().withPropertyType();
+
+        return applicationServerApi.searchSampleTypes(sessionToken, typeSearchCriteria, fetchOptions);
+    }
+
+    @Override
+    public String getPermId(final SampleType sampleType)
+    {
+        return sampleType.getPermId().getPermId();
+    }
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/operation/OperationsExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/operation/OperationsExecutor.java
index 999b67987740760aca81c786b719edf9b3c75d00..397082f394c52143ff803e1dece0da93971b47b5 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/operation/OperationsExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/operation/OperationsExecutor.java
@@ -70,6 +70,7 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.experiment.ISearchEx
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.experiment.IUpdateExperimentTypesOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.experiment.IUpdateExperimentsOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.experiment.IVerifyExperimentsOperationExecutor;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.exporter.ExportOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.externaldms.ICreateExternalDmsOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.externaldms.IDeleteExternalDmsOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.externaldms.IGetExternalDmsOperationExecutor;
@@ -655,6 +656,9 @@ public class OperationsExecutor implements IOperationsExecutor
     @Autowired
     private ImportOperationExecutor importOperationExecutor;
 
+    @Autowired
+    private ExportOperationExecutor exportOperationExecutor;
+
     @Override
     public List<IOperationResult> execute(IOperationContext context, List<? extends IOperation> operations, IOperationExecutionOptions options)
     {
@@ -686,6 +690,7 @@ public class OperationsExecutor implements IOperationsExecutor
             executeUpdates(operations, resultMap, context);
             resultMap.putAll(internalOperationExecutor.execute(context, operations));
             resultMap.putAll(importOperationExecutor.execute(context, operations));
+            resultMap.putAll(exportOperationExecutor.execute(context, operations));
 
             flushCurrentSession();
             clearCurrentSession();
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/search/translator/condition/CollectionFieldSearchConditionTranslator.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/search/translator/condition/CollectionFieldSearchConditionTranslator.java
index 0020b115aea999764416d2247e0cd205ac753027..369e4a2d4733be8f540edf420f4e5580996f5523 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/search/translator/condition/CollectionFieldSearchConditionTranslator.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/search/translator/condition/CollectionFieldSearchConditionTranslator.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.id.ObjectPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.CodesSearchCriteria;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.CollectionFieldSearchCriteria;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.IdsSearchCriteria;
@@ -36,7 +37,7 @@ import static ch.ethz.sis.openbis.generic.server.asapi.v3.search.translator.SQLL
 public class CollectionFieldSearchConditionTranslator implements IConditionTranslator<CollectionFieldSearchCriteria<?>>
 {
 
-    private static final Map<Class, Object[]> ARRAY_CASTING = new HashMap<>();
+    private static final Map<Class<?>, Object[]> ARRAY_CASTING = new HashMap<>();
 
     static
     {
@@ -87,10 +88,10 @@ public class CollectionFieldSearchConditionTranslator implements IConditionTrans
                 {
                     final Collection<?> fieldValue;
                     if (!initialFieldValue.isEmpty() && initialFieldValue.stream().anyMatch(
-                            (o) -> o instanceof EntityTypePermId))
+                            (o) -> o instanceof ObjectPermId))
                     {
                         fieldValue = initialFieldValue.stream().map(
-                                (o) -> ((EntityTypePermId) o).getPermId()).collect(Collectors.toList());
+                                (o) -> ((ObjectPermId) o).getPermId()).collect(Collectors.toList());
                     } else
                     {
                         fieldValue = initialFieldValue;
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/XLSExport.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/XLSExport.java
index 5800293f841ad25ece6db81be44c66c69d01b12b..b5a6e47810f600a16b267da880e35c0a22beddc8 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/XLSExport.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/XLSExport.java
@@ -24,6 +24,7 @@ import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.SPACE
 import static ch.ethz.sis.openbis.generic.server.xls.export.ExportableKind.VOCABULARY_TYPE;
 
 import java.io.BufferedOutputStream;
+import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
@@ -42,7 +43,6 @@ import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
-import org.apache.commons.io.IOUtils;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 
@@ -64,11 +64,31 @@ import ch.systemsx.cisd.openbis.generic.shared.ISessionWorkspaceProvider;
 public class XLSExport
 {
 
-    private static final String XLSX_EXTENSION = ".xlsx";
+    public static final String XLSX_EXTENSION = ".xlsx";
 
-    private static final String ZIP_EXTENSION = ".zip";
+    public static final String ZIP_EXTENSION = ".zip";
 
-    private static final String TYPE_KEY = "TYPE";
+    public static final String SCRIPTS_DIRECTORY = "scripts";
+
+    private static final String TYPE_EXPORT_FIELD_KEY = "TYPE";
+
+    private XLSExport()
+    {
+        throw new UnsupportedOperationException("Instantiation of a utility class.");
+    }
+
+    private static File createDirectory(final File parentDirectory, final String directoryName) throws IOException
+    {
+        final File scriptsDirectory = new File(parentDirectory, directoryName);
+        final boolean directoryCreated = scriptsDirectory.mkdir();
+
+        if (!directoryCreated)
+        {
+            throw new IOException(String.format("Failed create directory %s.", scriptsDirectory.getAbsolutePath()));
+        }
+
+        return scriptsDirectory;
+    }
 
     public static ExportResult export(final String filePrefix, final IApplicationServerApi api,
             final String sessionToken, final List<ExportablePermId> exportablePermIds,
@@ -84,9 +104,10 @@ public class XLSExport
         final String fullFileName = filePrefix + "." +
                 new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS").format(new Date()) +
                 (scripts.isEmpty() ? XLSX_EXTENSION : ZIP_EXTENSION);
-        final FileOutputStream os = sessionWorkspaceProvider.getFileOutputStream(sessionToken, fullFileName);
-        writeToOutputStream(os, filePrefix, exportResult, scripts);
-        IOUtils.closeQuietly(os);
+        try (final FileOutputStream os = sessionWorkspaceProvider.getFileOutputStream(sessionToken, fullFileName))
+        {
+            writeToOutputStream(os, filePrefix, exportResult, scripts);
+        }
         return new ExportResult(fullFileName, exportResult.getWarnings());
     }
 
@@ -114,7 +135,7 @@ public class XLSExport
             {
                 for (final Map.Entry<String, String> script : scripts.entrySet())
                 {
-                    zos.putNextEntry(new ZipEntry(String.format("scripts/%s.py", script.getKey())));
+                    zos.putNextEntry(new ZipEntry(String.format("%s/%s.py", SCRIPTS_DIRECTORY, script.getKey())));
                     bos.write(script.getValue().getBytes());
                     bos.flush();
                     zos.closeEntry();
@@ -126,7 +147,7 @@ public class XLSExport
         }
     }
 
-    static PrepareWorkbookResult prepareWorkbook(final IApplicationServerApi api, final String sessionToken,
+    public static PrepareWorkbookResult prepareWorkbook(final IApplicationServerApi api, final String sessionToken,
             List<ExportablePermId> exportablePermIds, final boolean exportReferredMasterData,
             final Map<String, Map<String, List<Map<String, String>>>> exportFields,
             final TextFormatting textFormatting, final boolean compatibleWithImport)
@@ -161,10 +182,7 @@ public class XLSExport
             final List<String> permIds = exportablePermIdGroup.stream()
                     .map(permId -> permId.getPermId().getPermId()).collect(Collectors.toList());
 
-            final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = exportFields == null
-                    ? null
-                    : exportFields.get(MASTER_DATA_EXPORTABLE_KINDS.contains(exportableKind) || exportableKind == SPACE || exportableKind == PROJECT
-                            ? TYPE_KEY : exportableKind.toString());
+            final Map<String, List<Map<String, String>>> entityTypeExportFieldsMap = getEntityTypeExportFieldsMap(exportFields, exportableKind);
             final IXLSExportHelper.AdditionResult additionResult = helper.add(api, sessionToken, wb, permIds, rowNumber,
                     entityTypeExportFieldsMap, textFormatting, compatibleWithImport);
             rowNumber = additionResult.getRowNumber();
@@ -194,6 +212,15 @@ public class XLSExport
         return new PrepareWorkbookResult(wb, scripts, warnings);
     }
 
+    private static Map<String, List<Map<String, String>>> getEntityTypeExportFieldsMap(
+            final Map<String, Map<String, List<Map<String, String>>>> exportFields, final ExportableKind exportableKind)
+    {
+        return exportFields == null
+                ? null
+                : exportFields.get(MASTER_DATA_EXPORTABLE_KINDS.contains(exportableKind) || exportableKind == SPACE || exportableKind == PROJECT
+                ? TYPE_EXPORT_FIELD_KEY : exportableKind.toString());
+    }
+
     private static List<ExportablePermId> expandReference(final IApplicationServerApi api,
             final String sessionToken, final List<ExportablePermId> exportablePermIds,
             final ExportHelperFactory exportHelperFactory)
@@ -210,7 +237,7 @@ public class XLSExport
             final String sessionToken, final ExportablePermId exportablePermId,
             final Set<ExportablePermId> processedIds, final ExportHelperFactory exportHelperFactory)
     {
-        final IXLSExportHelper helper = exportHelperFactory.getHelper(exportablePermId.getExportableKind());
+        final IXLSExportHelper<? extends IEntityType> helper = exportHelperFactory.getHelper(exportablePermId.getExportableKind());
         if (helper != null)
         {
             final IPropertyAssignmentsHolder propertyAssignmentsHolder = helper
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/XLSDataSetExportHelper.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/XLSDataSetExportHelper.java
index bf7ba57ec82ce9dff7bfa1e9837b3fbcdab98e2e..951afdeaaff348f51e785976746017fea4859c93 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/XLSDataSetExportHelper.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/xls/export/helper/XLSDataSetExportHelper.java
@@ -153,7 +153,7 @@ public class XLSDataSetExportHelper extends AbstractXLSEntityExportHelper<DataSe
             case SIZE:
             {
                 final PhysicalData physicalData = dataSet.getPhysicalData();
-                return physicalData != null ? physicalData.getSize().toString() : null;
+                return physicalData != null && physicalData.getSize() != null ? physicalData.getSize().toString() : null;
             }
             case IDENTIFIER:
             case CODE:
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
index 95ec68f22ddd29091379c8fb9ade5ea05dc73421..4dbede8a54b57d1100ceb7168c5568883b593c1c 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
@@ -55,7 +55,14 @@ public class CommonServiceProvider
 
     public static void setDataStoreServerApi(final String dssURL, final int timeoutInMinutes)
     {
-        dataStoreServerApi = HttpInvokerUtils.createServiceStub(IDataStoreServerApi.class, dssURL + IDataStoreServerApi.SERVICE_URL, timeoutInMinutes * 60 * 1000);
+        dataStoreServerApi = HttpInvokerUtils.createServiceStub(IDataStoreServerApi.class, dssURL +
+                        "/datastore_server" + IDataStoreServerApi.SERVICE_URL,
+                timeoutInMinutes * 60 * 1000);
+    }
+
+    public static void setDataStoreServerApi(final IDataStoreServerApi dataStoreServerApi)
+    {
+        CommonServiceProvider.dataStoreServerApi = dataStoreServerApi;
     }
 
     public static ICommonServerForInternalUse getCommonServer()
diff --git a/ui-admin/src/core-plugins/admin/1/as/services/xls-export/xls-export.py b/ui-admin/src/core-plugins/admin/1/as/services/xls-export/xls-export.py
index a49b4c060df61a3c9da8913f19ad6b1681dd555c..f9689079d48e18d0e8266a9f8f2c40affe0e7bb1 100644
--- a/ui-admin/src/core-plugins/admin/1/as/services/xls-export/xls-export.py
+++ b/ui-admin/src/core-plugins/admin/1/as/services/xls-export/xls-export.py
@@ -34,12 +34,12 @@ def export(context, parameters):
                         "<SAMPLE_TYPE | EXPERIMENT_TYPE | DATASET_TYPE | VOCABULARY_TYPE | SPACE | PROJECT>": [
                           {"type": "ATTRIBUTE", "id": "<attribute name>"},
                           ...
-                        ] - attribute for each type and entity without types to be exported,
+                        ] - attributes for each type and entity without types to be exported,
                             if the list is empty no attributes will be exported for the given one
                         ...
                     },
                     "SAMPLE": {
-                        "<typePermID>": [
+                        "<sampleTypePermID>": [
                           {"type": "PROPERTY", "id": "<property code>"},
                           {"type": "ATTRIBUTE", "id": "<attribute name>"},
                           ...
@@ -48,7 +48,7 @@ def export(context, parameters):
                             for the sample type
                     },
                     "EXPERIMENT": {
-                        "<typePermID>": [
+                        "<experimentTypePermID>": [
                             {"type": "PROPERTY", "id": "<property code>"},
                             {"type": "ATTRIBUTE", "id": "<attribute name>"},
                             ...
@@ -57,7 +57,7 @@ def export(context, parameters):
                             for the experiment type
                     },
                     "DATASET": {
-                        "<typePermID>": [
+                        "<dataSetTypePermID>": [
                           {"type": "PROPERTY", "id": "<property code>"},
                           {"type": "ATTRIBUTE", "id": "<attribute name>"} ,
                           ...