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