This is an automated email from the ASF dual-hosted git repository.
dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/dev by this push:
new e321304ef5 feat: Improve handling of generic storage document for
imports and exports (#5154)
e321304ef5 is described below
commit e321304ef5942caad2bb5745a24dbf395afe5e06
Author: Sven Oehler <[email protected]>
AuthorDate: Fri Oct 30 10:12:27 2026 +0100
feat: Improve handling of generic storage document for imports and exports
(#5154)
Co-authored-by: Dominik Riemer <[email protected]>
---
streampipes-data-export/pom.xml | 10 +
.../streampipes/export/AssetLinkResolver.java | 6 +-
.../apache/streampipes/export/ExportManager.java | 42 ++-
.../export/GenericStorageResourceRegistry.java | 45 +++
.../streampipes/export/PermissionDefaults.java | 41 +--
.../streampipes/export/ResourceRegistration.java | 40 +++
.../export/dataimport/PerformImportGenerator.java | 30 +-
.../export/dataimport/PreviewImportGenerator.java | 10 +-
.../export/generator/ExportPackageGenerator.java | 53 ++--
.../export/resolver/AbstractResolver.java | 6 +-
.../resolver/GenericStorageDocumentResolver.java | 2 +-
.../resolver/GenericStorageResolverProvider.java | 106 +++++++
.../export/utils/ArchiveEntryNames.java | 40 +--
.../streampipes/export/AssetLinkResolverTest.java | 65 ++++
.../export/GenericStorageCategoryExportTest.java | 333 +++++++++++++++++++++
.../export/GenericStorageResolverTest.java | 143 +++++++++
.../export/GenericStorageResourceRegistryTest.java | 109 +++++++
.../model/export/ExportConfiguration.java | 10 +
.../streampipes/model/export/ExportItem.java | 9 +
.../src/lib/model/gen/streampipes-model.ts | 8 +-
.../assets/dialog/move-asset-dialog.component.ts | 2 -
.../export/data-export-import.component.ts | 2 +
.../data-export-dialog.component.spec.ts | 123 ++++++++
.../export-dialog/data-export-dialog.component.ts | 78 ++++-
.../generic-storage-items.component.ts | 1 +
.../data-import-dialog.component.html | 22 +-
.../import-dialog/data-import-dialog.component.ts | 10 +
27 files changed, 1216 insertions(+), 130 deletions(-)
diff --git a/streampipes-data-export/pom.xml b/streampipes-data-export/pom.xml
index 623f074903..a9d89132fb 100644
--- a/streampipes-data-export/pom.xml
+++ b/streampipes-data-export/pom.xml
@@ -61,6 +61,16 @@
<groupId>org.lightcouch</groupId>
<artifactId>lightcouch</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/AssetLinkResolver.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/AssetLinkResolver.java
index af8aa370d1..5b120a6860 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/AssetLinkResolver.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/AssetLinkResolver.java
@@ -24,6 +24,7 @@ import org.apache.streampipes.export.resolver.ChartResolver;
import org.apache.streampipes.export.resolver.DashboardResolver;
import org.apache.streampipes.export.resolver.DataSourceResolver;
import org.apache.streampipes.export.resolver.FileResolver;
+import org.apache.streampipes.export.resolver.GenericStorageResolverProvider;
import org.apache.streampipes.export.resolver.MeasurementResolver;
import org.apache.streampipes.export.resolver.PipelineResolver;
import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
@@ -100,6 +101,9 @@ public class AssetLinkResolver {
exportConfig.setFiles(new
FileResolver(resourceManager.getFileMetadataStorage())
.resolve(getLinks(assetLinks, ResolvableAssetLinks.FILE)));
+ exportConfig.setGenericStorageDocuments(
+ new
GenericStorageResolverProvider(resourceManager.getGenericStorage()).resolve(assetLinks));
+
return exportConfig;
} catch (IOException e) {
LOG.error("IO Exception when writing export configuration", e);
@@ -111,7 +115,7 @@ public class AssetLinkResolver {
String queryHint) {
return assetLinks
.stream()
- .filter(link -> link.getQueryHint().equals(queryHint))
+ .filter(link -> queryHint.equals(link.getQueryHint()))
.collect(Collectors.toSet());
}
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
index 88c6d4f2d6..35b791fef7 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/ExportManager.java
@@ -19,6 +19,7 @@
package org.apache.streampipes.export;
import org.apache.streampipes.export.generator.ExportPackageGenerator;
+import org.apache.streampipes.export.resolver.GenericStorageResolverProvider;
import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
import org.apache.streampipes.manager.pipeline.PipelineManager;
import org.apache.streampipes.model.assets.SpAssetModel;
@@ -28,6 +29,7 @@ import
org.apache.streampipes.resource.management.SpResourceManager;
import org.apache.streampipes.storage.api.system.IGenericStorage;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -44,10 +46,8 @@ public class ExportManager {
assetId, extensionServiceRequestManager, resourceManager,
pipelineManager)
.resolveResources())
.collect(Collectors.toList());
- var genericStorageAppDocTypes =
getGenericStorageAppDocTypes(resourceManager.getGenericStorage());
-
exportConfig.setAssetExportConfiguration(assetExportConfigurations);
- exportConfig.setGenericStorageAppDocTypes(genericStorageAppDocTypes);
+ addGenericStoragePreview(exportConfig,
resourceManager.getGenericStorage(), selectedAssetIds);
return exportConfig;
}
@@ -61,16 +61,36 @@ public class ExportManager {
.generateExportPackage();
}
- private static List<ExportItem> getGenericStorageAppDocTypes(IGenericStorage
genericStorage) {
+ private static void addGenericStoragePreview(ExportConfiguration config,
+ IGenericStorage storage,
+ List<String> selectedAssetIds)
{
+ var resolvers = new GenericStorageResolverProvider(storage);
try {
- return genericStorage
- .getAllAppDocTypes()
- .stream()
- .filter(appDocType -> !SpAssetModel.APP_DOC_TYPE.equals(appDocType))
- .map(appDocType -> new ExportItem(appDocType, appDocType, false))
- .collect(Collectors.toList());
+ for (var appDocType : storage.getAllAppDocTypes()) {
+ if (SpAssetModel.APP_DOC_TYPE.equals(appDocType)) {
+ continue;
+ }
+ var exportType =
GenericStorageResourceRegistry.INSTANCE.find(appDocType).filter(type ->
type.category() != null);
+ if (exportType.isPresent()) {
+ if (!selectedAssetIds.isEmpty()) {
+ continue;
+ }
+ // Global exports list all documents; asset exports are resolved by
AssetLinkResolver.
+ try {
+ var items = new ArrayList<ExportItem>();
+ for (var document : storage.findAll(appDocType)) {
+ items.add(resolvers.toExportItem(document, false));
+ }
+ config.getGenericStorageDocuments().addAll(items);
+ } catch (IOException e) {
+ config.addGenericStorageAppDocType(new ExportItem(appDocType,
appDocType, false));
+ }
+ } else {
+ config.addGenericStorageAppDocType(new ExportItem(appDocType,
appDocType, false));
+ }
+ }
} catch (IOException e) {
- return List.of();
+ // Preserve the asset preview if generic storage is unavailable.
}
}
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/GenericStorageResourceRegistry.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/GenericStorageResourceRegistry.java
new file mode 100644
index 0000000000..41376ca240
--- /dev/null
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/GenericStorageResourceRegistry.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Registrations for resource types persisted in generic storage. */
+public class GenericStorageResourceRegistry {
+
+ public static final GenericStorageResourceRegistry INSTANCE = new
GenericStorageResourceRegistry();
+
+ private final Map<String, ResourceRegistration<?>> registrations = new
ConcurrentHashMap<>();
+
+ public void register(String appDocType, ResourceRegistration<?>
registration) {
+ Objects.requireNonNull(appDocType, "appDocType");
+ Objects.requireNonNull(registration, "registration");
+ var existing = registrations.putIfAbsent(appDocType, registration);
+ if (existing != null && !existing.equals(registration)) {
+ throw new IllegalArgumentException("Resource type already registered for
" + appDocType);
+ }
+ }
+
+ public Optional<ResourceRegistration<?>> find(String appDocType) {
+ return Optional.ofNullable(registrations.get(appDocType));
+ }
+}
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/PermissionDefaults.java
similarity index 52%
copy from
streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
copy to
streampipes-data-export/src/main/java/org/apache/streampipes/export/PermissionDefaults.java
index cb62711a05..ac630aa56a 100644
---
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/PermissionDefaults.java
@@ -16,44 +16,13 @@
*
*/
-package org.apache.streampipes.model.export;
+package org.apache.streampipes.export;
-public class ExportItem {
+import java.util.Objects;
- private String resourceId;
- private String label;
- private boolean selected;
+public record PermissionDefaults(Class<?> resourceClass, boolean
publicElement) {
- public ExportItem() {
- }
-
- public ExportItem(String resourceId, String label, boolean selected) {
- this.resourceId = resourceId;
- this.label = label;
- this.selected = selected;
- }
-
- public String getResourceId() {
- return resourceId;
- }
-
- public void setResourceId(String resourceId) {
- this.resourceId = resourceId;
- }
-
- public String getLabel() {
- return label;
- }
-
- public void setLabel(String label) {
- this.label = label;
- }
-
- public boolean isSelected() {
- return selected;
- }
-
- public void setSelected(boolean selected) {
- this.selected = selected;
+ public PermissionDefaults {
+ Objects.requireNonNull(resourceClass, "resourceClass");
}
}
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/ResourceRegistration.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/ResourceRegistration.java
new file mode 100644
index 0000000000..25811ffbbd
--- /dev/null
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/ResourceRegistration.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export;
+
+import org.apache.streampipes.export.resolver.AbstractResolver;
+import org.apache.streampipes.storage.api.system.IGenericStorage;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/** Metadata and a required document resolver factory for an external resource
type. */
+public record ResourceRegistration<T>(PermissionDefaults permissions,
+ String category,
+ Function<IGenericStorage, ? extends
AbstractResolver<T>> resolverFactory) {
+
+ public ResourceRegistration {
+ Objects.requireNonNull(permissions, "permissions");
+ Objects.requireNonNull(resolverFactory, "resolverFactory");
+ }
+
+ public AbstractResolver<T> createResolver(IGenericStorage storage) {
+ return Objects.requireNonNull(resolverFactory.apply(storage), "resolver");
+ }
+}
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
index c225b2956f..7fe960e711 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PerformImportGenerator.java
@@ -18,6 +18,7 @@
package org.apache.streampipes.export.dataimport;
+import org.apache.streampipes.export.GenericStorageResourceRegistry;
import org.apache.streampipes.export.model.PermissionInfo;
import org.apache.streampipes.export.resolver.AbstractResolver;
import org.apache.streampipes.export.resolver.AdapterResolver;
@@ -26,8 +27,10 @@ import
org.apache.streampipes.export.resolver.DashboardResolver;
import org.apache.streampipes.export.resolver.DataSourceResolver;
import org.apache.streampipes.export.resolver.FileResolver;
import org.apache.streampipes.export.resolver.GenericStorageDocumentResolver;
+import org.apache.streampipes.export.resolver.GenericStorageResolverProvider;
import org.apache.streampipes.export.resolver.MeasurementResolver;
import org.apache.streampipes.export.resolver.PipelineResolver;
+import org.apache.streampipes.export.utils.ArchiveEntryNames;
import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
import org.apache.streampipes.manager.file.FileHandler;
import org.apache.streampipes.manager.pipeline.PipelineManager;
@@ -53,6 +56,7 @@ import java.util.Set;
public class PerformImportGenerator extends ImportGenerator<Void> {
+ private final GenericStorageResolverProvider genericStorageResolvers;
private final AssetExportConfiguration config;
private final IGenericStorage genericStorage;
private final Set<PermissionInfo> permissionsToStore = new HashSet<>();
@@ -67,8 +71,19 @@ public class PerformImportGenerator extends
ImportGenerator<Void> {
ExtensionServiceRequestManager
extensionServiceRequestManager,
SpResourceManager resourceManager,
PipelineManager pipelineManager) {
+ this(config, ownerSid, extensionServiceRequestManager, resourceManager,
pipelineManager,
+ GenericStorageResourceRegistry.INSTANCE);
+ }
+
+ public PerformImportGenerator(AssetExportConfiguration config,
+ String ownerSid,
+ ExtensionServiceRequestManager
extensionServiceRequestManager,
+ SpResourceManager resourceManager,
+ PipelineManager pipelineManager,
+ GenericStorageResourceRegistry
resourceRegistry) {
this.config = config;
this.genericStorage = resourceManager.getGenericStorage();
+ this.genericStorageResolvers = new
GenericStorageResolverProvider(genericStorage, resourceRegistry);
this.ownerSid = ownerSid;
this.extensionServiceRequestManager = extensionServiceRequestManager;
this.resourceManager = resourceManager;
@@ -175,7 +190,20 @@ public class PerformImportGenerator extends
ImportGenerator<Void> {
@Override
protected void handleGenericStorageDocument(String document, String
documentId) throws JsonProcessingException {
if (shouldStore(documentId, config.getGenericStorageDocuments())) {
- writeDocument(document, new
GenericStorageDocumentResolver(genericStorage));
+ var resolver = new GenericStorageDocumentResolver(genericStorage);
+ var storedDocument = resolver.readDocument(document);
+ if (!(storedDocument.get("_id") instanceof String resourceId)
+ || !(documentId.equals(resourceId) ||
documentId.equals(ArchiveEntryNames.forResourceId(resourceId)))) {
+ throw new IllegalArgumentException("Generic document id does not match
manifest: " + documentId);
+ }
+ writeDocument(document,
genericStorageResolvers.resolverFor(storedDocument));
+
genericStorageResolvers.registrationFor(storedDocument).ifPresent(registration
-> {
+ var permissions = registration.permissions();
+ if
(resourceManager.managePermissions().findForObjectId(resourceId).isEmpty()) {
+ resourceManager.managePermissions().createDefault(resourceId,
+ permissions.resourceClass(), ownerSid,
permissions.publicElement());
+ }
+ });
}
}
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
index 6ba6c937ed..2328c5a9dd 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/dataimport/PreviewImportGenerator.java
@@ -24,6 +24,7 @@ import org.apache.streampipes.export.resolver.ChartResolver;
import org.apache.streampipes.export.resolver.DashboardResolver;
import org.apache.streampipes.export.resolver.DataSourceResolver;
import org.apache.streampipes.export.resolver.FileResolver;
+import org.apache.streampipes.export.resolver.GenericStorageResolverProvider;
import org.apache.streampipes.export.resolver.MeasurementResolver;
import org.apache.streampipes.export.resolver.PipelineResolver;
import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
@@ -151,8 +152,13 @@ public class PreviewImportGenerator extends
ImportGenerator<AssetExportConfigura
}
@Override
- protected void handleGenericStorageDocument(String document, String
genericDocId) {
- addExportItem(genericDocId, genericDocId,
importConfig::addGenericStorageDocument);
+ protected void handleGenericStorageDocument(String document, String
genericDocId) throws JsonProcessingException {
+ Map<String, Object> payload = defaultMapper.readValue(document, new
TypeReference<>() { });
+ var resolvers = new
GenericStorageResolverProvider(resourceManager.getGenericStorage());
+ var item = resolvers.toExportItem(payload, true);
+ // Import selections identify archive entries, whose names may differ from
stored document IDs.
+ item.setResourceId(genericDocId);
+ importConfig.addGenericStorageDocument(item);
}
@Override
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
index 8709874696..6352b0ae9d 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/generator/ExportPackageGenerator.java
@@ -25,9 +25,10 @@ import org.apache.streampipes.export.resolver.ChartResolver;
import org.apache.streampipes.export.resolver.DashboardResolver;
import org.apache.streampipes.export.resolver.DataSourceResolver;
import org.apache.streampipes.export.resolver.FileResolver;
-import org.apache.streampipes.export.resolver.GenericStorageDocumentResolver;
+import org.apache.streampipes.export.resolver.GenericStorageResolverProvider;
import org.apache.streampipes.export.resolver.MeasurementResolver;
import org.apache.streampipes.export.resolver.PipelineResolver;
+import org.apache.streampipes.export.utils.ArchiveEntryNames;
import
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
import org.apache.streampipes.manager.file.FileManager;
import org.apache.streampipes.manager.pipeline.PipelineManager;
@@ -63,6 +64,7 @@ public class ExportPackageGenerator {
private final ObjectMapper defaultMapper;
private final PipelineManager pipelineManager;
private final SpResourceManager resourceManager;
+ private final GenericStorageResolverProvider genericStorageResolvers;
public ExportPackageGenerator(ExportConfiguration exportConfiguration,
ExtensionServiceRequestManager
extensionServiceRequestManager,
@@ -72,6 +74,7 @@ public class ExportPackageGenerator {
this.extensionServiceRequestManager = extensionServiceRequestManager;
this.pipelineManager = pipelineManager;
this.resourceManager = resourceManager;
+ this.genericStorageResolvers = new
GenericStorageResolverProvider(resourceManager.getGenericStorage());
this.defaultMapper = JacksonSerializer.getObjectMapper(Map.of(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true,
SerializationFeature.INDENT_OUTPUT, false
@@ -136,17 +139,16 @@ public class ExportPackageGenerator {
config.getGenericStorageDocuments().forEach(item -> {
addGenericStorageDocument(builder,
item,
- new
GenericStorageDocumentResolver(resourceManager.getGenericStorage()),
manifest::addGenericStorageDocument,
exportedGenericStorageDocumentIds);
});
config.getLabels().forEach(item -> {
- addDoc(builder, item, new
GenericStorageDocumentResolver(resourceManager.getGenericStorage()),
manifest::addGenericStorageDocument);
+ addGenericStorageDocument(builder, item,
manifest::addGenericStorageDocument, exportedGenericStorageDocumentIds);
});
config.getSites().forEach(item -> {
- addDoc(builder, item, new
GenericStorageDocumentResolver(resourceManager.getGenericStorage()),
manifest::addGenericStorageDocument);
+ addGenericStorageDocument(builder, item,
manifest::addGenericStorageDocument, exportedGenericStorageDocumentIds);
});
config.getFiles().forEach(item -> {
@@ -168,6 +170,13 @@ public class ExportPackageGenerator {
});
});
+ if (exportConfiguration.getGenericStorageDocuments() != null) {
+ exportConfiguration.getGenericStorageDocuments().forEach(item ->
addGenericStorageDocument(builder,
+ item,
+ manifest::addGenericStorageDocument,
+ exportedGenericStorageDocumentIds));
+ }
+
if (exportConfiguration.getGenericStorageAppDocTypes() != null) {
exportConfiguration.getGenericStorageAppDocTypes().forEach(item -> {
if (item.isSelected()) {
@@ -196,34 +205,39 @@ public class ExportPackageGenerator {
ExportItem exportItem,
AbstractResolver<?> resolver,
Consumer<String> function) {
+ addDoc(builder, exportItem, resolver::getSerializedDocument, function);
+ }
+
+ private void addDoc(ZipFileBuilder builder,
+ ExportItem exportItem,
+ DocumentExporter exporter,
+ Consumer<String> function) {
try {
if (exportItem.isSelected()) {
var resourceId = exportItem.getResourceId();
- var sanitizedResourceId = sanitize(resourceId);
- builder.addText(sanitizedResourceId,
resolver.getSerializedDocument(resourceId));
+ var sanitizedResourceId = ArchiveEntryNames.forResourceId(resourceId);
+ builder.addText(sanitizedResourceId, exporter.serialize(resourceId));
function.accept(sanitizedResourceId);
}
} catch (JsonProcessingException | ElementNotFoundException e) {
- LOG.warn(
- "Could not find document with resource id {} with resolver {}",
- exportItem.getResourceId(),
- resolver.getClass().getCanonicalName(),
- e);
+ LOG.warn("Could not export document with resource id {}",
exportItem.getResourceId(), e);
}
}
+ @FunctionalInterface
+ private interface DocumentExporter {
+ String serialize(String resourceId) throws JsonProcessingException,
ElementNotFoundException;
+ }
+
private void addGenericStorageDocumentsByType(ZipFileBuilder builder,
String appDocType,
Consumer<String> function,
Set<String>
exportedGenericStorageDocumentIds) {
- var resolver = new
GenericStorageDocumentResolver(resourceManager.getGenericStorage());
-
try {
resourceManager.getGenericStorage()
.findAll(appDocType)
.forEach(document -> addGenericStorageDocument(builder,
- resolver.convert(document),
- resolver,
+ new ExportItem(document.get("_id").toString(), "", true),
function,
exportedGenericStorageDocumentIds));
} catch (IOException e) {
@@ -233,18 +247,13 @@ public class ExportPackageGenerator {
private void addGenericStorageDocument(ZipFileBuilder builder,
ExportItem exportItem,
- AbstractResolver<?> resolver,
Consumer<String> function,
Set<String>
exportedGenericStorageDocumentIds) {
- if (exportedGenericStorageDocumentIds.add(exportItem.getResourceId())) {
- addDoc(builder, exportItem, resolver, function);
+ if (exportItem.isSelected() &&
exportedGenericStorageDocumentIds.add(exportItem.getResourceId())) {
+ addDoc(builder, exportItem,
genericStorageResolvers::getSerializedDocument, function);
}
}
- private String sanitize(String resourceId) {
- return resourceId.replaceAll(":", "").replaceAll("\\.", "");
- }
-
private void addAssets(ZipFileBuilder builder,
List<String> assetIds,
StreamPipesApplicationPackage manifest) {
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/AbstractResolver.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/AbstractResolver.java
index 977d64bb3b..499c1ef820 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/AbstractResolver.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/AbstractResolver.java
@@ -62,12 +62,16 @@ public abstract class AbstractResolver<T> implements
DocumentResolver<T> {
public String getSerializedDocument(String resourceId) throws
JsonProcessingException, ElementNotFoundException {
var document = findDocument(resourceId);
if (document != null) {
- return
getObjectMapper().writeValueAsString(modifyDocumentForExport(document));
+ return serializeDocument(modifyDocumentForExport(document));
} else {
throw new ElementNotFoundException("Could not find element with resource
id " + resourceId);
}
}
+ public String serializeDocument(T document) throws JsonProcessingException {
+ return getObjectMapper().writeValueAsString(document);
+ }
+
protected void overrideProtocol(EventGrounding grounding) {
var newProtocol = new
EventGroundingProcessor().applyOverride(grounding.getTransportProtocol());
grounding.setTransportProtocol(newProtocol);
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageDocumentResolver.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageDocumentResolver.java
index e3530ecb27..beb00bfe96 100644
---
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageDocumentResolver.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageDocumentResolver.java
@@ -80,7 +80,7 @@ public class GenericStorageDocumentResolver extends
AbstractResolver<Map<String,
try {
genericStorage.create(document);
} catch (IOException e) {
- LOG.warn("Could not write document");
+ LOG.warn("Could not write document", e);
}
}
diff --git
a/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageResolverProvider.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageResolverProvider.java
new file mode 100644
index 0000000000..37777004df
--- /dev/null
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/resolver/GenericStorageResolverProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export.resolver;
+
+import org.apache.streampipes.commons.exceptions.ElementNotFoundException;
+import org.apache.streampipes.export.GenericStorageResourceRegistry;
+import org.apache.streampipes.export.ResourceRegistration;
+import org.apache.streampipes.model.assets.AssetLink;
+import org.apache.streampipes.model.export.ExportItem;
+import org.apache.streampipes.serializers.json.JacksonSerializer;
+import org.apache.streampipes.storage.api.system.IGenericStorage;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/** Selects resource resolvers and bridges generic storage documents to their
domain types. */
+public class GenericStorageResolverProvider {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(GenericStorageResolverProvider.class);
+
+ private final GenericStorageResourceRegistry registry;
+ private final IGenericStorage storage;
+ private final GenericStorageDocumentResolver fallback;
+ private final ObjectMapper mapper = JacksonSerializer.getObjectMapper();
+
+ public GenericStorageResolverProvider(IGenericStorage storage) {
+ this(storage, GenericStorageResourceRegistry.INSTANCE);
+ }
+
+ public GenericStorageResolverProvider(IGenericStorage storage,
GenericStorageResourceRegistry registry) {
+ this.storage = storage;
+ this.registry = registry;
+ this.fallback = new GenericStorageDocumentResolver(storage);
+ }
+
+ public Optional<ResourceRegistration<?>> registrationFor(Map<String, Object>
document) {
+ return document.get("appDocType") instanceof String type ?
registry.find(type) : Optional.empty();
+ }
+
+ public AbstractResolver<?> resolverFor(Map<String, Object> document) {
+ return registrationFor(document).<AbstractResolver<?>>map(entry ->
entry.createResolver(storage)).orElse(fallback);
+ }
+
+ public ExportItem toExportItem(Map<String, Object> document, boolean
selected) throws JsonProcessingException {
+ var item = convert(resolverFor(document),
mapper.writeValueAsString(document));
+ item.setSelected(selected);
+ registrationFor(document).ifPresent(entry ->
item.setCategory(entry.category()));
+ return item;
+ }
+
+ public Set<ExportItem> resolve(Set<AssetLink> links) {
+ var items = new HashSet<ExportItem>();
+ var resourceIds =
links.stream().map(AssetLink::getResourceId).filter(Objects::nonNull).distinct().toList();
+ for (var resourceId : resourceIds) {
+ var document = fallback.findDocument(resourceId);
+ if (document != null && registrationFor(document).filter(entry ->
entry.category() != null).isPresent()) {
+ try {
+ items.add(toExportItem(document, true));
+ } catch (JsonProcessingException e) {
+ LOG.warn("Could not resolve linked generic storage document {}",
resourceId, e);
+ }
+ }
+ }
+ return items;
+ }
+
+ public String getSerializedDocument(String resourceId) throws
JsonProcessingException, ElementNotFoundException {
+ var document = fallback.findDocument(resourceId);
+ if (document == null) {
+ throw new ElementNotFoundException("Could not find element with resource
id " + resourceId);
+ }
+ return serializeForExport(resolverFor(document),
mapper.writeValueAsString(document));
+ }
+
+ private <T> ExportItem convert(AbstractResolver<T> resolver, String
document) throws JsonProcessingException {
+ return resolver.convert(resolver.readDocument(document));
+ }
+
+ private <T> String serializeForExport(AbstractResolver<T> resolver, String
document) throws JsonProcessingException {
+ return
resolver.serializeDocument(resolver.modifyDocumentForExport(resolver.readDocument(document)));
+ }
+}
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/utils/ArchiveEntryNames.java
similarity index 52%
copy from
streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
copy to
streampipes-data-export/src/main/java/org/apache/streampipes/export/utils/ArchiveEntryNames.java
index cb62711a05..6b4d889c5d 100644
---
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
+++
b/streampipes-data-export/src/main/java/org/apache/streampipes/export/utils/ArchiveEntryNames.java
@@ -16,44 +16,14 @@
*
*/
-package org.apache.streampipes.model.export;
+package org.apache.streampipes.export.utils;
-public class ExportItem {
+public final class ArchiveEntryNames {
- private String resourceId;
- private String label;
- private boolean selected;
-
- public ExportItem() {
- }
-
- public ExportItem(String resourceId, String label, boolean selected) {
- this.resourceId = resourceId;
- this.label = label;
- this.selected = selected;
- }
-
- public String getResourceId() {
- return resourceId;
- }
-
- public void setResourceId(String resourceId) {
- this.resourceId = resourceId;
- }
-
- public String getLabel() {
- return label;
- }
-
- public void setLabel(String label) {
- this.label = label;
- }
-
- public boolean isSelected() {
- return selected;
+ private ArchiveEntryNames() {
}
- public void setSelected(boolean selected) {
- this.selected = selected;
+ public static String forResourceId(String resourceId) {
+ return resourceId.replace(":", "").replace(".", "");
}
}
diff --git
a/streampipes-data-export/src/test/java/org/apache/streampipes/export/AssetLinkResolverTest.java
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/AssetLinkResolverTest.java
new file mode 100644
index 0000000000..772c4bf957
--- /dev/null
+++
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/AssetLinkResolverTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export;
+
+import org.apache.streampipes.model.file.FileMetadata;
+import org.apache.streampipes.resource.management.SpResourceManager;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AssetLinkResolverTest {
+
+ @Test
+ void shouldIgnoreLinksWithoutQueryHintsAndResolveSupportedLinks() throws
IOException {
+ var resources = mock(SpResourceManager.class, RETURNS_DEEP_STUBS);
+ when(resources.getGenericStorage().findOne("asset-1")).thenReturn(Map.of(
+ "assetName", "Test asset",
+ "assets", List.of(),
+ "assetLinks", List.of(
+ Map.of("resourceId", "external-link"),
+ Map.of("resourceId", "file-1", "queryHint", "file"))));
+ var file = new FileMetadata();
+ file.setFileId("file-1");
+ file.setFilename("test.csv");
+
when(resources.getFileMetadataStorage().getElementById("file-1")).thenReturn(file);
+
+ var result = new AssetLinkResolver("asset-1", null, resources,
null).resolveResources();
+
+ assertEquals("asset-1", result.getAssetId());
+ assertEquals("Test asset", result.getAssetName());
+ assertEquals(1, result.getFiles().size());
+ assertEquals("file-1",
result.getFiles().iterator().next().getResourceId());
+ assertTrue(result.getAdapters().isEmpty());
+ assertTrue(result.getDataViews().isEmpty());
+ assertTrue(result.getDashboards().isEmpty());
+ assertTrue(result.getDataSources().isEmpty());
+ assertTrue(result.getPipelines().isEmpty());
+ assertTrue(result.getDataLakeMeasures().isEmpty());
+ }
+}
diff --git
a/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageCategoryExportTest.java
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageCategoryExportTest.java
new file mode 100644
index 0000000000..94c7322023
--- /dev/null
+++
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageCategoryExportTest.java
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export;
+
+import org.apache.streampipes.commons.zip.ZipFileExtractor;
+import org.apache.streampipes.export.dataimport.PerformImportGenerator;
+import org.apache.streampipes.export.dataimport.PreviewImportGenerator;
+import org.apache.streampipes.export.generator.ZipFileBuilder;
+import org.apache.streampipes.export.resolver.GenericStorageDocumentResolver;
+import org.apache.streampipes.model.export.AssetExportConfiguration;
+import org.apache.streampipes.model.export.ExportConfiguration;
+import org.apache.streampipes.model.export.ExportItem;
+import org.apache.streampipes.model.export.StreamPipesApplicationPackage;
+import org.apache.streampipes.resource.management.SpResourceManager;
+import org.apache.streampipes.serializers.json.JacksonSerializer;
+import org.apache.streampipes.storage.api.system.IGenericStorage;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GenericStorageCategoryExportTest {
+
+ private static final String TYPE = "test:categorized-pipeline";
+ private static final ResourceRegistration<Map<String, Object>> REGISTRATION
= new ResourceRegistration<>(
+ new PermissionDefaults(GenericStorageCategoryExportTest.class, false),
+ "Test pipelines", NamedDocumentResolver::new);
+ private final IGenericStorage storage = mock(IGenericStorage.class);
+ private final SpResourceManager resources = mock(SpResourceManager.class,
RETURNS_DEEP_STUBS);
+ private final Map<String, Object> first = Map.of(
+ "_id", "pipeline-1", "appDocType", TYPE, "name", "Named pipeline",
"_rev", "1-revision");
+ private final Map<String, Object> second = Map.of("_id", "pipeline-2",
"appDocType", TYPE);
+
+ @BeforeEach
+ void setUp() throws IOException {
+ GenericStorageResourceRegistry.INSTANCE.register(TYPE, REGISTRATION);
+ when(resources.getGenericStorage()).thenReturn(storage);
+ when(storage.getAllAppDocTypes()).thenReturn(List.of(TYPE,
"other-documents"));
+ when(storage.findAll(TYPE)).thenReturn(List.of(first, second));
+ // Export sanitization removes _rev from the retrieved map.
+ when(storage.findOne("pipeline-1")).thenAnswer(invocation -> new
HashMap<>(first));
+ when(storage.findOne("pipeline-2")).thenAnswer(invocation -> new
HashMap<>(second));
+ }
+
+ @Test
+ void shouldListNamedCategorySeparatelyAndUseIdWhenNameIsMissing() {
+ var preview = preview();
+
+ assertEquals(List.of("other-documents"),
preview.getGenericStorageAppDocTypes().stream()
+ .map(ExportItem::getResourceId).toList());
+ assertEquals(List.of("Named pipeline", "pipeline-2"),
preview.getGenericStorageDocuments().stream()
+ .map(ExportItem::getLabel).toList());
+ assertTrue(preview.getGenericStorageDocuments().stream().allMatch(item ->
"Test pipelines".equals(item.getCategory())));
+
assertTrue(preview.getGenericStorageDocuments().stream().noneMatch(ExportItem::isSelected));
+ }
+
+ @Test
+ void shouldExportOnlySelectedDocumentsUsingExistingArchiveFormat() throws
IOException {
+ var preview = preview();
+ preview.getGenericStorageDocuments().getFirst().setSelected(true);
+
+ var files = new ZipFileExtractor(new
ByteArrayInputStream(export(preview))).extractZipToMap();
+ var manifest =
JacksonSerializer.getObjectMapper().readValue(files.get("manifest"),
+ StreamPipesApplicationPackage.class);
+
+ assertEquals(Set.of("pipeline-1"), manifest.getGenericStorageDocuments());
+ assertTrue(files.containsKey("pipeline-1"));
+ assertFalse(files.containsKey("pipeline-2"));
+ }
+
+ @Test
+ void shouldStillSupportBulkExportsFromOlderClients() throws IOException {
+ var config = new ExportConfiguration();
+ config.addGenericStorageAppDocType(new ExportItem(TYPE, TYPE, true));
+
+ var files = new ZipFileExtractor(new
ByteArrayInputStream(export(config))).extractZipToMap();
+ var manifest =
JacksonSerializer.getObjectMapper().readValue(files.get("manifest"),
+ StreamPipesApplicationPackage.class);
+
+ assertEquals(Set.of("pipeline-1", "pipeline-2"),
manifest.getGenericStorageDocuments());
+ }
+
+ @Test
+ void shouldCategorizeLegacyArchiveDocumentsDuringImportPreview() throws
IOException {
+ var config = new ExportConfiguration();
+ config.addGenericStorageAppDocType(new ExportItem(TYPE, TYPE, true));
+
+ var preview = new PreviewImportGenerator(null, resources, null)
+ .generate(new ByteArrayInputStream(export(config)));
+
+ assertEquals(2, preview.getGenericStorageDocuments().size());
+ assertTrue(preview.getGenericStorageDocuments().stream().allMatch(item ->
"Test pipelines".equals(item.getCategory())));
+
assertTrue(preview.getGenericStorageDocuments().stream().allMatch(ExportItem::isSelected));
+ assertTrue(preview.getGenericStorageDocuments().stream().anyMatch(item ->
"Named pipeline".equals(item.getLabel())));
+ }
+
+ @Test
+ void shouldKeepUnregisteredDocumentsUncategorized() throws IOException {
+ when(storage.findAll("other-documents")).thenReturn(List.of(Map.of(
+ "_id", "other-1", "appDocType", "other-documents", "name", "Other")));
+ when(storage.findOne("other-1")).thenAnswer(invocation -> new
HashMap<>(Map.of(
+ "_id", "other-1", "appDocType", "other-documents", "name", "Other")));
+ var config = new ExportConfiguration();
+ config.addGenericStorageAppDocType(new ExportItem("other-documents",
"Other", true));
+
+ var preview = new PreviewImportGenerator(null, resources, null)
+ .generate(new ByteArrayInputStream(export(config)));
+
+ var item = preview.getGenericStorageDocuments().iterator().next();
+ assertNull(item.getCategory());
+ assertEquals("other-1", item.getLabel());
+ }
+
+ @Test
+ void shouldFallBackToBulkSelectionWhenDocumentEnumerationFails() throws
IOException {
+ when(storage.findAll(TYPE)).thenThrow(new IOException("unavailable"));
+
+ var preview = preview();
+
+ assertTrue(preview.getGenericStorageDocuments().isEmpty());
+ assertTrue(preview.getGenericStorageAppDocTypes().stream().anyMatch(item
-> TYPE.equals(item.getResourceId())));
+ }
+
+ @Test
+ void shouldNotLetUnselectedCategoryItemsSuppressAnExplicitBulkExport()
throws IOException {
+ var config = preview();
+ config.addGenericStorageAppDocType(new ExportItem(TYPE, TYPE, true));
+
+ var files = new ZipFileExtractor(new
ByteArrayInputStream(export(config))).extractZipToMap();
+ var manifest =
JacksonSerializer.getObjectMapper().readValue(files.get("manifest"),
+ StreamPipesApplicationPackage.class);
+
+ assertEquals(Set.of("pipeline-1", "pipeline-2"),
manifest.getGenericStorageDocuments());
+ }
+
+ @Test
+ void shouldRespectIndividualSelectionsWhenImportingCategorizedDocuments()
throws IOException {
+ var config = new ExportConfiguration();
+ config.addGenericStorageAppDocType(new ExportItem(TYPE, TYPE, true));
+ var archive = export(config);
+ var importConfig = new PreviewImportGenerator(null, resources, null)
+ .generate(new ByteArrayInputStream(archive));
+ importConfig.getGenericStorageDocuments().stream()
+ .filter(item -> "pipeline-1".equals(item.getResourceId()))
+ .forEach(item -> item.setSelected(false));
+
+ new PerformImportGenerator(importConfig, "importer", null, resources, null)
+ .generate(new ByteArrayInputStream(archive));
+
+ var document = ArgumentCaptor.forClass(String.class);
+ verify(storage).create(document.capture());
+ assertEquals("pipeline-2",
JacksonSerializer.getObjectMapper().readTree(document.getValue()).get("_id").asText());
+ }
+
+ @Test
+ void shouldRoundTripDocumentsWithSanitizedArchiveIds() throws IOException {
+
when(resources.managePermissions().findForObjectId(anyString())).thenReturn(List.of());
+ var archive = exportDocumentsWithSpecialIds();
+ var importConfig = new PreviewImportGenerator(null, resources, null)
+ .generate(new ByteArrayInputStream(archive));
+
+ new PerformImportGenerator(importConfig, "importer", null, resources, null)
+ .generate(new ByteArrayInputStream(archive));
+
+ var document = ArgumentCaptor.forClass(String.class);
+ verify(storage, times(3)).create(document.capture());
+ var importedIds = new HashSet<String>();
+ for (var payload : document.getAllValues()) {
+
importedIds.add(JacksonSerializer.getObjectMapper().readTree(payload).get("_id").asText());
+ }
+ assertEquals(Set.of("pipeline:1", "pipeline.2", "pipeline:3.4"),
importedIds);
+ for (var id : importedIds) {
+ verify(resources.managePermissions()).createDefault(id, getClass(),
"importer", false);
+ }
+ }
+
+ @Test
+ void shouldRespectDeselectionForSanitizedArchiveIds() throws IOException {
+ var archive = exportDocumentsWithSpecialIds();
+ var importConfig = new PreviewImportGenerator(null, resources, null)
+ .generate(new ByteArrayInputStream(archive));
+ importConfig.getGenericStorageDocuments().forEach(item ->
item.setSelected(false));
+
+ new PerformImportGenerator(importConfig, "importer", null, resources, null)
+ .generate(new ByteArrayInputStream(archive));
+
+ verify(storage, never()).create(anyString());
+ }
+
+ @Test
+ void shouldRejectDocumentIdsThatDoNotMatchArchiveEntries() throws
IOException {
+ var mapper = JacksonSerializer.getObjectMapper();
+ var manifest = new StreamPipesApplicationPackage();
+ manifest.addGenericStorageDocument("different-id");
+ var archive = ZipFileBuilder.create()
+ .addManifest(mapper.writeValueAsString(manifest))
+ .addText("different-id", mapper.writeValueAsString(Map.of("_id",
"pipeline:1", "appDocType", TYPE)))
+ .buildZip();
+ var generator = new PerformImportGenerator(new AssetExportConfiguration(),
"importer", null, resources, null);
+
+ assertThrows(IllegalArgumentException.class, () -> generator.generate(new
ByteArrayInputStream(archive)));
+ verify(storage, never()).create(anyString());
+ }
+
+ private byte[] exportDocumentsWithSpecialIds() throws IOException {
+ var config = new ExportConfiguration();
+ for (var id : List.of("pipeline:1", "pipeline.2", "pipeline:3.4")) {
+ when(storage.findOne(id)).thenReturn(Map.of("_id", id, "appDocType",
TYPE));
+ config.getGenericStorageDocuments().add(new ExportItem(id, id, true));
+ }
+ return export(config);
+ }
+
+ @Test
+ void shouldOnlyPreviewDocumentsLinkedToSelectedAssetsAndTheirChildren()
throws IOException {
+ when(storage.findAll(TYPE)).thenReturn(List.of(first, second,
+ Map.of("_id", "unrelated", "appDocType", TYPE)));
+ when(storage.findOne("asset-1")).thenReturn(Map.of(
+ "assetName", "First asset",
+ "assetLinks", List.of(Map.of("resourceId", "pipeline-1", "queryHint",
"ai-pipeline")),
+ "assets", List.of()));
+ when(storage.findOne("asset-2")).thenReturn(Map.of(
+ "assetName", "Second asset",
+ "assetLinks", List.of(Map.of("resourceId", "pipeline-1", "queryHint",
"ai-pipeline")),
+ "assets", List.of(Map.of("assetLinks", List.of(
+ Map.of("resourceId", "pipeline-2", "queryHint",
"ai-notebook"))))));
+
+ var firstPreview = ExportManager.getExportPreview(List.of("asset-1"),
null, resources, null);
+ assertTrue(firstPreview.getGenericStorageDocuments().isEmpty());
+ assertEquals(List.of("pipeline-1"),
firstPreview.getAssetExportConfiguration().getFirst()
+ .getGenericStorageDocuments().stream()
+ .map(ExportItem::getResourceId).toList());
+
+ var bothPreview = ExportManager.getExportPreview(List.of("asset-1",
"asset-2"), null, resources, null);
+ assertTrue(bothPreview.getGenericStorageDocuments().isEmpty());
+ var firstAsset = bothPreview.getAssetExportConfiguration().getFirst();
+ var secondAsset = bothPreview.getAssetExportConfiguration().get(1);
+ assertEquals("First asset", firstAsset.getAssetName());
+ assertEquals("Second asset", secondAsset.getAssetName());
+ assertEquals(Set.of("pipeline-1", "pipeline-2"), new
HashSet<>(secondAsset.getGenericStorageDocuments().stream()
+ .map(ExportItem::getResourceId).toList()));
+ firstAsset.getGenericStorageDocuments().forEach(item ->
item.setSelected(false));
+
assertTrue(secondAsset.getGenericStorageDocuments().stream().allMatch(ExportItem::isSelected));
+ verify(storage, never()).findAll(TYPE);
+ }
+
+ @Test
+ void shouldNotPreviewRegisteredDocumentsForAssetsWithoutLinks() throws
IOException {
+ when(storage.findOne("asset-1")).thenReturn(Map.of("assetName", "Empty
asset"));
+
+ var config = ExportManager.getExportPreview(List.of("asset-1"), null,
resources, null);
+
+ assertTrue(config.getGenericStorageDocuments().isEmpty());
+ assertTrue(config.getGenericStorageAppDocTypes().stream().noneMatch(item
-> TYPE.equals(item.getResourceId())));
+ }
+
+ @Test
+ void shouldSkipMissingLinksWithoutOfferingBulkFallback() throws IOException {
+ when(storage.findOne("asset-1")).thenReturn(Map.of(
+ "assetName", "Test asset",
+ "assetLinks", List.of(Map.of("resourceId", "missing", "queryHint",
"ai-pipeline"),
+ Map.of("resourceId", "pipeline-1", "queryHint", "ai-pipeline"))));
+ when(storage.findOne("missing")).thenThrow(new IOException("not found"));
+
+ var config = ExportManager.getExportPreview(List.of("asset-1"), null,
resources, null);
+ assertEquals(List.of("pipeline-1"),
config.getAssetExportConfiguration().getFirst()
+
.getGenericStorageDocuments().stream().map(ExportItem::getResourceId).toList());
+ verify(storage, never()).findAll(TYPE);
+
+
+ assertTrue(config.getGenericStorageDocuments().isEmpty());
+ assertTrue(config.getGenericStorageAppDocTypes().stream().noneMatch(item
-> TYPE.equals(item.getResourceId())));
+ }
+
+ private ExportConfiguration preview() {
+ return ExportManager.getExportPreview(List.of(), null, resources, null);
+ }
+
+ private byte[] export(ExportConfiguration config) throws IOException {
+ return ExportManager.getExportPackage(config, null, resources, null);
+ }
+ private static class NamedDocumentResolver extends
GenericStorageDocumentResolver {
+
+ NamedDocumentResolver(IGenericStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public ExportItem convert(Map<String, Object> document) {
+ var id = document.get("_id").toString();
+ var label = document.get("name");
+ return new ExportItem(id, label instanceof String name &&
!name.isBlank() ? name : id, true);
+ }
+ }
+
+}
diff --git
a/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageResolverTest.java
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageResolverTest.java
new file mode 100644
index 0000000000..0bf0d124bb
--- /dev/null
+++
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageResolverTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export;
+
+import org.apache.streampipes.commons.zip.ZipFileExtractor;
+import org.apache.streampipes.export.dataimport.PerformImportGenerator;
+import org.apache.streampipes.export.dataimport.PreviewImportGenerator;
+import org.apache.streampipes.export.resolver.AbstractResolver;
+import org.apache.streampipes.export.resolver.GenericStorageDocumentResolver;
+import org.apache.streampipes.model.export.AssetExportConfiguration;
+import org.apache.streampipes.model.export.ExportConfiguration;
+import org.apache.streampipes.model.export.ExportItem;
+import org.apache.streampipes.resource.management.SpResourceManager;
+import org.apache.streampipes.serializers.json.JacksonSerializer;
+import org.apache.streampipes.storage.api.system.IGenericStorage;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GenericStorageResolverTest {
+
+ private static final String TYPE = "test:custom-resolver";
+ private static final Function<IGenericStorage, AbstractResolver<Map<String,
Object>>> FACTORY = PipelineDocumentResolver::new;
+ private final IGenericStorage storage = mock(IGenericStorage.class);
+ private final SpResourceManager resources = mock(SpResourceManager.class,
RETURNS_DEEP_STUBS);
+
+ @BeforeEach
+ void setUp() throws IOException {
+ GenericStorageResourceRegistry.INSTANCE.register(TYPE,
+ new ResourceRegistration<>(new PermissionDefaults(getClass(), false),
"Custom pipelines", FACTORY));
+ when(resources.getGenericStorage()).thenReturn(storage);
+ var document = Map.<String, Object>of("_id", "custom-1", "_rev",
"1-revision", "appDocType", TYPE,
+ "title", "Custom name", "running", true, "selectedEndpointUrl",
"http://source");
+ when(storage.getAllAppDocTypes()).thenReturn(List.of(TYPE));
+ when(storage.findAll(TYPE)).thenReturn(List.of(document));
+ when(storage.findOne("custom-1")).thenAnswer(invocation -> new
HashMap<>(document));
+ }
+
+ @Test
+ void shouldUseCustomConversionInExportAndImportPreviews() throws IOException
{
+ var config = ExportManager.getExportPreview(List.of(), null, resources,
null);
+ assertEquals("Custom name",
config.getGenericStorageDocuments().getFirst().getLabel());
+ assertFalse(config.getGenericStorageDocuments().getFirst().isSelected());
+ config.getGenericStorageDocuments().getFirst().setSelected(true);
+
+ var preview = new PreviewImportGenerator(null, resources, null)
+ .generate(new
ByteArrayInputStream(ExportManager.getExportPackage(config, null, resources,
null)));
+ var item = preview.getGenericStorageDocuments().iterator().next();
+ assertEquals("Custom name", item.getLabel());
+ assertEquals("Custom pipelines", item.getCategory());
+ }
+
+ @Test
+ void shouldSanitizeIndividualAndBulkExports() throws IOException {
+ for (boolean bulk : List.of(false, true)) {
+ var config = new ExportConfiguration();
+ if (bulk) {
+ config.addGenericStorageAppDocType(new ExportItem(TYPE, TYPE, true));
+ } else {
+ config.getGenericStorageDocuments().add(new ExportItem("custom-1",
"Custom name", true));
+ }
+ var archive = ExportManager.getExportPackage(config, null, resources,
null);
+ var files = new ZipFileExtractor(new
ByteArrayInputStream(archive)).extractZipToMap();
+ var exported =
JacksonSerializer.getObjectMapper().readTree(files.get("custom-1"));
+ assertFalse(exported.get("running").asBoolean());
+ assertNull(exported.get("selectedEndpointUrl"));
+ assertNull(exported.get("_rev"));
+ }
+ }
+
+ @Test
+ void shouldUseRegisteredResolverForImportAndOverwrite() throws IOException {
+ var registry = new GenericStorageResourceRegistry();
+ var resolver = spy(new PipelineDocumentResolver(storage));
+ registry.register(TYPE, new ResourceRegistration<>(new
PermissionDefaults(getClass(), false),
+ "Custom pipelines", ignored -> resolver));
+ var config = new ExportConfiguration();
+ config.addGenericStorageAppDocType(new ExportItem(TYPE, TYPE, true));
+ var archive = ExportManager.getExportPackage(config, null, resources,
null);
+ var preview = new PreviewImportGenerator(null, resources,
null).generate(new ByteArrayInputStream(archive));
+ preview.setOverwriteExistingDocuments(true);
+
+ new PerformImportGenerator(preview, "importer", null, resources, null,
registry)
+ .generate(new ByteArrayInputStream(archive));
+
+ verify(resolver).deleteDocument(anyString(), any());
+ verify(resolver).writeDocument(anyString(),
any(AssetExportConfiguration.class));
+ }
+
+ private static class PipelineDocumentResolver extends
GenericStorageDocumentResolver {
+
+ PipelineDocumentResolver(IGenericStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public Map<String, Object> modifyDocumentForExport(Map<String, Object>
document) {
+ super.modifyDocumentForExport(document);
+ document.put("running", false);
+ document.remove("selectedEndpointUrl");
+ return document;
+ }
+
+ @Override
+ public ExportItem convert(Map<String, Object> document) {
+ return new ExportItem(document.get("_id").toString(),
document.get("title").toString(), true);
+ }
+ }
+}
diff --git
a/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageResourceRegistryTest.java
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageResourceRegistryTest.java
new file mode 100644
index 0000000000..47c9d5bccb
--- /dev/null
+++
b/streampipes-data-export/src/test/java/org/apache/streampipes/export/GenericStorageResourceRegistryTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.streampipes.export;
+
+import org.apache.streampipes.commons.exceptions.ElementNotFoundException;
+import org.apache.streampipes.export.resolver.GenericStorageDocumentResolver;
+import org.apache.streampipes.export.resolver.GenericStorageResolverProvider;
+import org.apache.streampipes.storage.api.system.IGenericStorage;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GenericStorageResourceRegistryTest {
+
+ @Test
+ void shouldRequireResolverFactoriesForResourceRegistrations() {
+ assertThrows(NullPointerException.class,
+ () -> new ResourceRegistration<>(new PermissionDefaults(getClass(),
false), "Resources", null));
+ }
+
+ @Test
+ void shouldRejectConflictingRegistrationsAndAllowRepeatedRegistration() {
+ var registry = new GenericStorageResourceRegistry();
+ var registration = new ResourceRegistration<>(new
PermissionDefaults(getClass(), false),
+ "Resources", GenericStorageDocumentResolver::new);
+ registry.register("test:resource", registration);
+ registry.register("test:resource", registration);
+
+ assertSame(registration, registry.find("test:resource").orElseThrow());
+ assertThrows(IllegalArgumentException.class, () ->
registry.register("test:resource",
+ new ResourceRegistration<>(new PermissionDefaults(getClass(), true),
+ "Resources", GenericStorageDocumentResolver::new)));
+ }
+
+ @Test
+ void shouldExportWithRegisteredSerializationWithoutReloadingDocument()
throws Exception {
+ var storage = mock(IGenericStorage.class);
+ var registry = new GenericStorageResourceRegistry();
+ registry.register("test:resource", new ResourceRegistration<>(new
PermissionDefaults(getClass(), false),
+ "Resources", CustomSerializationResolver::new));
+ when(storage.findOne("resource-1")).thenReturn(Map.of(
+ "_id", "resource-1", "_rev", "1-revision", "appDocType",
"test:resource"));
+
+ var exported = new GenericStorageResolverProvider(storage,
registry).getSerializedDocument("resource-1");
+
+ assertEquals("custom serialization", exported);
+ verify(storage, times(1)).findOne("resource-1");
+ }
+
+ @Test
+ void shouldPreserveGenericFallbackForUnregisteredTypes() throws Exception {
+ var storage = mock(IGenericStorage.class);
+ when(storage.findOne("unknown-1")).thenReturn(Map.of(
+ "_id", "unknown-1", "_rev", "1-revision", "appDocType", "unknown"));
+ var provider = new GenericStorageResolverProvider(storage, new
GenericStorageResourceRegistry());
+
+ assertFalse(provider.getSerializedDocument("unknown-1").contains("_rev"));
+ assertEquals("unknown-1", provider.toExportItem(Map.of("_id", "unknown-1",
"name", "Other"), true).getLabel());
+ }
+
+ @Test
+ void shouldReportMissingDocuments() throws IOException {
+ var storage = mock(IGenericStorage.class);
+ when(storage.findOne("missing")).thenThrow(new IOException("not found"));
+ var provider = new GenericStorageResolverProvider(storage, new
GenericStorageResourceRegistry());
+
+ assertThrows(ElementNotFoundException.class, () ->
provider.getSerializedDocument("missing"));
+ }
+
+ private static class CustomSerializationResolver extends
GenericStorageDocumentResolver {
+
+ CustomSerializationResolver(IGenericStorage storage) {
+ super(storage);
+ }
+
+ @Override
+ public String serializeDocument(Map<String, Object> document) {
+ assertFalse(document.containsKey("_rev"));
+ return "custom serialization";
+ }
+ }
+}
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
index f6656a4172..4f6195c3f5 100644
---
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportConfiguration.java
@@ -29,10 +29,20 @@ public class ExportConfiguration {
private List<AssetExportConfiguration> assetExportConfiguration;
private List<ExportItem> genericStorageAppDocTypes;
+ private List<ExportItem> genericStorageDocuments;
public ExportConfiguration() {
this.assetExportConfiguration = new ArrayList<>();
this.genericStorageAppDocTypes = new ArrayList<>();
+ this.genericStorageDocuments = new ArrayList<>();
+ }
+
+ public List<ExportItem> getGenericStorageDocuments() {
+ return genericStorageDocuments;
+ }
+
+ public void setGenericStorageDocuments(List<ExportItem>
genericStorageDocuments) {
+ this.genericStorageDocuments = genericStorageDocuments;
}
public List<AssetExportConfiguration> getAssetExportConfiguration() {
diff --git
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
index cb62711a05..79ed281dc5 100644
---
a/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
+++
b/streampipes-model/src/main/java/org/apache/streampipes/model/export/ExportItem.java
@@ -23,6 +23,7 @@ public class ExportItem {
private String resourceId;
private String label;
private boolean selected;
+ private String category;
public ExportItem() {
}
@@ -33,6 +34,14 @@ public class ExportItem {
this.selected = selected;
}
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
public String getResourceId() {
return resourceId;
}
diff --git
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
index b4144727fd..92cf0d4c2e 100644
---
a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
+++
b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts
@@ -17,7 +17,7 @@
* under the License.
*/
// @ts-nocheck
-// Generated using typescript-generator version 3.2.1263 on 2026-10-13
16:58:25.
+// Generated using typescript-generator version 3.2.1263 on 2026-10-26
15:02:34.
export class NamedStreamPipesEntity implements Storable {
'@class':
@@ -1993,6 +1993,7 @@ export class ExportConfig {
export class ExportConfiguration {
assetExportConfiguration: AssetExportConfiguration[];
genericStorageAppDocTypes: ExportItem[];
+ genericStorageDocuments: ExportItem[];
static fromData(
data: ExportConfiguration,
@@ -2008,11 +2009,15 @@ export class ExportConfiguration {
instance.genericStorageAppDocTypes = __getCopyArrayFn(
ExportItem.fromData,
)(data.genericStorageAppDocTypes);
+ instance.genericStorageDocuments = __getCopyArrayFn(
+ ExportItem.fromData,
+ )(data.genericStorageDocuments);
return instance;
}
}
export class ExportItem {
+ category: string;
label: string;
resourceId: string;
selected: boolean;
@@ -2022,6 +2027,7 @@ export class ExportItem {
return data;
}
const instance = target || new ExportItem();
+ instance.category = data.category;
instance.label = data.label;
instance.resourceId = data.resourceId;
instance.selected = data.selected;
diff --git a/ui/src/app/assets/dialog/move-asset-dialog.component.ts
b/ui/src/app/assets/dialog/move-asset-dialog.component.ts
index 16109c5d0c..d3e8cf8886 100644
--- a/ui/src/app/assets/dialog/move-asset-dialog.component.ts
+++ b/ui/src/app/assets/dialog/move-asset-dialog.component.ts
@@ -19,7 +19,6 @@
import { NestedTreeControl } from '@angular/cdk/tree';
import { Component, inject, Input } from '@angular/core';
import {
- FlexDirective,
LayoutAlignDirective,
LayoutDirective,
LayoutGapDirective,
@@ -72,7 +71,6 @@ export type MoveAssetDialogResult =
MatButton,
MatIconButton,
MatIcon,
- FlexDirective,
LayoutDirective,
LayoutAlignDirective,
LayoutGapDirective,
diff --git a/ui/src/app/configuration/export/data-export-import.component.ts
b/ui/src/app/configuration/export/data-export-import.component.ts
index ddaaae54a4..6f6d41aee5 100644
--- a/ui/src/app/configuration/export/data-export-import.component.ts
+++ b/ui/src/app/configuration/export/data-export-import.component.ts
@@ -211,6 +211,7 @@ export class SpDataExportImportComponent implements OnInit {
resourceId: label._id!,
label: label.label,
selected: true,
+ category: null,
}));
referencedSites[asset.elementId] = [...siteIds]
.map(siteId => sitesById.get(siteId))
@@ -219,6 +220,7 @@ export class SpDataExportImportComponent implements OnInit {
resourceId: site._id,
label: site.label,
selected: true,
+ category: null,
}));
});
diff --git
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.spec.ts
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.spec.ts
new file mode 100644
index 0000000000..db89fd414e
--- /dev/null
+++
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.spec.ts
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import { TestBed } from '@angular/core/testing';
+import { DialogRef } from '@streampipes/shared-ui';
+import {
+ AssetExportConfiguration,
+ ExportConfiguration,
+} from '@streampipes/platform-services';
+import { of } from 'rxjs';
+import { describe, expect, it } from 'vitest';
+import { DataExportService } from '../data-export.service';
+import { SpDataExportDialogComponent } from './data-export-dialog.component';
+
+function asset(assetId: string, assetName: string): AssetExportConfiguration {
+ return Object.assign(new AssetExportConfiguration(), {
+ assetId,
+ assetName,
+ adapters: [],
+ dashboards: [],
+ dataViews: [],
+ dataSources: [],
+ dataLakeMeasures: [],
+ files: [],
+ pipelines: [],
+ labels: [],
+ sites: [],
+ genericStorageDocuments: [],
+ });
+}
+
+function component(preview: ExportConfiguration): SpDataExportDialogComponent {
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: DialogRef, useValue: {} },
+ {
+ provide: DataExportService,
+ useValue: { getExportPreview: () => of(preview) },
+ },
+ ],
+ });
+ return TestBed.runInInjectionContext(
+ () => new SpDataExportDialogComponent(),
+ );
+}
+
+function preview(assets: AssetExportConfiguration[]): ExportConfiguration {
+ return Object.assign(new ExportConfiguration(), {
+ assetExportConfiguration: assets,
+ genericStorageAppDocTypes: [],
+ genericStorageDocuments: [
+ {
+ resourceId: 'shared',
+ label: 'Shared pipeline',
+ category: 'AI Pipelines',
+ selected: false,
+ },
+ {
+ resourceId: 'unrelated',
+ label: 'Other pipeline',
+ category: 'AI Pipelines',
+ selected: false,
+ },
+ ],
+ });
+}
+
+describe('generic export asset rows', () => {
+ it('shows named asset rows with independent selections for a shared
resource', () => {
+ const config = preview([asset('a', 'Asset A'), asset('b', 'Asset B')]);
+ const dialog = component(config);
+ dialog.selectedAssets = ['a', 'b'];
+ for (const assetConfig of config.assetExportConfiguration) {
+ assetConfig.genericStorageDocuments = [
+ { ...config.genericStorageDocuments[0] },
+ ];
+ }
+ config.genericStorageDocuments = [];
+ dialog.ngOnInit();
+
+ const rows = dialog.getAssetsForSection('genericStorage::AI
Pipelines');
+ expect(rows.map(row => row.assetName)).toEqual(['Asset A', 'Asset B']);
+ expect(rows.map(row => row.items.map(item =>
item.resourceId))).toEqual(
+ [['shared'], ['shared']],
+ );
+ expect(config.genericStorageDocuments).toEqual([]);
+ dialog.updateSelectedItems('genericStorage::AI Pipelines', rows[0],
[]);
+ expect(
+ config.assetExportConfiguration[0].genericStorageDocuments[0]
+ .selected,
+ ).toBe(false);
+ expect(
+ config.assetExportConfiguration[1].genericStorageDocuments[0]
+ .selected,
+ ).toBe(true);
+ });
+
+ it('retains the global category when no assets are selected', () => {
+ const config = preview([]);
+ const dialog = component(config);
+ dialog.selectedAssets = [];
+ dialog.ngOnInit();
+ expect(
+ dialog.getAssetsForSection('genericStorage::AI
Pipelines')[0].items,
+ ).toHaveLength(2);
+ expect(config.genericStorageDocuments).toHaveLength(2);
+ });
+});
diff --git
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
index 19479290e5..2f03df3b92 100644
---
a/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
+++
b/ui/src/app/configuration/export/export-dialog/data-export-dialog.component.ts
@@ -60,7 +60,7 @@ interface SectionAssetConfiguration {
items: ExportItem[];
}
-type ExportSectionKey =
+type AssetExportSectionKey =
| 'adapters'
| 'dashboards'
| 'dataViews'
@@ -109,7 +109,7 @@ export class SpDataExportDialogComponent implements OnInit {
@Input()
referencedSites: Record<string, ExportItem[]> = {};
- exportSections = [
+ private readonly resourceSections = [
{ key: 'adapters', title: 'Adapters' },
{ key: 'dashboards', title: 'Dashboards' },
{ key: 'dataViews', title: 'Charts' },
@@ -121,6 +121,10 @@ export class SpDataExportDialogComponent implements OnInit
{
{ key: 'sites', title: 'Sites' },
] as const;
+ exportSections: { key: string; title: string }[] = [
+ ...this.resourceSections,
+ ];
+
preview: ExportConfiguration;
exportInProgress = false;
exportAllSelections: Record<string, boolean> = {};
@@ -141,12 +145,12 @@ export class SpDataExportDialogComponent implements
OnInit {
getExportItems(
config: AssetExportConfiguration,
- key: (typeof this.exportSections)[number]['key'],
+ key: AssetExportSectionKey,
): ExportItem[] {
return config[key];
}
- getAssetsForSection(key: ExportSectionKey): SectionAssetConfiguration[] {
+ getAssetsForSection(key: string): SectionAssetConfiguration[] {
return this.sectionAssets[key] ?? [];
}
@@ -165,7 +169,7 @@ export class SpDataExportDialogComponent implements OnInit {
}
selectAllItemsForAsset(
- key: ExportSectionKey,
+ key: string,
assetConfig: SectionAssetConfiguration,
select: boolean,
): void {
@@ -177,7 +181,7 @@ export class SpDataExportDialogComponent implements OnInit {
}
isExportAllSelected(
- key: ExportSectionKey,
+ key: string,
assetConfig: SectionAssetConfiguration,
): boolean {
return this.exportAllSelections[
@@ -186,7 +190,7 @@ export class SpDataExportDialogComponent implements OnInit {
}
changeExportAllSelection(
- key: ExportSectionKey,
+ key: string,
assetConfig: SectionAssetConfiguration,
event: MatSlideToggleChange,
): void {
@@ -203,7 +207,7 @@ export class SpDataExportDialogComponent implements OnInit {
}
getSelectedItemResourceIds(
- key: ExportSectionKey,
+ key: string,
assetConfig: SectionAssetConfiguration,
): string[] {
return (
@@ -214,7 +218,7 @@ export class SpDataExportDialogComponent implements OnInit {
}
updateSelectedItems(
- key: ExportSectionKey,
+ key: string,
assetConfig: SectionAssetConfiguration,
selectedResourceIds: string[],
): void {
@@ -276,7 +280,8 @@ export class SpDataExportDialogComponent implements OnInit {
private initializeSectionAssets(): void {
this.sectionAssets = {};
- this.exportSections.forEach(section => {
+ this.exportSections = [...this.resourceSections];
+ this.resourceSections.forEach(section => {
this.sectionAssets[section.key] =
this.preview.assetExportConfiguration
.map(config => ({
@@ -286,11 +291,44 @@ export class SpDataExportDialogComponent implements
OnInit {
}))
.filter(config => config.items.length > 0);
});
+
+ const genericSections = new Map<string, SectionAssetConfiguration[]>();
+ for (const config of this.preview.assetExportConfiguration) {
+ for (const [title, items] of Object.entries(
+ groupExportItems(config.genericStorageDocuments),
+ )) {
+ const rows = genericSections.get(title) ?? [];
+ rows.push({
+ assetId: config.assetId,
+ assetName: config.assetName,
+ items,
+ });
+ genericSections.set(title, rows);
+ }
+ }
+ for (const [title, items] of Object.entries(
+ groupExportItems(this.preview.genericStorageDocuments),
+ )) {
+ const rows = genericSections.get(title) ?? [];
+ rows.push({
+ assetId: `genericStorage::${title}`,
+ assetName: '',
+ items,
+ });
+ genericSections.set(title, rows);
+ }
+ for (const [title, rows] of [...genericSections].sort(
+ ([left], [right]) => left.localeCompare(right),
+ )) {
+ const key = `genericStorage::${title}`;
+ this.exportSections.push({ key, title });
+ this.sectionAssets[key] = rows;
+ }
}
private sortPreviewItems(): void {
this.preview.assetExportConfiguration.forEach(config => {
- this.exportSections.forEach(section => {
+ this.resourceSections.forEach(section => {
this.sortExportItems(config[section.key]);
});
});
@@ -303,7 +341,7 @@ export class SpDataExportDialogComponent implements OnInit {
}
private getExportAllSelectionKey(
- key: ExportSectionKey,
+ key: string,
assetConfig: SectionAssetConfiguration,
): string {
return `${key}::${assetConfig.assetId}`;
@@ -336,3 +374,19 @@ export class SpDataExportDialogComponent implements OnInit
{
return [...existingItems, ...missingItems];
}
}
+
+export function groupExportItems(
+ items: ExportItem[] = [],
+): Record<string, ExportItem[]> {
+ return [...(items ?? [])]
+ .sort((left, right) => left.label.localeCompare(right.label))
+ .reduce(
+ (groups, item) => {
+ if (item.category) {
+ (groups[item.category] ??= []).push(item);
+ }
+ return groups;
+ },
+ Object.create(null) as Record<string, ExportItem[]>,
+ );
+}
diff --git
a/ui/src/app/configuration/export/export-dialog/generic-storage-items/generic-storage-items.component.ts
b/ui/src/app/configuration/export/export-dialog/generic-storage-items/generic-storage-items.component.ts
index 53da522613..633a1444a6 100644
---
a/ui/src/app/configuration/export/export-dialog/generic-storage-items/generic-storage-items.component.ts
+++
b/ui/src/app/configuration/export/export-dialog/generic-storage-items/generic-storage-items.component.ts
@@ -63,6 +63,7 @@ export class GenericStorageItemsComponent {
resourceId: this.newDocumentId,
label: this.newDocumentId,
selected: true,
+ category: null,
});
}
diff --git
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
index ffb54ca76c..430e19be35 100644
---
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
+++
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.html
@@ -148,11 +148,23 @@
[exportItems]="importConfiguration.sites"
[sectionTitle]="'Sites' | translate"
></sp-data-export-item>
- <sp-generic-storage-items
- [exportItems]="importConfiguration.genericStorageDocuments"
- [importMode]="true"
- >
- </sp-generic-storage-items>
+ @for (
+ category of groupExportItems(
+ importConfiguration.genericStorageDocuments
+ ) | keyvalue;
+ track category.key
+ ) {
+ <sp-data-export-item
+ [exportItems]="category.value"
+ [sectionTitle]="category.key | translate"
+ ></sp-data-export-item>
+ }
+ @if (genericDocuments.length > 0) {
+ <sp-generic-storage-items
+ [exportItems]="genericDocuments"
+ [importMode]="true"
+ ></sp-generic-storage-items>
+ }
@if (currentImportStep === 1) {
<sp-split-section
[level]="3"
diff --git
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
index 71eb45abd6..03908451ff 100644
---
a/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
+++
b/ui/src/app/configuration/export/import-dialog/data-import-dialog.component.ts
@@ -51,6 +51,8 @@ import { GenericStorageItemsComponent } from
'../export-dialog/generic-storage-i
import { MatCheckbox } from '@angular/material/checkbox';
import { FormsModule } from '@angular/forms';
import { MatDivider } from '@angular/material/divider';
+import { KeyValuePipe } from '@angular/common';
+import { groupExportItems } from
'../export-dialog/data-export-dialog.component';
@Component({
selector: 'sp-data-import-dialog',
@@ -78,6 +80,7 @@ import { MatDivider } from '@angular/material/divider';
SpSpinnerComponent,
MatDivider,
TranslatePipe,
+ KeyValuePipe,
],
})
export class SpDataImportDialogComponent {
@@ -88,6 +91,13 @@ export class SpDataImportDialogComponent {
private translateService = inject(TranslateService);
currentImportStep = 0;
+ readonly groupExportItems = groupExportItems;
+
+ get genericDocuments(): ExportItem[] {
+ return this.importConfiguration.genericStorageDocuments.filter(
+ item => !item.category,
+ );
+ }
inputValue: string;
fileName: string;