diff --git a/xtraplatform-features-geoparquet/build.gradle b/xtraplatform-features-geoparquet/build.gradle index c0cda3377..0c4537830 100644 --- a/xtraplatform-features-geoparquet/build.gradle +++ b/xtraplatform-features-geoparquet/build.gradle @@ -16,5 +16,6 @@ dependencies { provided project(':xtraplatform-features-sql') provided project(':xtraplatform-geometries') + //NOPMD - TODO: very big, only include binary for current platform embedded(libs.duckdb) } \ No newline at end of file diff --git a/xtraplatform-features-geoparquet/src/main/java/de/ii/xtraplatform/features/geoparquet/app/FeatureProviderGeoParquet.java b/xtraplatform-features-geoparquet/src/main/java/de/ii/xtraplatform/features/geoparquet/app/FeatureProviderGeoParquet.java index 937e1ae29..95ec31e95 100644 --- a/xtraplatform-features-geoparquet/src/main/java/de/ii/xtraplatform/features/geoparquet/app/FeatureProviderGeoParquet.java +++ b/xtraplatform-features-geoparquet/src/main/java/de/ii/xtraplatform/features/geoparquet/app/FeatureProviderGeoParquet.java @@ -34,6 +34,7 @@ import de.ii.xtraplatform.features.sql.domain.SqlQueryBatch; import de.ii.xtraplatform.features.sql.domain.SqlQueryOptions; import de.ii.xtraplatform.features.sql.domain.SqlRow; +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; @@ -217,6 +218,7 @@ public FeatureProviderGeoParquet( VolatileRegistry volatileRegistry, Cache cache, Scheduler scheduler, + AuditLog auditLog, @Assisted FeatureProviderDataV2 data) { super( crsTransformerFactory, @@ -231,6 +233,7 @@ public FeatureProviderGeoParquet( volatileRegistry, cache, scheduler, + auditLog, data, Map.of()); } 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 89f189625..d58f2c991 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 6e7ba7402..f8ea31000 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 @@ -104,6 +104,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; @@ -490,6 +491,7 @@ public FeatureProviderSql( VolatileRegistry volatileRegistry, Cache cache, Scheduler scheduler, + AuditLog auditLog, @Assisted FeatureProviderDataV2 data) { this( crsTransformerFactory, @@ -504,6 +506,7 @@ public FeatureProviderSql( volatileRegistry, cache, scheduler, + auditLog, data, decoderFactories.getConnectorDecoders()); } @@ -521,6 +524,7 @@ protected FeatureProviderSql( VolatileRegistry volatileRegistry, Cache cache, Scheduler scheduler, + AuditLog auditLog, FeatureProviderDataV2 data, Map subdecoders) { super( @@ -530,6 +534,7 @@ protected FeatureProviderSql( crsInfo, extensionRegistry, valueStore.forType(Codelist.class), + auditLog, data, volatileRegistry); @@ -965,11 +970,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 @@ -1367,7 +1368,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 = @@ -1492,7 +1493,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 05c4a4547..4c955cbe6 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 @@ -35,6 +35,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; @@ -87,6 +88,8 @@ public abstract class AbstractFeatureProvider< private boolean datasetChangedForced; private String previousDataset; + protected AuditLog auditLog; + protected AbstractFeatureProvider( ConnectorFactory connectorFactory, Reactive reactive, @@ -94,6 +97,7 @@ protected AbstractFeatureProvider( CrsInfo crsInfo, ProviderExtensionRegistry extensionRegistry, Values codelistStore, + AuditLog auditLog, FeatureProviderDataV2 data, VolatileRegistry volatileRegistry) { super(data, volatileRegistry); @@ -103,6 +107,7 @@ protected AbstractFeatureProvider( this.crsInfo = crsInfo; this.extensionRegistry = extensionRegistry; this.codelistStore = codelistStore; + this.auditLog = auditLog; this.volatileRegistry = volatileRegistry; this.changeHandler = new FeatureChangeHandlerImpl(); this.connector = @@ -527,7 +532,8 @@ public FeatureStream getFeatureStream(FeatureQuery query) { nativeCrsIs3d, getCodelists(), this::runQuery, - !query.hitsOnly()); + !query.hitsOnly(), + auditLog); } // TODO: more tests @@ -594,8 +600,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/FeatureProviderDataV2.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java index ec1a1f339..cef9cee09 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java @@ -184,8 +184,8 @@ default List getCql2Functions() { * definitions](#schema-definitions) with `type: OBJECT` and at least one property with `role: * ID`. * @langDe Definition von Feature-Types. Die Einträge sind - * [Schema-Definitionen](#schema-definitions) mit `type: OBJECT` und mindestens einem Property - * mit `role: ID`. + * [Schema-Definitionen](#schema-definitionen) mit `type: OBJECT` und mindestens einem + * Property mit `role: ID`. * @default {} */ @JsonMerge @@ -197,7 +197,7 @@ default List getCql2Functions() { * `types`. The entries are arbitrary [schema definitions](#schema-definitions). * @langDe Definition von wiederverwendbaren Schema-Fragmenten, die mittels `schema` in `types` * referenziert werden können. Die Einträge sind beliebige - * [Schema-Definitionen](#schema-definitions). + * [Schema-Definitionen](#schema-definitionen). * @default {} */ @JsonMerge 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 85bbf55ad..74ca5039f 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 @@ -75,6 +75,20 @@ public interface FeatureSchema String CONCAT_ELEMENT = "_CONCAT_ELEMENT_"; String COALESCE_ELEMENT = "_COALESCE_ELEMENT_"; + /** + * @langEn If set to `true` for properties of type `VALUE`/`VALUE_ARRAY`, these will be included + * in the audit log. If set to `true` for a feature type, all of its properties will be + * included in the audit log except for those explicitly excluded. Geometries are always + * excluded. + * @langDe Wenn für Eigenschaften vom Typ `VALUE`/`VALUE_ARRAY` auf `true` gesetzt, werden diese + * in das Audit-Log aufgenommen. Wenn für einen Feature-Type auf `true` gesetzt, werden alle + * seine Eigenschaften in das Audit-Log aufgenommen, außer diejenigen, die explizit + * ausgeschlossen sind. Geometrien sind immer ausgeschlossen. + * @default false + * @since v4.8 + */ + Optional getAudit(); + @JsonIgnore @Override String getName(); @@ -1244,8 +1258,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 7e507d977..d50a7c1ed 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 @@ -38,6 +38,7 @@ enum PipelineSteps { CLEAN, ETAG, METADATA, + AUDIT, ALL } @@ -153,23 +154,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 13d6d6e36..8d8c99fd2 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; private final boolean hasPropertyLinks; private final boolean deduplicate; private final boolean idsArePerType; @@ -61,7 +65,8 @@ public FeatureStreamImpl( boolean nativeCrsIs3d, Map codelists, QueryRunner runner, - boolean doTransform) { + boolean doTransform, + AuditLog auditLog) { this.query = query; this.data = data; this.crsTransformerFactory = crsTransformerFactory; @@ -69,6 +74,7 @@ public FeatureStreamImpl( this.codelists = codelists; this.runner = runner; this.doTransform = doTransform; + this.auditLog = auditLog; this.stepMappingSchema = !query.skipPipelineSteps().contains(PipelineSteps.MAPPING_SCHEMA) @@ -90,6 +96,10 @@ public FeatureStreamImpl( this.stepMetadata = !query.skipPipelineSteps().contains(PipelineSteps.METADATA) && !query.skipPipelineSteps().contains(PipelineSteps.ALL); + this.stepAudit = + auditLog.isEnabled() + && !query.skipPipelineSteps().contains(PipelineSteps.AUDIT) + && !query.skipPipelineSteps().contains(PipelineSteps.ALL); this.hasPropertyLinks = hasPropertyLinks(query, data); this.deduplicate = query instanceof MultiFeatureQuery && ((MultiFeatureQuery) query).getDeduplicate(); @@ -121,7 +131,8 @@ private static List getTypes(Query query) { public CompletionStage runWith( Sink sink, Map propertyTransformations, - CompletableFuture onCollectionMetadata) { + CompletableFuture onCollectionMetadata, + Optional requestId) { Map mergedTransformations = getMergedTransformations(data.getTypes(), query, propertyTransformations); @@ -175,6 +186,18 @@ public CompletionStage runWith( source = source.via(new FeatureTokenTransformerMetadata(resultBuilder)); } + FeatureTokenTransformerAudit auditTransformer = null; + if (stepAudit) { + if (requestId.isEmpty()) { + LOGGER.error("Audit logging not possible, no request-id provided!"); + } else if (auditLog.logIsAvailable(requestId.get())) { + auditTransformer = new FeatureTokenTransformerAudit(requestId.get(), auditLog); + source = source.via(auditTransformer); + } + } + final Runnable finishAuditLog = + auditTransformer != null ? auditTransformer::appendToLog : () -> {}; + source = source.via(new FeatureTokenTransformerHooks(resultBuilder, onCollectionMetadata)); @@ -191,13 +214,16 @@ 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) -> { + finishAuditLog.run(); + if (strongETag) { builder1.eTag(eTag.build(ETag.Type.STRONG)); } + return builder1.build(); }); }; @@ -209,7 +235,8 @@ public CompletionStage runWith( public CompletionStage> runWith( SinkReduced sink, Map propertyTransformations, - CompletableFuture onCollectionMetadata) { + CompletableFuture onCollectionMetadata, + Optional requestId) { Map mergedTransformations = getMergedTransformations(data.getTypes(), query, propertyTransformations); @@ -262,6 +289,18 @@ public CompletionStage> runWith( source = source.via(new FeatureTokenTransformerMetadata(resultBuilder)); } + FeatureTokenTransformerAudit auditTransformer = null; + if (stepAudit) { + if (requestId.isEmpty()) { + LOGGER.error("Audit logging not possible, no request-id provided!"); + } else if (auditLog.logIsAvailable(requestId.get())) { + auditTransformer = new FeatureTokenTransformerAudit(requestId.get(), auditLog); + source = source.via(auditTransformer); + } + } + final Runnable finishAuditLog = + auditTransformer != null ? auditTransformer::appendToLog : () -> {}; + source = source.via(new FeatureTokenTransformerHooks(resultBuilder, onCollectionMetadata)); @@ -284,6 +323,8 @@ public CompletionStage> runWith( }) .handleEnd( (ImmutableResultReduced.Builder xBuilder) -> { + finishAuditLog.run(); + if (strongETag) { xBuilder.eTag(eTag.build(ETag.Type.STRONG)); } 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..9ef1394bc --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerAudit.java @@ -0,0 +1,106 @@ +/* + * 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.services.domain.AuditLog; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class FeatureTokenTransformerAudit extends FeatureTokenTransformer { + + private final String requestId; + private final AuditLog auditLog; + private final Map featureHolder; + private final List> featureList; + private final boolean includePropertyValues; + private final List propertyNames; + private boolean logAllProperties; + + public FeatureTokenTransformerAudit(String requestId, AuditLog auditLog) { + this.requestId = requestId; + this.auditLog = auditLog; + this.includePropertyValues = auditLog.getIncludePropertyValues(requestId); + this.propertyNames = new ArrayList<>(); + this.featureHolder = new LinkedHashMap<>(); + this.featureList = new ArrayList<>(); + this.logAllProperties = false; + } + + @Override + public void onFeatureStart(ModifiableContext context) { + featureHolder.clear(); + propertyNames.clear(); + + this.logAllProperties = context.schema().flatMap(FeatureSchema::getAudit).orElse(false); + + super.onFeatureStart(context); + } + + @Override + public void onValue(ModifiableContext context) { + Optional prop = context.schema(); + + if (prop.isEmpty()) { + super.onValue(context); + return; + } + + FeatureSchema schema = prop.get(); + if (schema.isId()) { + featureHolder.put("id", context.value()); + super.onValue(context); + return; + } + + boolean doAudit = + (logAllProperties && schema.getAudit().orElse(true)) || schema.getAudit().orElse(false); + if (doAudit) { + String schemaName = schema.getFullPathAsString(); + String value = context.value(); + + addProperty(schemaName, value, context.inArray()); + } + + super.onValue(context); + } + + private void addProperty(String schemaName, String value, boolean isMultiValue) { + if (!includePropertyValues) { + propertyNames.add(schemaName); + return; + } + + if (!isMultiValue) { + featureHolder.put(schemaName, value); + return; + } + + List values = + (List) featureHolder.computeIfAbsent(schemaName, k -> new ArrayList()); + + values.add(value); + } + + @Override + public void onFeatureEnd(ModifiableContext context) { + if (!includePropertyValues) { + featureHolder.put("properties", new ArrayList<>(propertyNames)); + } + featureList.add(new LinkedHashMap<>(featureHolder)); + logAllProperties = false; + + super.onFeatureEnd(context); + } + + public void appendToLog() { + auditLog.setTarget(requestId, Map.of("features", featureList)); + } +}