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 f8ea31000..2d1829a5b 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 @@ -10,7 +10,6 @@ import static de.ii.xtraplatform.cql.domain.In.ID_PLACEHOLDER; import com.fasterxml.jackson.core.JsonParseException; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import dagger.assisted.Assisted; import dagger.assisted.AssistedInject; @@ -56,7 +55,6 @@ import de.ii.xtraplatform.features.domain.FeatureStreamImpl; import de.ii.xtraplatform.features.domain.FeatureTokenDecoder; import de.ii.xtraplatform.features.domain.FeatureTokenSource; -import de.ii.xtraplatform.features.domain.FeatureTokenTransformer; import de.ii.xtraplatform.features.domain.FeatureTransactions; import de.ii.xtraplatform.features.domain.FeatureTransactions.MutationResult.Builder; import de.ii.xtraplatform.features.domain.FeatureTransactions.MutationResult.Type; @@ -941,11 +939,6 @@ protected WkbDialect getWkbDialect() { return WkbDialect.SQL_MM; } - @Override - protected List getDecoderTransformers() { - return ImmutableList.of(); // new FeatureTokenTransformerSorting()); - } - @Override public FeatureProviderSqlData getData() { return (FeatureProviderSqlData) super.getData(); diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappings.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappings.java index 511c857eb..80477d102 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappings.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappings.java @@ -164,7 +164,6 @@ public void onFeatureStart(ModifiableContext conte downstream.onFeatureStart(newContext); downstream.bufferStart(); - downstream.next(0); this.currentValueTransformerChain = valueTransformerChains.get(context.type()); } @@ -203,95 +202,72 @@ private void applyTokenSliceTransformers(String type) { @Override public void onObjectStart(ModifiableContext context) { - int pos = context.pos(); - if (pos > -1) { - downstream.next(pos, context.parentPos()); - - if (context.schema().filter(schema -> schema.isObject() || schema.isSpatial()).isPresent()) { - FeatureSchema schema = context.schema().get(); - - downstream.onObjectStart(schema.getFullPath()); - } + // emit for object/spatial properties but not the feature root itself: a path-less marker + // resolves to the root schema (an object), which the type check alone would not exclude + if (context + .schema() + .filter(schema -> (schema.isObject() || schema.isSpatial()) && !schema.isFeature()) + .isPresent()) { + FeatureSchema schema = context.schema().get(); + + downstream.onObjectStart(schema.getFullPath()); } } @Override public void onObjectEnd(ModifiableContext context) { - int pos = context.pos(); - if (pos > -1) { - downstream.next(pos, context.parentPos()); - - if (context.schema().filter(schema -> schema.isObject() || schema.isSpatial()).isPresent()) { - FeatureSchema schema = context.schema().get(); - downstream.onObjectEnd(schema.getFullPath()); - } + // not the feature root itself (see onObjectStart) + if (context + .schema() + .filter(schema -> (schema.isObject() || schema.isSpatial()) && !schema.isFeature()) + .isPresent()) { + FeatureSchema schema = context.schema().get(); + downstream.onObjectEnd(schema.getFullPath()); } } @Override public void onArrayStart(ModifiableContext context) { - int pos = context.pos(); - if (pos > -1) { - downstream.next(pos, context.parentPos()); - - if (context.schema().filter(schema -> schema.isArray() || schema.isSpatial()).isPresent()) { - FeatureSchema schema = context.schema().get(); + if (context.schema().filter(schema -> schema.isArray() || schema.isSpatial()).isPresent()) { + FeatureSchema schema = context.schema().get(); - downstream.onArrayStart(schema.getFullPath()); - } + downstream.onArrayStart(schema.getFullPath()); } } @Override public void onArrayEnd(ModifiableContext context) { - int pos = context.pos(); - if (pos > -1) { - downstream.next(pos, context.parentPos()); - - if (context.schema().filter(schema -> schema.isArray() || schema.isSpatial()).isPresent()) { - FeatureSchema schema = context.schema().get(); - downstream.onArrayEnd(schema.getFullPath()); - } + if (context.schema().filter(schema -> schema.isArray() || schema.isSpatial()).isPresent()) { + FeatureSchema schema = context.schema().get(); + downstream.onArrayEnd(schema.getFullPath()); } } @Override public void onGeometry(ModifiableContext context) { - int pos = context.pos(); - if (pos > -1) { - downstream.next(pos, context.parentPos()); - - if (context.schema().filter(FeatureSchema::isSpatial).isPresent()) { - FeatureSchema schema = context.schema().get(); - Geometry geometry = context.geometry(); - downstream.onGeometry(schema.getFullPath(), geometry); - } + if (context.schema().filter(FeatureSchema::isSpatial).isPresent()) { + FeatureSchema schema = context.schema().get(); + Geometry geometry = context.geometry(); + downstream.onGeometry(schema.getFullPath(), geometry); } } @Override public void onValue(ModifiableContext context) { - int pos = context.pos(); - if (pos > -1) { - downstream.next(pos, context.parentPos()); + if (context.schema().filter(FeatureSchema::isValue).isPresent()) { + FeatureSchema schema = context.schema().get(); - if (context.schema().filter(FeatureSchema::isValue).isPresent()) { - FeatureSchema schema = context.schema().get(); + Type valueType = + schema.isSpatial() ? context.valueType() : schema.getValueType().orElse(schema.getType()); - Type valueType = - schema.isSpatial() - ? context.valueType() - : schema.getValueType().orElse(schema.getType()); + String path = schema.getFullPathAsString(); + String value = context.value(); - String path = schema.getFullPathAsString(); - String value = context.value(); - - if (Objects.nonNull(value)) { - value = currentValueTransformerChain.transform(path, value); - } - - downstream.onValue(schema.getFullPath(), value, valueType); + if (Objects.nonNull(value)) { + value = currentValueTransformerChain.transform(path, value); } + + downstream.onValue(schema.getFullPath(), value, valueType); } } } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerSorting.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerSorting.java deleted file mode 100644 index 7e6438027..000000000 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTokenTransformerSorting.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2022 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.Type; -import de.ii.xtraplatform.geometries.domain.Geometry; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Queue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/* -NOTE: while this works, it is cumbersome and hard to maintain - a much cleaner solution would use FeatureTokenEmitter and FeatureTokenReader for buffering - but these are out of sync with Context and have to be adjusted (geoDim, in(Geo|Array|Object)) -*/ -public class FeatureTokenTransformerSorting extends FeatureTokenTransformer { - - private static final Logger LOGGER = - LoggerFactory.getLogger(FeatureTokenTransformerSorting.class); - - private final Map, Integer>> pathIndex; - private final Map, Integer>> rearrange; - private final Queue indexQueue; - private final Queue> pathQueue; - private final Queue schemaIndexQueue; - private final Queue> indexesQueue; - private final Queue valueQueue; - private final Queue> geoQueue; - private final Queue inArrayQueue; - private final Queue inObjectQueue; - private final Queue tokenQueue; - private final Queue valueTypeQueue; - /*private FeatureTokenBuffer< - FeatureSchema, SchemaMapping, ModifiableContext> - downstream;*/ - - private String currentType; - private int bufferIndex; - private List bufferParent; - - public FeatureTokenTransformerSorting() { - this.pathIndex = new LinkedHashMap<>(); - this.rearrange = new LinkedHashMap<>(); - this.indexQueue = new LinkedList<>(); - this.pathQueue = new LinkedList<>(); - this.schemaIndexQueue = new LinkedList<>(); - this.indexesQueue = new LinkedList<>(); - this.valueQueue = new LinkedList<>(); - this.geoQueue = new LinkedList<>(); - this.inArrayQueue = new LinkedList<>(); - this.inObjectQueue = new LinkedList<>(); - this.tokenQueue = new LinkedList<>(); - this.valueTypeQueue = new LinkedList<>(); - this.currentType = null; - this.bufferIndex = 0; - this.bufferParent = new ArrayList<>(); - } - - @Override - protected void init() { - // this.downstream = new FeatureTokenBuffer<>(getDownstream(), getContext()); - - super.init(); - } - - @Override - public void onStart(ModifiableContext context) { - analyzeMapping(context.mapping()); - - super.onStart(context); - } - - @Override - public void onFeatureStart(ModifiableContext context) { - analyzeMapping(context.mapping()); - - super.onFeatureStart(context); - } - - @Override - public void onFeatureEnd(ModifiableContext context) { - if (bufferIndex > 0) { - emptyBuffer(context, Integer.MAX_VALUE); - // downstream.bufferStop(true); - } - - super.onFeatureEnd(context); - } - - @Override - public void onObjectStart(ModifiableContext context) { - int index = getIndex(context.path()); - - checkBuffer(context, FeatureTokenType.OBJECT, index, index > bufferIndex); - } - - @Override - public void onObjectEnd(ModifiableContext context) { - boolean isBufferParent = startsWith(bufferParent, context.path()); - int triggerIndex = 0; - if (isBufferParent) { - triggerIndex = findLastStartsWith(context.path()) + 1; - } - - checkBuffer(context, FeatureTokenType.OBJECT_END, triggerIndex, isBufferParent); - } - - @Override - public void onArrayStart(ModifiableContext context) { - int index = getIndex(context.path()); - - checkBuffer(context, FeatureTokenType.ARRAY, index, index > bufferIndex); - } - - @Override - public void onArrayEnd(ModifiableContext context) { - int index = getIndex(context.path()); - - checkBuffer(context, FeatureTokenType.ARRAY_END, index, false); - } - - @Override - public void onGeometry(ModifiableContext context) { - int index = getIndex(context.path()); - - checkBuffer(context, FeatureTokenType.GEOMETRY, index, index > bufferIndex); - } - - @Override - public void onValue(ModifiableContext context) { - int index = getIndex(context.path()); - - checkBuffer(context, FeatureTokenType.VALUE, index, index > bufferIndex); - } - - private int getIndex(List path) { - if (Objects.nonNull(currentType)) { - return Objects.requireNonNullElse(pathIndex.get(currentType).get(path), -1); - } - return -1; - } - - // TODO: identify out of order elements, on element after ooe start buffer and mark 0, on ooe - // insert 0 and flush - // TODO: multiple ooes following each other, multiple marks - // TODO: test with xleit, create unit tests - private void analyzeMapping(SchemaMapping mapping) { - if (Objects.isNull(mapping)) { - return; - } - - this.currentType = mapping.getTargetSchema().getName(); - - if (pathIndex.containsKey(currentType) || rearrange.containsKey(currentType)) { - return; - } - - pathIndex.put(currentType, new LinkedHashMap<>()); - rearrange.put(currentType, new LinkedHashMap<>()); - - int index = 0; - List lastParent = null; - boolean doRearrange = false; - - for (List path : mapping.getSchemasByTargetPath().keySet()) { - if (path.size() > 1) { - if (path.stream().anyMatch(elem -> elem.matches("\\[[^=\\]]+].+"))) { - continue; - } - boolean isNested = path.size() > 2 || path.get(path.size() - 1).startsWith("["); - List parent = path.subList(0, path.size() - 1); - if (lastParent == null) { - lastParent = parent; - } - - if (!doRearrange && !Objects.equals(parent, lastParent) && !isNested) { - doRearrange = true; - } else if (doRearrange && (!Objects.equals(parent, lastParent) || isNested)) { - doRearrange = false; - } - - lastParent = parent; - - if (doRearrange) { - this.rearrange.get(currentType).put(path, index); - } - } - - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("{}: {}{}", index, doRearrange ? "QUEUE " : "", path); - } - - this.pathIndex.get(currentType).put(path, index); - index++; - } - } - - private void checkBuffer( - ModifiableContext context, - FeatureTokenType token, - int triggerIndex, - boolean doEmptyBuffer) { - int index = getIndex(context.path()); - boolean doRearrange = - rearrange.containsKey(currentType) - && rearrange.get(currentType).containsKey(context.path()); - - if (doRearrange) { - buffer(context, index, token); - /*if (!downstream.isBuffering()) { - if (bufferIndex <= 0) { - this.bufferIndex = index; - this.bufferParent = context.path().subList(0, context.path().size() - 1); - } - downstream.bufferStart(); - downstream.bufferMark(); - push(context, token); - }*/ - } else { - if (bufferIndex > 0 && doEmptyBuffer) { - emptyBuffer(context, triggerIndex); - // downstream.bufferFlush(); - } - // downstream.bufferStop(false); - push(context, token); - } - } - - private static boolean startsWith(List a, List b) { - return Objects.equals(a, b) - || (a.size() > b.size() && Objects.equals(a.subList(0, b.size()), b)); - } - - private int findLastStartsWith(List parent) { - if (!pathIndex.containsKey(currentType)) { - return -1; - } - return pathIndex.get(currentType).entrySet().stream() - .filter(entry -> startsWith(entry.getKey(), parent)) - .mapToInt(Entry::getValue) - .max() - .orElse(-1); - } - - private void buffer( - ModifiableContext context, int index, FeatureTokenType token) { - if (bufferIndex <= 0) { - this.bufferIndex = index; - this.bufferParent = context.path().subList(0, context.path().size() - 1); - } - - indexQueue.add(index); - tokenQueue.add(token); - pathQueue.add(context.path()); - schemaIndexQueue.add(context.schemaIndex()); - indexesQueue.add(new ArrayList<>(context.indexes())); - valueQueue.add(context.value()); - geoQueue.add(context.geometry()); - inArrayQueue.add(context.inArray()); - inObjectQueue.add(context.inObject()); - valueTypeQueue.add(context.valueType()); - } - - private void emptyBuffer( - ModifiableContext context, int triggerIndex) { - - List path = context.path(); - int schemaIndex = context.schemaIndex(); - ArrayList indexes = new ArrayList<>(context.indexes()); - String value = context.value(); - Geometry geometry = context.geometry(); - boolean inArray = context.inArray(); - boolean inObject = context.inObject(); - - while (!indexQueue.isEmpty() && indexQueue.peek() < triggerIndex) { - int index = indexQueue.remove(); - FeatureTokenType token = tokenQueue.remove(); - - context.pathTracker().track(pathQueue.remove()); - context.setSchemaIndex(schemaIndexQueue.remove()); - context.setIndexes(indexesQueue.remove()); - context.setValue(valueQueue.remove()); - context.setValueType(valueTypeQueue.remove()); - context.setGeometry(geoQueue.remove()); - context.setInArray(inArrayQueue.remove()); - context.setInObject(inObjectQueue.remove()); - - push(context, token); - } - - context.pathTracker().track(path); - context.setSchemaIndex(schemaIndex); - context.setIndexes(indexes); - context.setValue(value); - context.setGeometry(geometry); - context.setInArray(inArray); - context.setInObject(inObject); - - this.bufferIndex = Objects.requireNonNullElse(indexQueue.peek(), 0); - this.bufferParent = - Objects.nonNull(pathQueue.peek()) && pathQueue.peek().size() > 1 - ? pathQueue.peek().subList(0, pathQueue.peek().size() - 1) - : new ArrayList<>(); - } - - private void push( - ModifiableContext context, FeatureTokenType token) { - switch (token) { - case VALUE: - // downstream.onValue(context); - super.onValue(context); - break; - case GEOMETRY: - // downstream.onGeometry(context); - super.onGeometry(context); - break; - case OBJECT: - // downstream.onObjectStart(context); - super.onObjectStart(context); - break; - case OBJECT_END: - // downstream.onObjectEnd(context); - super.onObjectEnd(context); - break; - case ARRAY: - // downstream.onArrayStart(context); - super.onArrayStart(context); - break; - case ARRAY_END: - // downstream.onArrayEnd(context); - super.onArrayEnd(context); - break; - } - } -} diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/FeatureEventBuffer.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/FeatureEventBuffer.java index 7c0273113..f28e0bcd7 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/FeatureEventBuffer.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/FeatureEventBuffer.java @@ -16,9 +16,12 @@ import de.ii.xtraplatform.features.domain.SchemaMapping; import de.ii.xtraplatform.features.domain.SchemaMappingBase; import de.ii.xtraplatform.geometries.domain.GeometryType; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -26,6 +29,28 @@ import java.util.Vector; import java.util.stream.Collectors; +/** + * Per-feature token buffer with two distinct responsibilities: + * + *
    + *
  1. A position-addressable slice index for the in-buffer token-slice transformers + * (flatten, concat, codelist, …): {@link #getSlice(int)}/{@link #replaceSlice(int, List)} + * read and rewrite a property - or an enclosing object together with its descendants - by its + * schema position. The index ({@link #start(int)}/{@link #length(int)}) is derived on demand + * from the buffered tokens by {@link #computeIndex()}; the transformers rewrite within a + * property and do not reorder across properties. + *
  2. Emission order, applied by the single {@link #orderedBySchema(List)} pass. + *
+ * + *

{@link #append(Object)} stores tokens in the order the provider produces them, which is the + * provider's per-table order, not the schema order: a property backed by a joined table (object, + * object array, value array, feature reference) is produced after the main table's columns even + * when it is declared before them, and a property produced from several tables arrives as several + * fragments. {@link #ensureOrdered()} applies the schema-order pass in place, lazily and exactly + * once per feature: before the slice transformers read the buffer - so each property's fragments + * are contiguous and a transformer's slice cannot swallow an unrelated property emitted between + * them - and at the latest at flush when no transformer ran. + */ public class FeatureEventBuffer< U extends SchemaBase, V extends SchemaMappingBase, W extends ModifiableContext> implements FeatureTokenEmitter2 { @@ -39,8 +64,8 @@ public class FeatureEventBuffer< private final Vector> enclosings; private final Map mappings; private boolean doBuffer; - public int current; - public List currentEnclosing; + private boolean indexStale; + private boolean schemaOrdered; private String lastType; public FeatureEventBuffer( @@ -53,8 +78,8 @@ public FeatureEventBuffer( this.mappings = mappings; this.doBuffer = false; - this.current = 0; - this.currentEnclosing = List.of(); + this.indexStale = true; + this.schemaOrdered = false; int maxEvents = mappings.values().stream() @@ -71,15 +96,6 @@ public FeatureTokenEmitter2 getBuffer() { return bufferIn; } - public void next(int pos) { - next(pos, List.of()); - } - - public void next(int pos, List enclosing) { - this.current = pos; - this.currentEnclosing = enclosing; - } - /** * An event consists of 1 to n tokens and is saved in the buffer. An event has a desired position * that must not match the order of occurrence. events contains the buffer start index and token @@ -109,36 +125,16 @@ private int end(int pos) { } /** - * Increase length for given event position in buffer. - * - * @param pos event position + * Updates the index after a slice changed size: grows the length of {@code pos} by {@code delta} + * and shifts the start of every later position by the same amount. The buffer is in + * schema-position order when this runs (see {@link #ensureOrdered()}), so a resized slice moves + * every position after it by {@code delta} - this keeps the index correct without a full {@link + * #computeIndex()} rebuild per slice rewrite. */ - private void increase(int pos) { - plus(pos, 1); - } - - private void increase(int pos, List enclosing) { - plus(pos, 1); - - for (int pos2 : enclosing) { - plus(pos2, 1, false); - } - } - private void plus(int pos, int delta) { - plus(pos, delta, true); - } - - private void plus(int pos, int delta, boolean propagate) { - // increase length of pos - int lenPos = (pos * 2) + 1; - events[lenPos] += delta; - - // increase start of following pos - if (propagate) { - for (int i = (pos + 1) * 2; i < events.length; i += 2) { - events[i] += delta; - } + events[(pos * 2) + 1] += delta; + for (int i = (pos + 1) * 2; i < events.length; i += 2) { + events[i] += delta; } } @@ -157,17 +153,129 @@ private int minPos(int pos, List enclosing) { return enclosing.get(enclosing.size() - 1); } + /** + * Buffers a token in the order the provider produces it. No ordering is attempted here; {@link + * #orderedBySchema(List)} applies schema order later, on demand. The buffer is marked unordered + * and its slice index stale, both rebuilt lazily the next time a slice is read, written, or + * flushed. + */ void append(Object token) { - int end = end(current); - buffer.add(end, token); + buffer.add(token); + schemaOrdered = false; + indexStale = true; + } + + /** + * Re-sorts the buffer into schema order in place, once. This must run before the in-buffer slice + * transformers read or rewrite a slice: a property produced by the provider as several per-table + * fragments (e.g. a concatenated object array) is contiguous only after sorting, and {@link + * #getSlice(int)} hands a transformer a contiguous buffer range - without this, an unrelated + * property emitted between two fragments would be swallowed into the slice. It is also the single + * place schema order is applied for the final emission. The slice transformers preserve schema + * order (they rewrite within a property), so it is not repeated after they run. + */ + private void ensureOrdered() { + if (schemaOrdered) { + return; + } + List ordered = orderedBySchema(buffer); + buffer.clear(); + buffer.addAll(ordered); + schemaOrdered = true; + indexStale = true; + } + + /** + * Rebuilds the position-addressable slice index from the buffered tokens in a single pass. Each + * token group is accrued to its outermost enclosing position ({@link #minPos(int, List)} of the + * token's position and its ancestors, or the token's own position when it is top-level) - the + * property whose slice encloses it. An enclosing object's range therefore spans all of its + * descendants even when the object's own marker is not emitted (e.g. a flattened source object), + * which is exactly the slice {@link #getSlice(int)} hands to a transformer. Runs only when a + * slice is actually accessed, so features without slice transformers never pay for it. + */ + private void computeIndex() { + Arrays.fill(events, 0); + indexStale = false; + + SchemaMapping mapping = Objects.isNull(lastType) ? null : mappings.get(lastType); + if (Objects.isNull(mapping) || buffer.isEmpty()) { + return; + } - int minPos = minPos(current, currentEnclosing); + int i = 0; + while (i < buffer.size()) { + int j = i + 1; + while (j < buffer.size() && !(buffer.get(j) instanceof FeatureTokenType)) { + j++; + } + List path = + i + 1 < buffer.size() && buffer.get(i + 1) instanceof List + ? (List) buffer.get(i + 1) + : List.of(); + int pos = positionForPath(mapping, path); + if (pos >= 0) { + setSpan(minPos(pos, enclosings.get(pos)), i, j); + } + + i = j; + } - increase(minPos); + // Positions without tokens (an absent property, or a nested position whose tokens were accrued + // to its enclosing) keep length 0, but still need a valid buffer offset as their start: + // getSlice + // hands out buffer.subList(start, end) and replaceSlice inserts at start, and the incremental + // plus() shift after a slice shrinks moves every later position - including these - by the same + // delta. Left at 0, an empty position after a shrunk slice is driven negative and subList + // throws. + // The buffer is in schema-position order here, so a forward scan yields monotonic, non-negative + // starts; only top-level (enclosing) positions carry a non-zero length, so nextStart advances + // exactly across the buffer. + int nextStart = 0; + for (int pos = 0; pos < events.length / 2; pos++) { + if (length(pos) == 0) { + events[pos * 2] = nextStart; + } else { + nextStart = end(pos); + } + } + } + + /** + * Records the buffer range [start, end) for a schema position. A position can occur more than + * once at the same level - object-array elements share the array's position, and several + * consecutive arrays at the same path are merged by the concat transformer - so repeated spans + * are unioned into the enclosing range rather than overwritten. The occurrences are contiguous in + * production order, so the union is the full span the transformer expects. + */ + private void setSpan(int pos, int start, int end) { + if (pos < 0) { + return; + } + int length = length(pos); + if (length == 0) { + events[pos * 2] = start; + events[(pos * 2) + 1] = end - start; + } else { + int unionStart = Math.min(start(pos), start); + int unionEnd = Math.max(end(pos), end); + events[pos * 2] = unionStart; + events[(pos * 2) + 1] = unionEnd - unionStart; + } + } + + private int positionForPath(SchemaMapping mapping, List path) { + if (path.isEmpty()) { + return -1; + } + List positions = mapping.getPositionsForTargetPath(path); + return positions.isEmpty() ? -1 : positions.get(0); } void reset(String type) { Arrays.fill(events, 0); + this.indexStale = true; + this.schemaOrdered = false; if (!Objects.equals(lastType, type)) { Collections.fill(enclosings, List.of()); @@ -198,11 +306,152 @@ public void bufferStop(boolean flush) { } public void bufferFlush() { - buffer.add(FeatureTokenType.FLUSH); + ensureOrdered(); buffer.forEach(bufferOut::onToken); + bufferOut.onToken(FeatureTokenType.FLUSH); buffer.clear(); } + /** + * Re-sorts the buffered feature so that properties are emitted in the order declared in the + * schema, regardless of the order in which the provider produced them. The buffer holds tokens in + * the provider's per-table order, not the schema order: a property backed by a joined table + * (object, object array, value array, feature reference) is produced after the columns of the + * main table even when it is declared before them. The pass rebuilds the token stream as a tree + * and serialises each object's children in schema-position order; array elements keep their + * (data) order, and the children inside each element are ordered by schema position like any + * other object. {@link #ensureOrdered()} applies it in place, before the slice transformers read + * the buffer (so each property's fragments are contiguous) and at the latest by flush. + */ + private List orderedBySchema(List tokens) { + SchemaMapping mapping = Objects.isNull(lastType) ? null : mappings.get(lastType); + + if (Objects.isNull(mapping) || tokens.isEmpty()) { + return new ArrayList<>(tokens); + } + + Node root = new Node(null, List.of()); + buildTree(tokens, root); + orderChildren(root, mapping); + + List ordered = new ArrayList<>(tokens.size()); + for (Node child : root.children) { + child.flattenInto(ordered); + } + return ordered; + } + + private static void buildTree(List tokens, Node root) { + Deque stack = new ArrayDeque<>(); + stack.push(root); + + int i = 0; + while (i < tokens.size()) { + // a token group is a marker (FeatureTokenType) followed by its context tokens up to the next + // marker + int j = i + 1; + while (j < tokens.size() && !(tokens.get(j) instanceof FeatureTokenType)) { + j++; + } + List group = new ArrayList<>(tokens.subList(i, j)); + FeatureTokenType type = + tokens.get(i) instanceof FeatureTokenType ? (FeatureTokenType) tokens.get(i) : null; + + if (type == FeatureTokenType.OBJECT || type == FeatureTokenType.ARRAY) { + Node node = new Node(type, group); + stack.peek().children.add(node); + stack.push(node); + } else if (type == FeatureTokenType.OBJECT_END || type == FeatureTokenType.ARRAY_END) { + if (stack.size() > 1) { + stack.pop().close = group; + } + } else { + stack.peek().children.add(new Node(type, group)); + } + + i = j; + } + } + + private static void orderChildren(Node node, SchemaMapping mapping) { + // array elements keep their data order; the children of any object (including each array + // element) are ordered by their schema position + if (node.type != FeatureTokenType.ARRAY && node.children.size() > 1) { + node.children.sort(Comparator.comparingInt(child -> positionOf(child, mapping))); + coalesceSplitObjects(node, mapping); + } + for (Node child : node.children) { + orderChildren(child, mapping); + } + } + + /** + * Coalesces adjacent OBJECT children that share a schema position into one object. The provider + * produces a single-valued object backed by more than one table as several per-table fragments - + * separate {@code OBJECT[path]…OBJECT_END[path]} blocks at the same position - which must become + * one object. Object-array elements are never affected: they are children of an ARRAY node, which + * is excluded from sorting and coalescing by the caller. Runs after the sort, so the fragments + * are already adjacent; the merged children are ordered by the recursive {@link #orderChildren} + * pass. + */ + private static void coalesceSplitObjects(Node node, SchemaMapping mapping) { + List coalesced = new ArrayList<>(node.children.size()); + for (Node child : node.children) { + Node previous = coalesced.isEmpty() ? null : coalesced.get(coalesced.size() - 1); + if (Objects.nonNull(previous) + && previous.type == FeatureTokenType.OBJECT + && child.type == FeatureTokenType.OBJECT + && positionOf(child, mapping) != Integer.MAX_VALUE + && positionOf(previous, mapping) == positionOf(child, mapping)) { + previous.children.addAll(child.children); + } else { + coalesced.add(child); + } + } + node.children.clear(); + node.children.addAll(coalesced); + } + + private static int positionOf(Node node, SchemaMapping mapping) { + List path = node.path(); + if (path.isEmpty()) { + return Integer.MAX_VALUE; + } + List positions = mapping.getPositionsForTargetPath(path); + int position = positions.isEmpty() ? -1 : positions.get(0); + // keep paths without a known position at the end, in their original (stable) order + return position < 0 ? Integer.MAX_VALUE : position; + } + + private static final class Node { + private final FeatureTokenType type; + private final List open; + private final List children = new ArrayList<>(); + private List close; + + private Node(FeatureTokenType type, List open) { + this.type = type; + this.open = open; + } + + @SuppressWarnings("unchecked") + private List path() { + return open.size() > 1 && open.get(1) instanceof List + ? (List) open.get(1) + : List.of(); + } + + private void flattenInto(List out) { + out.addAll(open); + for (Node child : children) { + child.flattenInto(out); + } + if (Objects.nonNull(close)) { + out.addAll(close); + } + } + } + public boolean isBuffering() { return doBuffer; } @@ -211,23 +460,21 @@ public List getSlice(int pos) { if (pos < 0) { return List.of(); } + + ensureOrdered(); + if (pos == 0) { return Collections.unmodifiableList(buffer); } + if (indexStale) { + computeIndex(); + } + int enclosing = minPos(pos, enclosings.get(pos)); List slice = buffer.subList(start(enclosing), end(enclosing)); - /*if (slice.isEmpty() && !enclosings.get(pos).isEmpty()) { - for (int pos2: enclosings.get(pos)) { - slice = buffer.subList(start(pos2), end(pos2)); - if (!slice.isEmpty()) { - break; - } - } - }*/ - return Collections.unmodifiableList(slice); } @@ -236,19 +483,35 @@ public boolean replaceSlice(int pos, List replacement) { return false; } - int enclosing = minPos(pos, enclosings.get(pos)); + ensureOrdered(); - List slice = pos == 0 ? buffer : buffer.subList(start(enclosing), end(enclosing)); + if (pos == 0) { + if (Objects.equals(buffer, replacement)) { + return false; + } + buffer.clear(); + buffer.addAll(replacement); + // the whole buffer was replaced; rebuild the index before the next slice access + indexStale = true; + return true; + } + + if (indexStale) { + computeIndex(); + } + + int enclosing = minPos(pos, enclosings.get(pos)); + List slice = buffer.subList(start(enclosing), end(enclosing)); if (Objects.equals(slice, replacement)) { return false; } int delta = replacement.size() - slice.size(); - slice.clear(); slice.addAll(replacement); + // keep the index correct in place instead of rebuilding it for every slice rewrite if (delta != 0) { plus(enclosing, delta); } diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureEventBufferOrderSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureEventBufferOrderSpec.groovy new file mode 100644 index 000000000..4515248fe --- /dev/null +++ b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureEventBufferOrderSpec.groovy @@ -0,0 +1,150 @@ +/* + * 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.Type +import spock.lang.Specification + +// Regression for the FeatureEventBuffer schema-order reordering: a property backed by a joined +// table is produced by the provider after the main-table columns even when it is declared before +// them. The buffer stores tokens in provider order and the flush pass re-sorts them into schema +// order — otherwise a property declared before its scalar siblings ends up after them, producing +// XML/GML that does not match the application schema's element order. +class FeatureEventBufferOrderSpec extends Specification { + + static List run(FeatureSchema schema, List source) { + List actual = [] + FeatureTokenReader reader = Util.createReader(schema, actual) + source.forEach(token -> reader.onToken(token)) + return actual + } + + // id, child (OBJECT on a joined table), a, b (scalar columns on the main table) + static FeatureSchema TOPLEVEL = new ImmutableFeatureSchema.Builder() + .name("t").sourcePath("/t").type(Type.OBJECT) + .putProperties2("id", new ImmutableFeatureSchema.Builder() + .sourcePath("objid").type(Type.STRING).role(SchemaBase.Role.ID)) + .putProperties2("child", new ImmutableFeatureSchema.Builder() + .sourcePath("[id=rid]t__child").type(Type.OBJECT) + .putProperties2("value", new ImmutableFeatureSchema.Builder() + .sourcePath("value").type(Type.STRING))) + .putProperties2("a", new ImmutableFeatureSchema.Builder().sourcePath("a").type(Type.STRING)) + .putProperties2("b", new ImmutableFeatureSchema.Builder().sourcePath("b").type(Type.STRING)) + .build() + + // provider order: the main-table scalars (a, b) arrive before the joined object (child) + static List TOPLEVEL_SOURCE = [ + FeatureTokenType.INPUT, true, FeatureTokenType.FEATURE, + FeatureTokenType.VALUE, ["t", "objid"], "f1", Type.STRING, + FeatureTokenType.VALUE, ["t", "a"], "2000", Type.STRING, + FeatureTokenType.VALUE, ["t", "b"], "1200", Type.STRING, + FeatureTokenType.OBJECT, ["t", "[id=rid]t__child"], + FeatureTokenType.VALUE, ["t", "[id=rid]t__child", "value"], "1000", Type.STRING, + FeatureTokenType.OBJECT_END, ["t", "[id=rid]t__child"], + FeatureTokenType.FEATURE_END, FeatureTokenType.INPUT_END + ] + + // schema order: child is emitted before a/b, with its object markers and child value intact + static List TOPLEVEL_EXPECTED = [ + FeatureTokenType.INPUT, true, FeatureTokenType.FEATURE, + FeatureTokenType.VALUE, ["id"], "f1", Type.STRING, + FeatureTokenType.OBJECT, ["child"], + FeatureTokenType.VALUE, ["child", "value"], "1000", Type.STRING, + FeatureTokenType.OBJECT_END, ["child"], + FeatureTokenType.VALUE, ["a"], "2000", Type.STRING, + FeatureTokenType.VALUE, ["b"], "1200", Type.STRING, + FeatureTokenType.FEATURE_END, FeatureTokenType.INPUT_END + ] + + def "a joined object declared before main-table columns is emitted at its schema position"() { + when: + List actual = run(TOPLEVEL, TOPLEVEL_SOURCE) + + then: + actual == TOPLEVEL_EXPECTED + } + + // outer object q; inner object dpl (on a joined table) declared before the inner scalars gst, vwl + static FeatureSchema NESTED = new ImmutableFeatureSchema.Builder() + .name("t").sourcePath("/t").type(Type.OBJECT) + .putProperties2("id", new ImmutableFeatureSchema.Builder() + .sourcePath("objid").type(Type.STRING).role(SchemaBase.Role.ID)) + .putProperties2("q", new ImmutableFeatureSchema.Builder() + .sourcePath("q").type(Type.OBJECT) + .putProperties2("dpl", new ImmutableFeatureSchema.Builder() + .sourcePath("[id=rid]q__dpl").type(Type.OBJECT) + .putProperties2("h", new ImmutableFeatureSchema.Builder() + .sourcePath("h").type(Type.STRING))) + .putProperties2("gst", new ImmutableFeatureSchema.Builder().sourcePath("gst").type(Type.STRING)) + .putProperties2("vwl", new ImmutableFeatureSchema.Builder().sourcePath("vwl").type(Type.STRING))) + .build() + + // provider order: the inner main-table scalars (gst, vwl) arrive before the joined object (dpl) + static List NESTED_SOURCE = [ + FeatureTokenType.INPUT, true, FeatureTokenType.FEATURE, + FeatureTokenType.VALUE, ["t", "objid"], "f1", Type.STRING, + FeatureTokenType.OBJECT, ["t", "q"], + FeatureTokenType.VALUE, ["t", "q", "gst"], "G", Type.STRING, + FeatureTokenType.VALUE, ["t", "q", "vwl"], "V", Type.STRING, + FeatureTokenType.OBJECT, ["t", "q", "[id=rid]q__dpl"], + FeatureTokenType.VALUE, ["t", "q", "[id=rid]q__dpl", "h"], "H", Type.STRING, + FeatureTokenType.OBJECT_END, ["t", "q", "[id=rid]q__dpl"], + FeatureTokenType.OBJECT_END, ["t", "q"], + FeatureTokenType.FEATURE_END, FeatureTokenType.INPUT_END + ] + + // schema order: dpl is emitted before gst/vwl inside q (the case the cursor placement alone gets + // wrong — it leaves dpl after gst/vwl; only the flush pass corrects it) + static List NESTED_EXPECTED = [ + FeatureTokenType.INPUT, true, FeatureTokenType.FEATURE, + FeatureTokenType.VALUE, ["id"], "f1", Type.STRING, + FeatureTokenType.OBJECT, ["q"], + FeatureTokenType.OBJECT, ["q", "dpl"], + FeatureTokenType.VALUE, ["q", "dpl", "h"], "H", Type.STRING, + FeatureTokenType.OBJECT_END, ["q", "dpl"], + FeatureTokenType.VALUE, ["q", "gst"], "G", Type.STRING, + FeatureTokenType.VALUE, ["q", "vwl"], "V", Type.STRING, + FeatureTokenType.OBJECT_END, ["q"], + FeatureTokenType.FEATURE_END, FeatureTokenType.INPUT_END + ] + + def "a nested joined object declared before its scalar siblings is emitted at its schema position"() { + when: + List actual = run(NESTED, NESTED_SOURCE) + + then: + actual == NESTED_EXPECTED + } + + // the provider produces a single-valued object backed by more than one table as several + // per-table fragments: q first with its main-table scalars (gst, vwl), then again with the + // joined object (dpl) - two OBJECT[q]..OBJECT_END[q] blocks + static List SPLIT_SOURCE = [ + FeatureTokenType.INPUT, true, FeatureTokenType.FEATURE, + FeatureTokenType.VALUE, ["t", "objid"], "f1", Type.STRING, + FeatureTokenType.OBJECT, ["t", "q"], + FeatureTokenType.VALUE, ["t", "q", "gst"], "G", Type.STRING, + FeatureTokenType.VALUE, ["t", "q", "vwl"], "V", Type.STRING, + FeatureTokenType.OBJECT_END, ["t", "q"], + FeatureTokenType.OBJECT, ["t", "q"], + FeatureTokenType.OBJECT, ["t", "q", "[id=rid]q__dpl"], + FeatureTokenType.VALUE, ["t", "q", "[id=rid]q__dpl", "h"], "H", Type.STRING, + FeatureTokenType.OBJECT_END, ["t", "q", "[id=rid]q__dpl"], + FeatureTokenType.OBJECT_END, ["t", "q"], + FeatureTokenType.FEATURE_END, FeatureTokenType.INPUT_END + ] + + // the fragments coalesce into one q object, its children in schema order (dpl, gst, vwl) + def "object fragments split across tables coalesce into a single object"() { + when: + List actual = run(NESTED, SPLIT_SOURCE) + + then: + actual == NESTED_EXPECTED + } +} diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureEventBufferSliceSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureEventBufferSliceSpec.groovy new file mode 100644 index 000000000..61c4162f2 --- /dev/null +++ b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureEventBufferSliceSpec.groovy @@ -0,0 +1,98 @@ +/* + * 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.Type +import de.ii.xtraplatform.features.domain.transform.FeatureEventBuffer +import spock.lang.Specification + +// A property produced from more than one table arrives as several fragments. When an unrelated +// property is produced between those fragments (provider order), the slice handed to an in-buffer +// transformer (e.g. concat) must still contain only the property's own tokens - otherwise the +// transformer would swallow the unrelated property into it (e.g. nest a separate array inside a +// concatenated object array). The buffer guarantees this by ordering itself before a slice is read. +class FeatureEventBufferSliceSpec extends Specification { + + // arrA and arrB are both top-level object arrays; arrA is declared first + static FeatureSchema SCHEMA = new ImmutableFeatureSchema.Builder() + .name("t").sourcePath("/t").type(Type.OBJECT) + .putProperties2("id", new ImmutableFeatureSchema.Builder().sourcePath("objid").type(Type.STRING).role(SchemaBase.Role.ID)) + .putProperties2("arrA", new ImmutableFeatureSchema.Builder().sourcePath("[id=a]t__arrA").type(Type.OBJECT_ARRAY) + .putProperties2("x", new ImmutableFeatureSchema.Builder().sourcePath("x").type(Type.STRING))) + .putProperties2("arrB", new ImmutableFeatureSchema.Builder().sourcePath("[id=b]t__arrB").type(Type.OBJECT_ARRAY) + .putProperties2("y", new ImmutableFeatureSchema.Builder().sourcePath("y").type(Type.STRING))) + .build() + + // provider order: arrA fragment 1, then arrB, then arrA fragment 2 (arrA split around arrB) + static List SOURCE = [ + FeatureTokenType.VALUE, ["id"], "f1", Type.STRING, + FeatureTokenType.ARRAY, ["arrA"], + FeatureTokenType.OBJECT, ["arrA"], FeatureTokenType.VALUE, ["arrA", "x"], "1", Type.STRING, FeatureTokenType.OBJECT_END, ["arrA"], + FeatureTokenType.ARRAY_END, ["arrA"], + FeatureTokenType.ARRAY, ["arrB"], + FeatureTokenType.OBJECT, ["arrB"], FeatureTokenType.VALUE, ["arrB", "y"], "b", Type.STRING, FeatureTokenType.OBJECT_END, ["arrB"], + FeatureTokenType.ARRAY_END, ["arrB"], + FeatureTokenType.ARRAY, ["arrA"], + FeatureTokenType.OBJECT, ["arrA"], FeatureTokenType.VALUE, ["arrA", "x"], "2", Type.STRING, FeatureTokenType.OBJECT_END, ["arrA"], + FeatureTokenType.ARRAY_END, ["arrA"] + ] + + // a schema with a present property followed by an absent one. Shrinking the present property's + // slice shifts every later position by the (negative) size delta; an absent position left at + // start 0 would be driven negative and a subsequent getSlice would throw fromIndex < 0. + static FeatureSchema SCHEMA_ABSENT = new ImmutableFeatureSchema.Builder() + .name("t").sourcePath("/t").type(Type.OBJECT) + .putProperties2("id", new ImmutableFeatureSchema.Builder().sourcePath("objid").type(Type.STRING).role(SchemaBase.Role.ID)) + .putProperties2("a", new ImmutableFeatureSchema.Builder().sourcePath("a").type(Type.STRING)) + .putProperties2("b", new ImmutableFeatureSchema.Builder().sourcePath("b").type(Type.STRING)) + .build() + + def "getSlice on an absent position after a shrunk slice does not go negative"() { + given: + FeatureEventBuffer buffer = Util.createBuffer(SCHEMA_ABSENT, []) + buffer.reset("test") + // id and a are present, b is absent + [ + FeatureTokenType.VALUE, ["id"], "x", Type.STRING, + FeatureTokenType.VALUE, ["a"], "longvalue", Type.STRING + ].forEach(token -> buffer.push(token)) + def mapping = SchemaMapping.of(SCHEMA_ABSENT) + int posA = mapping.getPositionsForTargetPath(["a"]).get(0) + int posB = mapping.getPositionsForTargetPath(["b"]).get(0) + + when: + // read then shrink a's slice (delta < 0), which shifts every later position + buffer.getSlice(posA) + buffer.replaceSlice(posA, []) + // b is absent; its slice must still resolve to an empty, in-range range + List sliceB = buffer.getSlice(posB) + + then: + noExceptionThrown() + sliceB.isEmpty() + } + + def "a slice spans only its own property when its fragments are split around another"() { + given: + FeatureEventBuffer buffer = Util.createBuffer(SCHEMA, []) + buffer.reset("test") + SOURCE.forEach(token -> buffer.push(token)) + int posA = SchemaMapping.of(SCHEMA).getPositionsForTargetPath(["arrA"]).get(0) + + when: + List slice = buffer.getSlice(posA) + + then: + // both arrA fragments are present (the slice gathers the whole property) ... + slice.contains("1") + slice.contains("2") + // ... and the unrelated arrB is NOT swallowed into arrA's slice + !slice.contains(["arrB"]) + !slice.contains("b") + } +} diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappingSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappingSpec.groovy index ed8b2381d..39986d174 100644 --- a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappingSpec.groovy +++ b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerMappingSpec.groovy @@ -34,7 +34,7 @@ class FeatureTokenTransformerMappingSpec extends Specification { where: casename | schema | source | expected - //TODO "joined value array between main columns" | FeatureSchemaFixtures.BIOTOP | FeatureTokenFixtures.SINGLE_FEATURE_VALUE_ARRAY_AT_END | FeatureTokenFixtures.SINGLE_FEATURE_VALUE_ARRAY_IN_ORDER_MAPPED + "joined value array between main columns" | FeatureSchemaFixtures.BIOTOP | FeatureTokenFixtures.SINGLE_FEATURE_VALUE_ARRAY_AT_END | FeatureTokenFixtures.SINGLE_FEATURE_VALUE_ARRAY_IN_ORDER_MAPPED "joined object array" | FeatureSchemaFixtures.OBJECT_ARRAY | FeatureTokenFixtures.EXPLORATION_SITE_OBJECT_ARRAY | FeatureTokenFixtures.EXPLORATION_SITE_OBJECT_ARRAY_MAPPED //"object without source path" | FeatureSchemaFixtures.OBJECT_WITHOUT_SOURCE_PATH | FeatureTokenFixtures.OBJECT_WITHOUT_SOURCE_PATH | FeatureTokenFixtures.OBJECT_WITHOUT_SOURCE_PATH_MAPPED diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerSortingSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerSortingSpec.groovy deleted file mode 100644 index 3bc2668ac..000000000 --- a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureTokenTransformerSortingSpec.groovy +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2022 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 spock.lang.Ignore -import spock.lang.Specification - -/** - * @author zahnen - */ -@Ignore -class FeatureTokenTransformerSortingSpec extends Specification { - - FeatureTokenReader tokenReader - List tokens - - def setup() { - FeatureTokenTransformerSorting mapper = new FeatureTokenTransformerSorting() - FeatureQuery query = ImmutableFeatureQuery.builder().type("test").build() - FeatureEventHandler.ModifiableContext context = mapper.createContext() - .setQuery(query) - .setMappings([test: FeatureSchemaFixtures.BIOTOP_MAPPING]) - .setType('test') - - tokenReader = new FeatureTokenReader(mapper, context) - tokens = [] - mapper.init(token -> tokens.add(token)) - } - - def 'single feature join before column value'() { - - given: - - when: - - FeatureTokenFixtures.SINGLE_FEATURE_VALUE_ARRAY_AT_END.forEach(token -> tokenReader.onToken(token)) - - then: - - tokens == FeatureTokenFixtures.SINGLE_FEATURE_VALUE_ARRAY_IN_ORDER - - } - -} diff --git a/xtraplatform-features/src/testFixtures/groovy/de/ii/xtraplatform/features/domain/FeatureTokenFixtures.groovy b/xtraplatform-features/src/testFixtures/groovy/de/ii/xtraplatform/features/domain/FeatureTokenFixtures.groovy index dfbbb79b5..ec906e273 100644 --- a/xtraplatform-features/src/testFixtures/groovy/de/ii/xtraplatform/features/domain/FeatureTokenFixtures.groovy +++ b/xtraplatform-features/src/testFixtures/groovy/de/ii/xtraplatform/features/domain/FeatureTokenFixtures.groovy @@ -390,7 +390,7 @@ class FeatureTokenFixtures { "611320001-1", Type.STRING, FeatureTokenType.ARRAY, - ["biotop", "[eid=id]erfasser"], + ["biotop", "[eid=id]erfasser", "name"], FeatureTokenType.VALUE, ["biotop", "[eid=id]erfasser", "name"], "John Doe", @@ -400,7 +400,7 @@ class FeatureTokenFixtures { "Jane Doe", Type.STRING, FeatureTokenType.ARRAY_END, - ["biotop", "[eid=id]erfasser"], + ["biotop", "[eid=id]erfasser", "name"], FeatureTokenType.FEATURE_END, FeatureTokenType.INPUT_END ] @@ -414,7 +414,7 @@ class FeatureTokenFixtures { "24", Type.STRING, FeatureTokenType.ARRAY, - ["biotop", "[eid=id]erfasser"], + ["biotop", "[eid=id]erfasser", "name"], FeatureTokenType.VALUE, ["biotop", "[eid=id]erfasser", "name"], "John Doe", @@ -424,7 +424,7 @@ class FeatureTokenFixtures { "Jane Doe", Type.STRING, FeatureTokenType.ARRAY_END, - ["biotop", "[eid=id]erfasser"], + ["biotop", "[eid=id]erfasser", "name"], FeatureTokenType.VALUE, ["biotop", "kennung"], "611320001-1", @@ -652,37 +652,37 @@ class FeatureTokenFixtures { FeatureTokenType.OBJECT, ["task"], FeatureTokenType.VALUE, - ["task", "title"], - "11", - Type.STRING, - FeatureTokenType.VALUE, ["task", "id"], "34", Type.STRING, + FeatureTokenType.VALUE, + ["task", "title"], + "11", + Type.STRING, FeatureTokenType.OBJECT_END, ["task"], FeatureTokenType.OBJECT, ["task"], FeatureTokenType.VALUE, - ["task", "title"], - "35", - Type.STRING, - FeatureTokenType.VALUE, ["task", "id"], "36", Type.STRING, + FeatureTokenType.VALUE, + ["task", "title"], + "35", + Type.STRING, FeatureTokenType.OBJECT_END, ["task"], FeatureTokenType.OBJECT, ["task"], FeatureTokenType.VALUE, - ["task", "title"], - "12", - Type.STRING, - FeatureTokenType.VALUE, ["task", "id"], "37", Type.STRING, + FeatureTokenType.VALUE, + ["task", "title"], + "12", + Type.STRING, FeatureTokenType.OBJECT_END, ["task"], FeatureTokenType.ARRAY_END, diff --git a/xtraplatform-features/src/testFixtures/resources/feature-tokens/pfs_plan-hatObjekt.yml b/xtraplatform-features/src/testFixtures/resources/feature-tokens/pfs_plan-hatObjekt.yml index 8b292fb49..c21ea8d77 100644 --- a/xtraplatform-features/src/testFixtures/resources/feature-tokens/pfs_plan-hatObjekt.yml +++ b/xtraplatform-features/src/testFixtures/resources/feature-tokens/pfs_plan-hatObjekt.yml @@ -16,18 +16,18 @@ tokens: source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung target: hatObjekt onlyIf: source,concat,coalesce,mapped - - type: VALUE - source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/_id - target: hatObjekt.1_id - value: 1 - valueType: INTEGER - onlyIf: source,concat,coalesce - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/constant_hatObjekt_37 target: hatObjekt.1_type value: bst_erdgasleitung valueType: STRING onlyIf: source,concat,coalesce + - type: VALUE + source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/_id + target: hatObjekt.1_id + value: 1 + valueType: INTEGER + onlyIf: source,concat,coalesce - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/text target: hatObjekt.1_title @@ -58,18 +58,18 @@ tokens: source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung target: hatObjekt onlyIf: source,concat,mapped - - type: VALUE - source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/_id - target: hatObjekt.1_id - value: 2 - valueType: INTEGER - onlyIf: source,concat - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/constant_hatObjekt_37 target: hatObjekt.1_type value: bst_erdgasleitung valueType: STRING onlyIf: source,concat + - type: VALUE + source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/_id + target: hatObjekt.1_id + value: 2 + valueType: INTEGER + onlyIf: source,concat - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_erdgasleitung/text target: hatObjekt.1_title @@ -108,18 +108,18 @@ tokens: source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher target: hatObjekt onlyIf: source,concat - - type: VALUE - source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/_id - target: hatObjekt.2_id - value: null - valueType: INTEGER - onlyIf: source,concat - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/constant_hatObjekt_38 target: hatObjekt.2_type value: bst_speicher valueType: STRING onlyIf: source,concat + - type: VALUE + source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/_id + target: hatObjekt.2_id + value: null + valueType: INTEGER + onlyIf: source,concat - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/text target: hatObjekt.2_title @@ -140,18 +140,18 @@ tokens: source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher target: hatObjekt onlyIf: source,concat,mapped - - type: VALUE - source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/_id - target: hatObjekt.2_id - value: 2 - valueType: INTEGER - onlyIf: source,concat - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/constant_hatObjekt_38 target: hatObjekt.2_type value: bst_speicher valueType: STRING onlyIf: source,concat + - type: VALUE + source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/_id + target: hatObjekt.2_id + value: 2 + valueType: INTEGER + onlyIf: source,concat - type: VALUE source: /pfs_plan/[_id=gehoertzuplan_pfs_plan_fk]bst_speicher/text target: hatObjekt.2_title