From 6d49b93ca2ffe5ec3014c24238901f7a10bc2a58 Mon Sep 17 00:00:00 2001
From: vkovtun <vkovtun@ethz.ch>
Date: Thu, 17 Aug 2023 16:29:24 +0200
Subject: [PATCH] SSDM-13926: Writing tests for import/export API.

---
 .../asapi/v3/IApplicationServerApi.java       |   5 +-
 .../importer/data/UncompressedImportData.java |   8 +-
 .../asapi/v3/ApplicationServerApiLogger.java  |   5 +-
 ...iPersonalAccessTokenInvocationHandler.java |   3 +-
 .../v3/executor/importer/ImportExecutor.java  |   7 +-
 .../importer/ImportOperationExecutor.java     |   2 +
 .../operation/OperationsExecutor.java         |   5 +
 .../systemtest/asapi/v3/ImportTest.java       |  97 ++++++++++++++++++
 .../asapi/v3/test_files/xls/import.xlsx       | Bin 0 -> 9238 bytes
 9 files changed, 119 insertions(+), 13 deletions(-)
 create mode 100644 server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ImportTest.java
 create mode 100644 server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/test_files/xls/import.xlsx

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 0a24c8350ad..976bf70c80f 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,9 +75,6 @@ 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;
@@ -2264,6 +2261,6 @@ public interface IApplicationServerApi extends IRpcService
 
     public void executeImport(String sessionToken, ImportData 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/importer/data/UncompressedImportData.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/importer/data/UncompressedImportData.java
index 71184cd8a6c..0305a2d2690 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/importer/data/UncompressedImportData.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/importer/data/UncompressedImportData.java
@@ -17,7 +17,7 @@
 package ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data;
 
 import java.io.Serializable;
-import java.util.List;
+import java.util.Collection;
 
 import ch.systemsx.cisd.base.annotation.JsonObject;
 
@@ -30,9 +30,9 @@ public class UncompressedImportData implements Serializable, ImportData
 
     private final byte[] file;
 
-    private final List<ImportScript> scripts;
+    private final Collection<ImportScript> scripts;
 
-    public UncompressedImportData(final ImportFormat format, final byte[] file, final List<ImportScript> scripts)
+    public UncompressedImportData(final ImportFormat format, final byte[] file, final Collection<ImportScript> scripts)
     {
         this.format = format;
         this.file = file;
@@ -49,7 +49,7 @@ public class UncompressedImportData implements Serializable, ImportData
         return file;
     }
 
-    public List<ImportScript> getScripts()
+    public Collection<ImportScript> getScripts()
     {
         return scripts;
     }
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 f7effd8acfe..f22e5f58378 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
@@ -87,6 +87,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.update.ExternalDmsUp
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.global.GlobalSearchObject;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.global.fetchoptions.GlobalSearchObjectFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.global.search.GlobalSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data.ImportData;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.options.ImportOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.MaterialType;
@@ -1365,9 +1366,9 @@ public class ApplicationServerApiLogger extends AbstractServerLogger implements
     }
 
     @Override
-    public void doImport(final String sessionToken, final byte[] file, final ImportOptions importOptions)
+    public void executeImport(final String sessionToken, final ImportData importData, final ImportOptions importOptions)
     {
-        logAccess(sessionToken, "do-import", "Path(%s) ImportOptions(%s)", file, importOptions);
+        logAccess(sessionToken, "execute-import", "ImportData(%s) ImportOptions(%s)", importData, importOptions);
     }
 
 }
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 309d0c278e6..7b13dffafa7 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
@@ -88,6 +88,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.update.ExternalDmsUp
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.global.GlobalSearchObject;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.global.fetchoptions.GlobalSearchObjectFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.global.search.GlobalSearchCriteria;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data.ImportData;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.options.ImportOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.Material;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.MaterialType;
@@ -1254,7 +1255,7 @@ public class ApplicationServerApiPersonalAccessTokenInvocationHandler implements
     }
 
     @Override
-    public void doImport(final String sessionToken, final byte[] file, final ImportOptions importOptions)
+    public void executeImport(final String sessionToken, final ImportData importData, final ImportOptions importOptions)
     {
         invocation.proceedWithNewFirstArgument(converter.convert(sessionToken));
     }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportExecutor.java
index 658d7533798..eb2490c96e9 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportExecutor.java
@@ -63,12 +63,15 @@ public class ImportExecutor implements IImportExecutor
             {
                 // XLS file
 
-                importXls(context, operation, Map.of(), ((UncompressedImportData) importData).getFile());
+                final UncompressedImportData uncompressedImportData = (UncompressedImportData) importData;
+
+                importXls(context, operation, Map.of(), uncompressedImportData.getFile());
             } else if (importData instanceof ZipImportData)
             {
                 // ZIP file
 
-                try (final ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(((ZipImportData) importData).getFile())))
+                final ZipImportData zipImportData = (ZipImportData) importData;
+                try (final ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(zipImportData.getFile())))
                 {
                     final Map<String, String> scripts = new HashMap<>();
                     byte[] xlsFileContent = null;
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportOperationExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportOperationExecutor.java
index 72d971a4a9c..ef4032eeaa8 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportOperationExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/importer/ImportOperationExecutor.java
@@ -18,12 +18,14 @@
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.importer;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
 
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.ImportOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.ImportOperationResult;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.common.OperationExecutor;
 
+@Component
 public class ImportOperationExecutor extends OperationExecutor<ImportOperation, ImportOperationResult>
 {
 
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 87b6cd191b6..999b6798774 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
@@ -76,6 +76,7 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.externaldms.IGetExte
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.externaldms.ISearchExternalDmsOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.externaldms.IUpdateExternalDmsOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.globalsearch.ISearchGloballyOperationExecutor;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.importer.ImportOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.material.ICreateMaterialTypesOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.material.ICreateMaterialsOperationExecutor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.material.IDeleteMaterialTypesOperationExecutor;
@@ -651,6 +652,9 @@ public class OperationsExecutor implements IOperationsExecutor
     @Autowired
     private IGetSessionInformationOperationExecutor getSessionInformationExecutor;
 
+    @Autowired
+    private ImportOperationExecutor importOperationExecutor;
+
     @Override
     public List<IOperationResult> execute(IOperationContext context, List<? extends IOperation> operations, IOperationExecutionOptions options)
     {
@@ -681,6 +685,7 @@ public class OperationsExecutor implements IOperationsExecutor
             executeCreations(operations, resultMap, context);
             executeUpdates(operations, resultMap, context);
             resultMap.putAll(internalOperationExecutor.execute(context, operations));
+            resultMap.putAll(importOperationExecutor.execute(context, operations));
 
             flushCurrentSession();
             clearCurrentSession();
diff --git a/server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ImportTest.java b/server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ImportTest.java
new file mode 100644
index 00000000000..c42172366a9
--- /dev/null
+++ b/server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/ImportTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.systemtest.asapi.v3;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data.ImportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data.ImportFormat;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data.ImportScript;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.data.UncompressedImportData;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.options.ImportMode;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.importer.options.ImportOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.Vocabulary;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.VocabularyTerm;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularySearchCriteria;
+
+public class ImportTest extends AbstractTest
+{
+
+    private final Map<String, String> IMPORT_SCRIPT_MAP = Map.of("Script 1", "Value 1", "Script 2", "Value 2");
+
+    private final Collection<ImportScript> IMPORT_SCRIPTS = IMPORT_SCRIPT_MAP.entrySet().stream()
+            .map(entry -> new ImportScript(entry.getKey(), entry.getValue())).collect(Collectors.toList());
+
+    private static byte[] fileContent;
+
+    @BeforeClass
+    public void setupClass()
+    {
+        try (final InputStream is = ImportTest.class.getResourceAsStream("test_files/xls/import.xlsx"))
+        {
+            if (is == null)
+            {
+                throw new RuntimeException();
+            }
+
+            fileContent = is.readAllBytes();
+        } catch (final IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testUncompressedDataImport()
+    {
+        final String sessionToken = v3api.login(TEST_USER, PASSWORD);
+
+        final ImportData importData = new UncompressedImportData(ImportFormat.XLS, fileContent, IMPORT_SCRIPTS);
+        final ImportOptions importOptions = new ImportOptions(ImportMode.UPDATE_IF_EXISTS);
+
+        v3api.executeImport(sessionToken, importData, importOptions);
+
+        final VocabularySearchCriteria vocabularySearchCriteria = new VocabularySearchCriteria();
+        vocabularySearchCriteria.withCode().thatEquals("DETECTION");
+
+        final VocabularyFetchOptions vocabularyFetchOptions = new VocabularyFetchOptions();
+        vocabularyFetchOptions.withTerms();
+
+        final SearchResult<Vocabulary> vocabularySearchResult =
+                v3api.searchVocabularies(sessionToken, vocabularySearchCriteria, vocabularyFetchOptions);
+
+        assertEquals(1, vocabularySearchResult.getTotalCount());
+
+        final List<VocabularyTerm> vocabularyTerms = vocabularySearchResult.getObjects().get(0).getTerms();
+        assertEquals(2, vocabularyTerms.size());
+        assertEquals(Set.of("HRP", "AAA"), vocabularyTerms.stream().map(VocabularyTerm::getCode).collect(Collectors.toSet()));
+    }
+
+}
diff --git a/server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/test_files/xls/import.xlsx b/server-application-server/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/test_files/xls/import.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..2ed0c7e2a6091a46bd5fa3b055a723a6a1838fd4
GIT binary patch
literal 9238
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

literal 0
HcmV?d00001

-- 
GitLab