diff --git a/gradle/layers.versions.toml b/gradle/layers.versions.toml index c4d40b332..12460694b 100644 --- a/gradle/layers.versions.toml +++ b/gradle/layers.versions.toml @@ -1,4 +1,4 @@ [versions] -xtraplatform-core = '7.0.0-SNAPSHOT' +xtraplatform-core = '7.0.0-ldp-95-audit-logging-new-module-SNAPSHOT' xtraplatform-native = '2.6.0-SNAPSHOT' diff --git a/xtraplatform-features-gml/build.gradle b/xtraplatform-features-gml/build.gradle index 1cc0f5132..b7eb10154 100644 --- a/xtraplatform-features-gml/build.gradle +++ b/xtraplatform-features-gml/build.gradle @@ -1,4 +1,3 @@ - maturity = 'CANDIDATE' maintenance = 'NONE' description = 'WFS feature provider and GML features.' @@ -9,6 +8,7 @@ dependencies { provided 'de.interactive_instruments:xtraplatform-streams' provided 'de.interactive_instruments:xtraplatform-values' provided 'de.interactive_instruments:xtraplatform-web' + provided 'de.interactive_instruments:xtraplatform-services' provided project(':xtraplatform-codelists') provided project(':xtraplatform-cql') provided project(':xtraplatform-crs') diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java index 4620e2d8f..cbf17913e 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java @@ -57,6 +57,7 @@ import de.ii.xtraplatform.features.gml.domain.FeatureProviderWfsData; import de.ii.xtraplatform.features.gml.domain.WfsConnector; import de.ii.xtraplatform.features.gml.domain.XMLNamespaceNormalizer; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; @@ -142,6 +143,7 @@ public FeatureProviderWfs( Reactive reactive, ValueStore valueStore, ProviderExtensionRegistry extensionRegistry, + AuditLog auditLog, VolatileRegistry volatileRegistry, @Assisted FeatureProviderDataV2 data) { super( @@ -151,6 +153,7 @@ public FeatureProviderWfs( crsInfo, extensionRegistry, valueStore.forType(Codelist.class), + auditLog, data, volatileRegistry); @@ -227,10 +230,9 @@ protected FeatureQueryEncoder getQueryEncoder() { private FeatureTokenDecoder< byte[], FeatureSchema, SchemaMapping, ModifiableContext> getDecoder(Query query, Map mappings, boolean passThrough) { - if (!(query instanceof FeatureQuery)) { + if (!(query instanceof FeatureQuery featureQuery)) { throw new IllegalArgumentException(); } - FeatureQuery featureQuery = (FeatureQuery) query; Map namespaces = getData().getConnectionInfo().getNamespaces(); XMLNamespaceNormalizer namespaceNormalizer = new XMLNamespaceNormalizer(namespaces); FeatureSchema featureSchema = getData().getTypes().get(featureQuery.getType()); @@ -376,6 +378,7 @@ public FeatureStream getFeatureStreamPassThrough(FeatureQuery query) { nativeCrsIs3d, getCodelists(), this::runQuery, - false); + false, + auditLog); } } diff --git a/xtraplatform-features-graphql/build.gradle b/xtraplatform-features-graphql/build.gradle index ece5323a0..849a1e5f7 100644 --- a/xtraplatform-features-graphql/build.gradle +++ b/xtraplatform-features-graphql/build.gradle @@ -9,6 +9,7 @@ dependencies { provided 'de.interactive_instruments:xtraplatform-streams' provided 'de.interactive_instruments:xtraplatform-values' provided 'de.interactive_instruments:xtraplatform-web' + provided 'de.interactive_instruments:xtraplatform-services' provided project(':xtraplatform-codelists') provided project(':xtraplatform-cql') provided project(':xtraplatform-crs') diff --git a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java index 4158afa5f..0ad1914ce 100644 --- a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java +++ b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java @@ -49,6 +49,7 @@ import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.graphql.domain.FeatureProviderGraphQlData; import de.ii.xtraplatform.features.graphql.domain.GraphQlConnector; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; @@ -197,6 +198,7 @@ public FeatureProviderGraphQl( Reactive reactive, ValueStore valueStore, ProviderExtensionRegistry extensionRegistry, + AuditLog auditLog, VolatileRegistry volatileRegistry, @Assisted FeatureProviderDataV2 data) { super( @@ -206,6 +208,7 @@ public FeatureProviderGraphQl( crsInfo, extensionRegistry, valueStore.forType(Codelist.class), + auditLog, data, volatileRegistry); @@ -271,10 +274,9 @@ protected FeatureQueryEncoder getQueryEncoder() { protected FeatureTokenDecoder< byte[], FeatureSchema, SchemaMapping, ModifiableContext> getDecoder(Query query, Map mappings) { - if (!(query instanceof FeatureQuery)) { + if (!(query instanceof FeatureQuery featureQuery)) { throw new IllegalArgumentException(); } - FeatureQuery featureQuery = (FeatureQuery) query; FeatureSchema featureSchema = getSourceSchemas().get(featureQuery.getType()).get(0); String name = featureSchema.getSourcePath().map(sourcePath -> sourcePath.substring(1)).orElse(null); diff --git a/xtraplatform-features-oracle/src/main/java/de/ii/xtraplatform/features/oracle/app/FeatureProviderOracle.java b/xtraplatform-features-oracle/src/main/java/de/ii/xtraplatform/features/oracle/app/FeatureProviderOracle.java index 257b525a2..c9c2bac42 100644 --- a/xtraplatform-features-oracle/src/main/java/de/ii/xtraplatform/features/oracle/app/FeatureProviderOracle.java +++ b/xtraplatform-features-oracle/src/main/java/de/ii/xtraplatform/features/oracle/app/FeatureProviderOracle.java @@ -35,6 +35,7 @@ import de.ii.xtraplatform.features.sql.domain.SqlQueryOptions; import de.ii.xtraplatform.features.sql.domain.SqlRow; import de.ii.xtraplatform.geometries.domain.transcode.wktwkb.WkbDialect; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.services.domain.Scheduler; import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.values.domain.ValueStore; @@ -126,6 +127,7 @@ public FeatureProviderOracle( VolatileRegistry volatileRegistry, Cache cache, Scheduler scheduler, + AuditLog auditLog, @Assisted FeatureProviderDataV2 data) { super( crsTransformerFactory, @@ -140,6 +142,7 @@ public FeatureProviderOracle( volatileRegistry, cache, scheduler, + auditLog, data, Map.of()); } diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java index 68600a73a..ddf8e59c2 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java @@ -100,6 +100,7 @@ import de.ii.xtraplatform.features.sql.domain.SqlQueryColumn.Operation; import de.ii.xtraplatform.features.sql.infra.db.SourceSchemaValidatorSql; import de.ii.xtraplatform.geometries.domain.transcode.wktwkb.WkbDialect; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.services.domain.Scheduler; import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.RunnableStream; @@ -483,6 +484,7 @@ public FeatureProviderSql( VolatileRegistry volatileRegistry, Cache cache, Scheduler scheduler, + AuditLog auditLog, @Assisted FeatureProviderDataV2 data) { this( crsTransformerFactory, @@ -497,6 +499,7 @@ public FeatureProviderSql( volatileRegistry, cache, scheduler, + auditLog, data, decoderFactories.getConnectorDecoders()); } @@ -514,6 +517,7 @@ protected FeatureProviderSql( VolatileRegistry volatileRegistry, Cache cache, Scheduler scheduler, + AuditLog auditLog, FeatureProviderDataV2 data, Map subdecoders) { super( @@ -523,6 +527,7 @@ protected FeatureProviderSql( crsInfo, extensionRegistry, valueStore.forType(Codelist.class), + auditLog, data, volatileRegistry); @@ -945,11 +950,7 @@ public boolean supportsMutationsInternal() { if (!Objects.equals(getData().getConnectionInfo().getDialect(), SqlDbmsPgis.ID)) { return false; } - if (!getData().getDatasetChanges().isModeCrud()) { - return false; - } - - return true; + return getData().getDatasetChanges().isModeCrud(); } @Override @@ -1335,7 +1336,7 @@ private MutationResult writeFeatures( crsTransformerFactory, getData().getNativeTimeZone(), partial ? Optional.of(FeatureTransactions.PATCH_NULL_VALUE) : Optional.empty())) - .via(Transformer.map(feature -> (FeatureDataSql) feature)); + .via(Transformer.map(feature -> feature)); if (partial) { featureSqlSource = @@ -1449,7 +1450,8 @@ public FeatureStream getFeatureStream(MultiFeatureQuery query) { nativeCrsIs3d, getCodelists(), this::runQuery, - !query.hitsOnly()); + !query.hitsOnly(), + auditLog); } @Override diff --git a/xtraplatform-features/build.gradle b/xtraplatform-features/build.gradle index 26c542246..f5387471f 100644 --- a/xtraplatform-features/build.gradle +++ b/xtraplatform-features/build.gradle @@ -1,13 +1,14 @@ - maturity = 'MATURE' maintenance = 'FULL' description = 'Feature providers and transformations.' descriptionDe = 'Feature-Provider und Transformationen.' dependencies { + provided 'de.interactive_instruments:xtraplatform-base' provided 'de.interactive_instruments:xtraplatform-entities' provided 'de.interactive_instruments:xtraplatform-streams' provided 'de.interactive_instruments:xtraplatform-values' + provided 'de.interactive_instruments:xtraplatform-services' provided project(':xtraplatform-codelists') provided project(':xtraplatform-cql') provided project(':xtraplatform-crs') diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java index 076431621..65ba0ce07 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java @@ -34,6 +34,7 @@ import de.ii.xtraplatform.features.domain.transform.WithScope; import de.ii.xtraplatform.features.domain.transform.WithoutProperties; import de.ii.xtraplatform.geometries.domain.GeometryType; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Runner; import de.ii.xtraplatform.streams.domain.Reactive.Stream; @@ -85,6 +86,8 @@ public abstract class AbstractFeatureProvider< private boolean datasetChangedForced; private String previousDataset; + protected AuditLog auditLog; + protected AbstractFeatureProvider( ConnectorFactory connectorFactory, Reactive reactive, @@ -92,6 +95,7 @@ protected AbstractFeatureProvider( CrsInfo crsInfo, ProviderExtensionRegistry extensionRegistry, Values codelistStore, + AuditLog auditLog, FeatureProviderDataV2 data, VolatileRegistry volatileRegistry) { super(data, volatileRegistry); @@ -101,6 +105,7 @@ protected AbstractFeatureProvider( this.crsInfo = crsInfo; this.extensionRegistry = extensionRegistry; this.codelistStore = codelistStore; + this.auditLog = auditLog; this.volatileRegistry = volatileRegistry; this.changeHandler = new FeatureChangeHandlerImpl(); this.connector = @@ -520,7 +525,8 @@ public FeatureStream getFeatureStream(FeatureQuery query) { nativeCrsIs3d, getCodelists(), this::runQuery, - !query.hitsOnly()); + !query.hitsOnly(), + auditLog); } // TODO: more tests @@ -587,8 +593,7 @@ private FeatureTokenSource getFeatureTokenSource( private Map createMapping( Query query, Map propertyTransformations) { - if (query instanceof FeatureQuery) { - FeatureQuery featureQuery = (FeatureQuery) query; + if (query instanceof FeatureQuery featureQuery) { WithScope withScope = featureQuery.getSchemaScope() == SchemaBase.Scope.RETURNABLE diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java index eb28bab64..567887b7e 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java @@ -73,6 +73,8 @@ public interface FeatureSchema String CONCAT_ELEMENT = "_CONCAT_ELEMENT_"; String COALESCE_ELEMENT = "_COALESCE_ELEMENT_"; + Optional getAudit(); + @JsonIgnore @Override String getName(); @@ -1146,8 +1148,7 @@ default FeatureSchema accept(FeatureSchemaTransformer visitor, List new SimpleEntry<>( - entry.getKey(), - (FeatureSchema) visit.apply(entry.getValue()))) + entry.getKey(), visit.apply(entry.getValue()))) .collect( ImmutableMap.toImmutableMap( Map.Entry::getKey, Map.Entry::getValue))) diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStream.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStream.java index bdaf88659..1fff6f79d 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStream.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStream.java @@ -37,6 +37,7 @@ enum PipelineSteps { CLEAN, ETAG, METADATA, + AUDIT, ALL } @@ -148,23 +149,39 @@ default boolean isSuccess() { default CompletionStage runWith( Sink sink, Map propertyTransformations) { - return runWith(sink, propertyTransformations, new CompletableFuture<>()); + return runWith(sink, propertyTransformations, new CompletableFuture<>(), Optional.empty()); + } + + default CompletionStage runWith( + Sink sink, + Map propertyTransformations, + CompletableFuture onCollectionMetadata) { + return runWith(sink, propertyTransformations, onCollectionMetadata, Optional.empty()); } CompletionStage runWith( Sink sink, Map propertyTransformations, - CompletableFuture onCollectionMetadata); + CompletableFuture onCollectionMetadata, + Optional requestId); default CompletionStage> runWith( SinkReduced sink, Map propertyTransformations) { - return runWith(sink, propertyTransformations, new CompletableFuture<>()); + return runWith(sink, propertyTransformations, new CompletableFuture<>(), Optional.empty()); + } + + default CompletionStage> runWith( + SinkReduced sink, + Map propertyTransformations, + CompletableFuture onCollectionMetadata) { + return runWith(sink, propertyTransformations, onCollectionMetadata, Optional.empty()); } CompletionStage> runWith( SinkReduced sink, Map propertyTransformations, - CompletableFuture onCollectionMetadata); + CompletableFuture onCollectionMetadata, + Optional requestId); // CompletionStage runWith(SinkTransformed sink, // Optional propertyTransformations); diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java index 65400a1a9..77563545d 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureStreamImpl.java @@ -19,6 +19,7 @@ import de.ii.xtraplatform.features.domain.transform.ImmutablePropertyTransformation; import de.ii.xtraplatform.features.domain.transform.PropertyTransformation; import de.ii.xtraplatform.features.domain.transform.PropertyTransformations; +import de.ii.xtraplatform.services.domain.AuditLog; import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Sink; import de.ii.xtraplatform.streams.domain.Reactive.SinkReduced; @@ -50,6 +51,9 @@ public class FeatureStreamImpl implements FeatureStream { private final boolean stepClean; private final boolean stepEtag; private final boolean stepMetadata; + private final boolean stepAudit; + + private final AuditLog auditLog; public FeatureStreamImpl( Query query, @@ -58,7 +62,8 @@ public FeatureStreamImpl( boolean nativeCrsIs3d, Map codelists, QueryRunner runner, - boolean doTransform) { + boolean doTransform, + AuditLog auditLog) { this.query = query; this.data = data; this.crsTransformerFactory = crsTransformerFactory; @@ -66,6 +71,7 @@ public FeatureStreamImpl( this.codelists = codelists; this.runner = runner; this.doTransform = doTransform; + this.auditLog = auditLog; this.stepMappingSchema = !query.skipPipelineSteps().contains(PipelineSteps.MAPPING_SCHEMA) @@ -87,13 +93,150 @@ public FeatureStreamImpl( this.stepMetadata = !query.skipPipelineSteps().contains(PipelineSteps.METADATA) && !query.skipPipelineSteps().contains(PipelineSteps.ALL); + this.stepAudit = + !query.skipPipelineSteps().contains(PipelineSteps.AUDIT) + && !query.skipPipelineSteps().contains(PipelineSteps.ALL); + } + + static Map getMergedTransformations( + Map featureSchemas, + Query query, + Map propertyTransformations) { + if (query instanceof FeatureQuery featureQuery) { + return ImmutableMap.of( + featureQuery.getType(), + getPropertyTransformations( + featureSchemas, + featureQuery, + Optional.ofNullable(propertyTransformations.get(featureQuery.getType())))); + } + + if (query instanceof MultiFeatureQuery multiFeatureQuery) { + return multiFeatureQuery.getQueries().stream() + .map( + typeQuery -> + new SimpleImmutableEntry<>( + typeQuery.getType(), + getPropertyTransformations( + featureSchemas, + typeQuery, + Optional.ofNullable(propertyTransformations.get(typeQuery.getType()))))) + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + return ImmutableMap.of(); + } + + static PropertyTransformations getPropertyTransformations( + Map featureSchemas, + TypeQuery typeQuery, + Optional propertyTransformations) { + if (typeQuery instanceof FeatureQuery + && ((FeatureQuery) typeQuery).getSchemaScope() == SchemaBase.Scope.RECEIVABLE) { + return () -> + getProviderTransformations( + featureSchemas.get(typeQuery.getType()), SchemaBase.Scope.RECEIVABLE); + } + + PropertyTransformations providerTransformations = + () -> + getProviderTransformations( + featureSchemas.get(typeQuery.getType()), SchemaBase.Scope.RETURNABLE); + + PropertyTransformations merged = + propertyTransformations + .map(p -> p.mergeInto(providerTransformations)) + .orElse(providerTransformations); + + return applyRename(merged); + } + + private static PropertyTransformations applyRename( + PropertyTransformations propertyTransformations) { + if (propertyTransformations.getTransformations().values().stream() + .flatMap(Collection::stream) + .anyMatch(propertyTransformation -> propertyTransformation.getRename().isPresent())) { + Map> renamed = new LinkedHashMap<>(); + + propertyTransformations + .getTransformations() + .forEach( + (key, value) -> { + Optional rename = + value.stream() + .filter( + propertyTransformation -> + propertyTransformation.getRename().isPresent()) + .map(propertyTransformation -> propertyTransformation.getRename().get()) + .findFirst(); + + if (rename.isPresent()) { + renamed.put(rename.get(), value); + + String prefix = key + "."; + + propertyTransformations + .getTransformations() + .forEach( + (key2, value2) -> { + if (key2.startsWith(prefix)) { + renamed.put(key2.replace(key, rename.get()), value2); + } + }); + } + }); + + return propertyTransformations.mergeInto(() -> renamed); + } + + return propertyTransformations; + } + + private static Map> getProviderTransformations( + FeatureSchema featureSchema, SchemaBase.Scope scope) { + return featureSchema + .accept( + scope == SchemaBase.Scope.RECEIVABLE + ? AbstractFeatureProvider.WITH_SCOPE_RECEIVABLE + : AbstractFeatureProvider.WITH_SCOPE_RETURNABLE) + .accept( + (schema, visitedProperties) -> + java.util.stream.Stream.concat( + getProviderTransformationsForProperty(schema, scope), + visitedProperties.stream().flatMap(m -> m.entrySet().stream())) + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))); + } + + private static java.util.stream.Stream>> + getProviderTransformationsForProperty(FeatureSchema schema, SchemaBase.Scope scope) { + if (schema.getTransformations().isEmpty()) { + if (schema.isTemporal() && schema.getType() == SchemaBase.Type.DATETIME) { + return java.util.stream.Stream.of( + Map.entry( + schema.getFullPathAsString(), + List.of( + new ImmutablePropertyTransformation.Builder() + .dateFormat(DATETIME_FORMAT) + .build()))); + } + return java.util.stream.Stream.empty(); + } + + return java.util.stream.Stream.of( + Map.entry( + schema.getFullPath().isEmpty() ? WILDCARD : schema.getFullPathAsString(), + schema.getTransformations().stream() + // TODO: mark transformations with scope? + .filter(pt -> scope != SchemaBase.Scope.RECEIVABLE || pt.getWrap().isPresent()) + .toList())); } @Override public CompletionStage runWith( Sink sink, Map propertyTransformations, - CompletableFuture onCollectionMetadata) { + CompletableFuture onCollectionMetadata, + Optional requestId) { Map mergedTransformations = getMergedTransformations(data.getTypes(), query, propertyTransformations); @@ -126,6 +269,16 @@ public CompletionStage runWith( source = source.via(new FeatureTokenTransformerMetadata(resultBuilder)); } + if (stepAudit) { + if (auditLog.isEnabled()) { + if (requestId.isEmpty()) { + LOGGER.error("Audit logging not possible, no request-id provided!"); + } else if (auditLog.logIsAvailable(requestId.get())) { + source = source.via(new FeatureTokenTransformerAudit(requestId.get(), auditLog)); + } + } + } + source = source.via(new FeatureTokenTransformerHooks(resultBuilder, onCollectionMetadata)); @@ -142,7 +295,7 @@ public CompletionStage runWith( if (strongETag && x instanceof byte[]) { eTag.put((byte[]) x); } - return builder.isEmpty(x instanceof byte[] ? ((byte[]) x).length <= 0 : false); + return builder.isEmpty(x instanceof byte[] && ((byte[]) x).length <= 0); }) .handleEnd( (ImmutableResult.Builder builder1) -> { @@ -160,7 +313,8 @@ public CompletionStage runWith( public CompletionStage> runWith( SinkReduced sink, Map propertyTransformations, - CompletableFuture onCollectionMetadata) { + CompletableFuture onCollectionMetadata, + Optional requestId) { Map mergedTransformations = getMergedTransformations(data.getTypes(), query, propertyTransformations); @@ -171,7 +325,7 @@ public CompletionStage> runWith( doTransform ? getFeatureTokenSourceTransformed(tokenSource, mergedTransformations) : tokenSource; - ImmutableResultReduced.Builder resultBuilder = ImmutableResultReduced.builder(); + ImmutableResultReduced.Builder resultBuilder = ImmutableResultReduced.builder(); final ETag.Incremental eTag = ETag.incremental(); final boolean strongETag = query instanceof FeatureQuery @@ -191,6 +345,17 @@ public CompletionStage> runWith( if (stepMetadata) { source = source.via(new FeatureTokenTransformerMetadata(resultBuilder)); } + + if (stepAudit) { + if (auditLog.isEnabled()) { + if (requestId.isEmpty()) { + LOGGER.error("Audit logging not possible, no request-id provided!"); + } else if (auditLog.logIsAvailable(requestId.get())) { + source = source.via(new FeatureTokenTransformerAudit(requestId.get(), auditLog)); + } + } + } + source = source.via(new FeatureTokenTransformerHooks(resultBuilder, onCollectionMetadata)); @@ -278,137 +443,4 @@ private FeatureTokenSource getFeatureTokenSourceTransformed( return tokenSourceTransformed; } - - static Map getMergedTransformations( - Map featureSchemas, - Query query, - Map propertyTransformations) { - if (query instanceof FeatureQuery featureQuery) { - return ImmutableMap.of( - featureQuery.getType(), - getPropertyTransformations( - featureSchemas, - featureQuery, - Optional.ofNullable(propertyTransformations.get(featureQuery.getType())))); - } - - if (query instanceof MultiFeatureQuery multiFeatureQuery) { - return multiFeatureQuery.getQueries().stream() - .map( - typeQuery -> - new SimpleImmutableEntry<>( - typeQuery.getType(), - getPropertyTransformations( - featureSchemas, - typeQuery, - Optional.ofNullable(propertyTransformations.get(typeQuery.getType()))))) - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - return ImmutableMap.of(); - } - - static PropertyTransformations getPropertyTransformations( - Map featureSchemas, - TypeQuery typeQuery, - Optional propertyTransformations) { - if (typeQuery instanceof FeatureQuery - && ((FeatureQuery) typeQuery).getSchemaScope() == SchemaBase.Scope.RECEIVABLE) { - return () -> - getProviderTransformations( - featureSchemas.get(typeQuery.getType()), SchemaBase.Scope.RECEIVABLE); - } - - PropertyTransformations providerTransformations = - () -> - getProviderTransformations( - featureSchemas.get(typeQuery.getType()), SchemaBase.Scope.RETURNABLE); - - PropertyTransformations merged = - propertyTransformations - .map(p -> p.mergeInto(providerTransformations)) - .orElse(providerTransformations); - - return applyRename(merged); - } - - private static PropertyTransformations applyRename( - PropertyTransformations propertyTransformations) { - if (propertyTransformations.getTransformations().values().stream() - .flatMap(Collection::stream) - .anyMatch(propertyTransformation -> propertyTransformation.getRename().isPresent())) { - Map> renamed = new LinkedHashMap<>(); - - propertyTransformations - .getTransformations() - .forEach( - (key, value) -> { - Optional rename = - value.stream() - .filter( - propertyTransformation -> - propertyTransformation.getRename().isPresent()) - .map(propertyTransformation -> propertyTransformation.getRename().get()) - .findFirst(); - - if (rename.isPresent()) { - renamed.put(rename.get(), value); - - String prefix = key + "."; - - propertyTransformations - .getTransformations() - .forEach( - (key2, value2) -> { - if (key2.startsWith(prefix)) { - renamed.put(key2.replace(key, rename.get()), value2); - } - }); - } - }); - - return propertyTransformations.mergeInto(() -> renamed); - } - - return propertyTransformations; - } - - private static Map> getProviderTransformations( - FeatureSchema featureSchema, SchemaBase.Scope scope) { - return featureSchema - .accept( - scope == SchemaBase.Scope.RECEIVABLE - ? AbstractFeatureProvider.WITH_SCOPE_RECEIVABLE - : AbstractFeatureProvider.WITH_SCOPE_RETURNABLE) - .accept( - (schema, visitedProperties) -> - java.util.stream.Stream.concat( - getProviderTransformationsForProperty(schema, scope), - visitedProperties.stream().flatMap(m -> m.entrySet().stream())) - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))); - } - - private static java.util.stream.Stream>> - getProviderTransformationsForProperty(FeatureSchema schema, SchemaBase.Scope scope) { - if (schema.getTransformations().isEmpty()) { - if (schema.isTemporal() && schema.getType() == SchemaBase.Type.DATETIME) { - return java.util.stream.Stream.of( - Map.entry( - schema.getFullPathAsString(), - List.of( - new ImmutablePropertyTransformation.Builder() - .dateFormat(DATETIME_FORMAT) - .build()))); - } - return java.util.stream.Stream.empty(); - } - - return java.util.stream.Stream.of( - Map.entry( - schema.getFullPath().isEmpty() ? WILDCARD : schema.getFullPathAsString(), - schema.getTransformations().stream() - // TODO: mark transformations with scope? - .filter(pt -> scope != SchemaBase.Scope.RECEIVABLE || pt.getWrap().isPresent()) - .toList())); - } } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerAudit.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerAudit.java new file mode 100644 index 000000000..4eea16302 --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerAudit.java @@ -0,0 +1,72 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.domain; + +import de.ii.xtraplatform.features.domain.SchemaBase.Role; +import de.ii.xtraplatform.services.domain.AuditLog; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FeatureTokenTransformerAudit extends FeatureTokenTransformer { + + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureTokenTransformerAudit.class); + private final String requestId; + private final AuditLog auditLog; + private final Map featureHolder = new LinkedHashMap<>(); + private final List> featureList = new ArrayList<>(); + + public FeatureTokenTransformerAudit(String requestId, AuditLog auditLog) { + this.requestId = requestId; + this.auditLog = auditLog; + } + + @Override + public void onFeatureStart(ModifiableContext context) { + featureHolder.clear(); + super.onFeatureStart(context); + } + + @Override + public void onValue(ModifiableContext context) { + String type = context.type(); + Optional prop = + context.mappings().get(type).getTargetSchema().getProperties().stream() + .filter(p -> p.getFullPathAsString().equals(context.pathTracker().toString())) + .findFirst(); + + if (prop.isEmpty()) { + LOGGER.error("No property found with path: {}", context.pathTracker().toString()); + super.onValue(context); + return; + } + + if (prop.get().getRole().filter(Role.ID::equals).isPresent()) { + featureHolder.put("id", context.value()); + } else if (prop.get().getAudit().isPresent()) { + featureHolder.put(prop.get().getName(), context.value()); + } + super.onValue(context); + } + + @Override + public void onFeatureEnd(ModifiableContext context) { + featureList.add(new LinkedHashMap<>(featureHolder)); + super.onFeatureEnd(context); + } + + @Override + public void onEnd(ModifiableContext context) { + auditLog.setTarget(requestId, Map.of("features", featureList)); + super.onEnd(context); + } +}