From 9c31e3949586701009d51c3ed02d9640a8b247c0 Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Sat, 2 May 2026 13:29:59 +0200 Subject: [PATCH 1/9] GML geometry encoder: Use XMLStreamWriter Use `XMLStreamWriter` instead of `StringBuilder`. --- .../gml/domain/GeometryEncoderGml.java | 184 ++++++------- .../features/gml/domain/GeometrySpec.groovy | 245 ++++++++++++------ 2 files changed, 265 insertions(+), 164 deletions(-) diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java index 64a715f52..4128d90c7 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java @@ -33,15 +33,12 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; public class GeometryEncoderGml implements GeometryVisitor { public static final String SPACE = " "; - public static final String OPEN = "<"; - public static final String CLOSE = ">"; - public static final String EQUALS = "="; - public static final String QUOTE = "\""; - public static final String SLASH = "/"; public static final String COMMA = ","; private static final String POINT = "Point"; @@ -124,7 +121,7 @@ public enum Options { POLYGON_AS_PATCH } - private final StringBuilder builder; + private final XMLStreamWriter xmlWriter; private final Optional gmlPrefix; private final String gmlIdPrefix; private final Set options; @@ -135,8 +132,8 @@ public enum Options { private int nextGmlId = 0; private String srsName; - public GeometryEncoderGml(StringBuilder builder) { - this.builder = builder; + public GeometryEncoderGml(XMLStreamWriter xmlWriter) { + this.xmlWriter = xmlWriter; this.gmlPrefix = Optional.of("gml"); this.gmlIdPrefix = "geom_"; this.options = Set.of(); @@ -144,7 +141,7 @@ public GeometryEncoderGml(StringBuilder builder) { this.encodeAsSegmentOrPatch = Optional.of( new GeometryEncoderGml( - builder, + xmlWriter, GmlVersion.GML32, Set.of(Options.LINE_STRING_AS_SEGMENT, Options.POLYGON_AS_PATCH), this.gmlPrefix, @@ -153,19 +150,24 @@ public GeometryEncoderGml(StringBuilder builder) { this.encodeAsEmbeddedGeometry = Optional.of( new GeometryEncoderGml( - builder, GmlVersion.GML32, Set.of(), this.gmlPrefix, Optional.empty(), List.of())); + xmlWriter, + GmlVersion.GML32, + Set.of(), + this.gmlPrefix, + Optional.empty(), + List.of())); this.srsName = null; this.version = GmlVersion.GML32; } public GeometryEncoderGml( - StringBuilder builder, + XMLStreamWriter xmlWriter, GmlVersion version, Set options, Optional gmlPrefix, Optional gmlIdPrefix, List precision) { - this.builder = builder; + this.xmlWriter = xmlWriter; this.gmlPrefix = gmlPrefix; this.gmlIdPrefix = gmlIdPrefix.orElse("geom_"); this.options = options; @@ -175,7 +177,7 @@ public GeometryEncoderGml( ? Optional.empty() : Optional.of( new GeometryEncoderGml( - builder, + xmlWriter, version, ImmutableSet.builder() .addAll(options) @@ -188,7 +190,7 @@ public GeometryEncoderGml( options.contains(Options.WITH_SRS_NAME) ? Optional.of( new GeometryEncoderGml( - builder, + xmlWriter, version, ImmutableSet.builder() .addAll( @@ -225,23 +227,35 @@ public Optional initAndCheckGeometry(Geometry geometry) { } private void write(String s) { - builder.append(s); + try { + xmlWriter.writeCharacters(s); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void write(double d) { - builder.append(d); + try { + xmlWriter.writeCharacters(Double.toString(d)); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void write(BigDecimal d) { - builder.append(d.toString()); + try { + xmlWriter.writeCharacters(d.toString()); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void writeAttribute(String name, String value) { - write(name); - write(EQUALS); - write(QUOTE); - write(value); - write(QUOTE); + try { + xmlWriter.writeAttribute(name, value); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void writeStartTagObject(String tagName, boolean suppressSrsName) { @@ -262,33 +276,33 @@ private void writeStartTagProperty(String tagName, Map attribute private void writeStartTag( String tagName, Map attributes, boolean isObject, boolean suppressSrsName) { - write(OPEN); - gmlPrefix.ifPresent(pre -> write(pre + ':')); - write(tagName); - if (isObject) { - if (options.contains(Options.WITH_GML_ID)) { - write(SPACE); - writeAttribute(gmlPrefix.map(pre -> pre + ':' + ID).orElse(ID), gmlIdPrefix + nextGmlId++); + try { + if (gmlPrefix.isPresent()) { + xmlWriter.writeStartElement(gmlPrefix.get(), tagName, null); + } else { + xmlWriter.writeStartElement(tagName); } - if (options.contains(Options.WITH_SRS_NAME) && !suppressSrsName) { - write(SPACE); - writeAttribute(SRS_NAME, srsName); + if (isObject) { + if (options.contains(Options.WITH_GML_ID)) { + writeAttribute( + gmlPrefix.map(pre -> pre + ':' + ID).orElse(ID), gmlIdPrefix + nextGmlId++); + } + if (options.contains(Options.WITH_SRS_NAME) && !suppressSrsName) { + writeAttribute(SRS_NAME, srsName); + } } + attributes.forEach(this::writeAttribute); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); } - attributes.forEach( - (key, value) -> { - write(SPACE); - writeAttribute(key, value); - }); - write(CLOSE); } - private void writeEndTag(String tagName) { - write(OPEN); - write(SLASH); - gmlPrefix.ifPresent(pre -> write(pre + ':')); - write(tagName); - write(CLOSE); + private void writeEndTag() { + try { + xmlWriter.writeEndElement(); + } catch (XMLStreamException e) { + throw new IllegalStateException(e); + } } private void writeCoordinates(double[] coordinates, Axes axes) { @@ -326,7 +340,7 @@ private void writePosition(double[] coordinates, Axes axes) { writeStartTagProperty(COORDINATES); } writeCoordinates(coordinates, axes); - writeEndTag(version != GmlVersion.GML21 ? POS : COORDINATES); + writeEndTag(); } private void writePositionList(double[] coordinates, Axes axes) { @@ -340,14 +354,14 @@ private void writePositionList(double[] coordinates, Axes axes) { writeStartTagProperty(COORDINATES); } writeCoordinates(coordinates, axes); - writeEndTag(version != GmlVersion.GML21 ? POS_LIST : COORDINATES); + writeEndTag(); } @Override public Void visit(Point geometry) { writeStartTagObject(POINT, false); writePosition(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(POINT); + writeEndTag(); return null; } @@ -358,9 +372,9 @@ public Void visit(MultiPoint geometry) { Point point = geometry.getValue().get(i); writeStartTagProperty(POINT_MEMBER); point.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(POINT_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_POINT); + writeEndTag(); return null; } @@ -384,7 +398,7 @@ private void writeLineString(LineString geometry, boolean asSegment) { writeStartTagObject(tagName, false); } writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(tagName); + writeEndTag(); } private void writeCircularString(CircularString geometry, boolean asSegment) { @@ -395,10 +409,10 @@ private void writeCircularString(CircularString geometry, boolean asSegment) { String tagName = geometry.getValue().getNumPositions() == 3 ? ARC : ARC_STRING; writeStartTagDataType(tagName); writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(tagName); + writeEndTag(); if (!asSegment) { - writeEndTag(SEGMENTS); - writeEndTag(CURVE); + writeEndTag(); + writeEndTag(); } } @@ -409,9 +423,9 @@ public Void visit(MultiLineString geometry) { LineString lineString = geometry.getValue().get(i); writeStartTagProperty(version != GmlVersion.GML21 ? CURVE_MEMBER : LINE_STRING_MEMBER); lineString.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(version != GmlVersion.GML21 ? CURVE_MEMBER : LINE_STRING_MEMBER); + writeEndTag(); } - writeEndTag(version != GmlVersion.GML21 ? MULTI_CURVE : MULTI_LINE_STRING); + writeEndTag(); return null; } @@ -432,18 +446,10 @@ public Void visit(Polygon geometry) { } writeStartTagObject(LINEAR_RING, true); writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(LINEAR_RING); - if (i == 0) { - writeEndTag(version != GmlVersion.GML21 ? EXTERIOR : OUTER_BOUNDARY_IS); - } else { - writeEndTag(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); - } - } - if (asPatch) { - writeEndTag(POLYGON_PATCH); - } else { - writeEndTag(POLYGON); + writeEndTag(); + writeEndTag(); } + writeEndTag(); return null; } @@ -454,9 +460,9 @@ public Void visit(MultiPolygon geometry) { Polygon polygon = geometry.getValue().get(i); writeStartTagProperty(version != GmlVersion.GML21 ? SURFACE_MEMBER : POLYGON_MEMBER); polygon.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(version != GmlVersion.GML21 ? SURFACE_MEMBER : POLYGON_MEMBER); + writeEndTag(); } - writeEndTag(version != GmlVersion.GML21 ? MULTI_SURFACE : MULTI_POLYGON); + writeEndTag(); return null; } @@ -467,9 +473,9 @@ public Void visit(MultiCurve geometry) { Geometry geometry2 = geometry.getValue().get(i); writeStartTagProperty(CURVE_MEMBER); geometry2.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(CURVE_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_CURVE); + writeEndTag(); return null; } @@ -480,9 +486,9 @@ public Void visit(MultiSurface geometry) { Geometry geometry2 = geometry.getValue().get(i); writeStartTagProperty(SURFACE_MEMBER); geometry2.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(SURFACE_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_SURFACE); + writeEndTag(); return null; } @@ -493,9 +499,9 @@ public Void visit(GeometryCollection geometry) { Geometry geometry2 = geometry.getValue().get(i); writeStartTagProperty(GEOMETRY_MEMBER); geometry2.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(GEOMETRY_MEMBER); + writeEndTag(); } - writeEndTag(MULTI_GEOMETRY); + writeEndTag(); return null; } @@ -507,8 +513,8 @@ public Void visit(CompoundCurve geometry) { SingleCurve curve = geometry.getValue().get(i); curve.accept(encodeAsSegmentOrPatch.orElse(this)); } - writeEndTag(SEGMENTS); - writeEndTag(CURVE); + writeEndTag(); + writeEndTag(); return null; } @@ -522,18 +528,14 @@ public Void visit(CurvePolygon geometry) { } else { writeStartTagProperty(INTERIOR); } - writeStartTagObject(RING, false); + writeStartTagObject(RING, true); writeStartTagProperty(CURVE_MEMBER); ring.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(CURVE_MEMBER); - writeEndTag(RING); - if (i == 0) { - writeEndTag(EXTERIOR); - } else { - writeEndTag(INTERIOR); - } + writeEndTag(); + writeEndTag(); + writeEndTag(); } - writeEndTag(POLYGON); + writeEndTag(); return null; } @@ -547,11 +549,11 @@ public Void visit(PolyhedralSurface geometry) { writeStartTagProperty(SURFACE_MEMBER); Polygon polygon = geometry.getValue().get(i); polygon.accept(encodeAsEmbeddedGeometry.orElse(this)); - writeEndTag(SURFACE_MEMBER); + writeEndTag(); } - writeEndTag(SHELL); - writeEndTag(EXTERIOR); - writeEndTag(SOLID); + writeEndTag(); + writeEndTag(); + writeEndTag(); } else { writeStartTagObject(POLYHEDRAL_SURFACE, false); writeStartTagProperty(PATCHES); @@ -559,8 +561,8 @@ public Void visit(PolyhedralSurface geometry) { Polygon polygon = geometry.getValue().get(i); polygon.accept(encodeAsSegmentOrPatch.orElse(this)); } - writeEndTag(PATCHES); - writeEndTag(POLYHEDRAL_SURFACE); + writeEndTag(); + writeEndTag(); } return null; } diff --git a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy index 6d8f076cc..ff5dc2088 100644 --- a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy +++ b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy @@ -26,28 +26,37 @@ import de.ii.xtraplatform.geometries.domain.Position import de.ii.xtraplatform.geometries.domain.PositionList import spock.lang.Specification +import javax.xml.stream.XMLOutputFactory + class GeometrySpec extends Specification { - StringBuilder sb = new StringBuilder() - GeometryEncoderGml gmlEncoderWith = new GeometryEncoderGml(sb, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.WITH_GML_ID, GeometryEncoderGml.Options.WITH_SRS_NAME), Optional.of("gml"), Optional.of("g_"), List.of(1,1)) - GeometryEncoderGml gmlEncoderWithout = new GeometryEncoderGml(sb, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) - GeometryEncoderGml gmlEncoderGml21 = new GeometryEncoderGml(sb, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) + // ... def 'POINT XY'() { - given: Geometry geometry = Point.of(10.81, 10.37) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut1 = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut1 = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderWith = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.WITH_GML_ID, GeometryEncoderGml.Options.WITH_SRS_NAME), Optional.of("gml"), Optional.of("g_"), List.of(1,1)) geometry.accept(gmlEncoderWith) - String gmlOut2 = sb.toString() - sb.setLength(0) + xmlWriter2.flush() + String gmlOut2 = sw2.toString() + + def sw3 = new StringWriter() + def xmlWriter3 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw3) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter3, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter3.flush() + String gmlOut21 = sw3.toString() then: gmlOut1 == "10.81 10.37" @@ -60,12 +69,19 @@ class GeometrySpec extends Specification { Geometry geometry = Point.of(Position.ofXYZ(10.81, 10.37, 5.00)) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.81 10.37 5.0" @@ -77,9 +93,12 @@ class GeometrySpec extends Specification { Geometry geometry = Point.of(Position.ofXYM(10.81, 10.37, 5.00)) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "10.81 10.37" @@ -90,9 +109,12 @@ class GeometrySpec extends Specification { Geometry geometry = Point.of(Position.ofXYZM(10.81, 10.37, 5.00, 7.50)) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "10.81 10.37 5.0" @@ -100,12 +122,15 @@ class GeometrySpec extends Specification { def 'POINT XY EMPTY'() { given: - Geometry geometry = Point.empty(Axes.XY); + Geometry geometry = Point.empty(Axes.XY) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "NaN NaN" @@ -113,12 +138,15 @@ class GeometrySpec extends Specification { def 'POINT XYZ EMPTY'() { given: - Geometry geometry = Point.empty(Axes.XYZ); + Geometry geometry = Point.empty(Axes.XYZ) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "NaN NaN NaN" @@ -126,15 +154,22 @@ class GeometrySpec extends Specification { def 'LINESTRING XY'() { given: - Geometry geometry = LineString.of(new double[]{10.0,10.0,20.0,20.0,30.0,40.0}); + Geometry geometry = LineString.of(new double[]{10.0,10.0,20.0,20.0,30.0,40.0}) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.0 30.0 40.0" @@ -143,15 +178,22 @@ class GeometrySpec extends Specification { def 'LINESTRING XYZ'() { given: - Geometry geometry = LineString.of(PositionList.of(Axes.XYZ, new double[]{10.0,10.0,1.0,20.0,20.0,2.0,30.0,40.0,3.0})); + Geometry geometry = LineString.of(PositionList.of(Axes.XYZ, new double[]{10.0,10.0,1.0,20.0,20.0,2.0,30.0,40.0,3.0})) when: - sb.setLength(0) + def sw1 = new StringWriter() + def xmlWriter1 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw1) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter1, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter1.flush() + String gmlOut = sw1.toString() + + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 1.0 20.0 20.0 2.0 30.0 40.0 3.0" @@ -163,12 +205,18 @@ class GeometrySpec extends Specification { Geometry geometry = MultiPoint.of(List.of(Point.of(10,10),Point.of(20,20))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.020.0 20.0" @@ -180,12 +228,18 @@ class GeometrySpec extends Specification { Geometry geometry = Polygon.of(List.of(PositionList.of(Axes.XY,new double[]{10.0,10.0,20.0,20.0,30.0,40.0,10.0,10.0}))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.0 30.0 40.0 10.0 10.0" @@ -200,12 +254,18 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.030.0 40.0 50.0 60.0" @@ -217,12 +277,18 @@ class GeometrySpec extends Specification { Geometry geometry = MultiPolygon.of(List.of(Polygon.of(List.of(PositionList.of(Axes.XY,new double[]{10.0,10.0,20.0,20.0,30.0,40.0,10.0,10.0}))), Polygon.of(List.of(PositionList.of(Axes.XY,new double[]{50.0,50.0,60.0,60.0,70.0,80.0,50.0,50.0}))))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.0 20.0 20.0 30.0 40.0 10.0 10.050.0 50.0 60.0 60.0 70.0 80.0 50.0 50.0" @@ -234,12 +300,18 @@ class GeometrySpec extends Specification { Geometry geometry = GeometryCollection.of(List.of(Point.of(10,10),LineString.of(new double[]{20.0,20.0,30.0,30.0}))) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.020.0 20.0 30.0 30.0" @@ -253,12 +325,18 @@ class GeometrySpec extends Specification { Geometry geometry = GeometryCollection.of(List.of(geometryCollection, multiPoint)) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() - sb.setLength(0) + xmlWriter.flush() + String gmlOut = sw.toString() + def sw2 = new StringWriter() + def xmlWriter2 = XMLOutputFactory.newInstance().createXMLStreamWriter(sw2) + def gmlEncoderGml21 = new GeometryEncoderGml(xmlWriter2, GmlVersion.GML21, Set.of(), Optional.of("gml"), Optional.empty(), List.of(1,1)) geometry.accept(gmlEncoderGml21) - String gmlOut21 = sb.toString() + xmlWriter2.flush() + String gmlOut21 = sw2.toString() then: gmlOut == "10.0 10.020.0 20.0 30.0 30.010.0 10.020.0 20.0" @@ -272,9 +350,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.00.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0" @@ -285,9 +366,12 @@ class GeometrySpec extends Specification { Geometry geometry = CircularString.of(PositionList.of(Axes.XY, new double[]{0.0, 0.0, 1.0, 1.0, 2.0, 0.0})) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.0 2.0 0.0" @@ -301,9 +385,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0" @@ -316,9 +403,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.0 0.0 2.0 -1.0 -1.0 0.0 0.0" @@ -332,9 +422,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0 3.0 1.0" @@ -348,9 +441,12 @@ class GeometrySpec extends Specification { )) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.01.0 1.0 2.0 2.0 3.0 2.0 2.0 1.0 1.0 1.0" @@ -371,9 +467,12 @@ class GeometrySpec extends Specification { ), true) when: - sb.setLength(0) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoderWithout = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(), Optional.of("gml"), Optional.empty(), List.of()) geometry.accept(gmlEncoderWithout) - String gmlOut = sb.toString() + xmlWriter.flush() + String gmlOut = sw.toString() then: gmlOut == "280414.631 5660090.756 40.255 280414.631 5660088.454 40.255 280414.631 5660088.454 32.967 280414.631 5660090.756 32.967 280414.631 5660090.756 40.255280414.631 5660088.454 40.255 280405.623 5660088.454 33.256 280405.623 5660088.454 32.967 280414.631 5660088.454 32.967 280414.631 5660088.454 40.255280405.623 5660088.454 33.256 280405.623 5660090.756 33.256 280405.623 5660090.756 32.967 280405.623 5660088.454 32.967 280405.623 5660088.454 33.256280405.623 5660090.756 33.256 280414.631 5660090.756 40.255 280414.631 5660090.756 32.967 280405.623 5660090.756 32.967 280405.623 5660090.756 33.256280405.623 5660088.454 33.256 280414.631 5660088.454 40.255 280411.722 5660088.454 41.63 280405.623 5660088.454 33.256280414.631 5660090.756 40.255 280405.623 5660090.756 33.256 280411.722 5660090.756 41.63 280414.631 5660090.756 40.255280414.631 5660088.454 40.255 280414.631 5660090.756 40.255 280411.722 5660090.756 41.63 280411.722 5660088.454 41.63 280414.631 5660088.454 40.255280405.623 5660090.756 33.256 280405.623 5660088.454 33.256 280411.722 5660088.454 41.63 280411.722 5660090.756 41.63 280405.623 5660090.756 33.256280414.631 5660090.756 32.967 280414.631 5660088.454 32.967 280405.623 5660088.454 32.967 280405.623 5660090.756 32.967 280414.631 5660090.756 32.967" From c725682b56665f406bdf89d5c9b6a466d9bf35fb Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Fri, 8 May 2026 12:41:08 +0000 Subject: [PATCH 2/9] GML geometry encoder: add USE_SURFACE_RING_CURVE option --- .../gml/domain/GeometryEncoderGml.java | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java index 4128d90c7..33be1e6aa 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java @@ -51,6 +51,7 @@ public class GeometryEncoderGml implements GeometryVisitor { private static final String MULTI_CURVE = "MultiCurve"; private static final String MULTI_LINE_STRING = "MultiLineString"; private static final String POLYGON = "Polygon"; + private static final String SURFACE = "Surface"; private static final String POLYGON_PATCH = "PolygonPatch"; private static final String POLYHEDRAL_SURFACE = "PolyhedralSurface"; private static final String MULTI_SURFACE = "MultiSurface"; @@ -118,7 +119,8 @@ public enum Options { WITH_SRS_NAME, WITH_SRS_DIMENSION, LINE_STRING_AS_SEGMENT, - POLYGON_AS_PATCH + POLYGON_AS_PATCH, + USE_SURFACE_RING_CURVE } private final XMLStreamWriter xmlWriter; @@ -389,16 +391,24 @@ public Void visit(SingleCurve geometry) { } private void writeLineString(LineString geometry, boolean asSegment) { - String tagName; + boolean useRing = options.contains(Options.USE_SURFACE_RING_CURVE); if (asSegment) { - tagName = LINE_STRING_SEGMENT; - writeStartTagDataType(tagName); + writeStartTagDataType(LINE_STRING_SEGMENT); + writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); + } else if (useRing) { + writeStartTagObject(CURVE, false); + writeStartTagProperty(SEGMENTS); + writeStartTagDataType(LINE_STRING_SEGMENT); + writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); + writeEndTag(); + writeEndTag(); } else { - tagName = LINE_STRING; - writeStartTagObject(tagName, false); + writeStartTagObject(LINE_STRING, false); + writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); } - writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); - writeEndTag(); } private void writeCircularString(CircularString geometry, boolean asSegment) { @@ -432,7 +442,12 @@ public Void visit(MultiLineString geometry) { @Override public Void visit(Polygon geometry) { boolean asPatch = options.contains(Options.POLYGON_AS_PATCH); - if (asPatch) { + boolean useRing = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useRing) { + writeStartTagObject(SURFACE, false); + writeStartTagProperty(PATCHES); + writeStartTagDataType(POLYGON_PATCH); + } else if (asPatch) { writeStartTagDataType(POLYGON_PATCH); } else { writeStartTagObject(POLYGON, false); @@ -444,8 +459,21 @@ public Void visit(Polygon geometry) { } else { writeStartTagProperty(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); } - writeStartTagObject(LINEAR_RING, true); - writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); + if (useRing) { + writeStartTagObject(RING, true); + writeStartTagProperty(CURVE_MEMBER); + writeStartTagDataType(LINE_STRING_SEGMENT); + writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); + writeEndTag(); + writeEndTag(); + } else { + writeStartTagObject(LINEAR_RING, true); + writePositionList(ring.getValue().getCoordinates(), geometry.getAxes()); + } + writeEndTag(); + writeEndTag(); + } + if (useRing) { writeEndTag(); writeEndTag(); } From bd36c6af4751bf3610fffad8d4fba9e972ae5e40 Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Sat, 9 May 2026 11:04:55 +0000 Subject: [PATCH 3/9] GML geometry encoder: emit Surface/PolygonPatch and CompositeCurve for USE_SURFACE_RING_CURVE CurvePolygon now serializes as gml:Surface with gml:PolygonPatch; gml:Ring holds curve members directly (it is implicitly a CompositeCurve). CompoundCurve serializes as gml:CompositeCurve. Adds Spock tests for LineString, Polygon, CurvePolygon (CircularString and CompoundCurve rings), and CompoundCurve. --- .../gml/domain/GeometryEncoderGml.java | 68 ++++++++++---- .../features/gml/domain/GeometrySpec.groovy | 93 +++++++++++++++++++ 2 files changed, 143 insertions(+), 18 deletions(-) diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java index 33be1e6aa..72482eb57 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java @@ -391,12 +391,12 @@ public Void visit(SingleCurve geometry) { } private void writeLineString(LineString geometry, boolean asSegment) { - boolean useRing = options.contains(Options.USE_SURFACE_RING_CURVE); + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); if (asSegment) { writeStartTagDataType(LINE_STRING_SEGMENT); writePositionList(geometry.getValue().getCoordinates(), geometry.getAxes()); writeEndTag(); - } else if (useRing) { + } else if (useSurfaceAndCurve) { writeStartTagObject(CURVE, false); writeStartTagProperty(SEGMENTS); writeStartTagDataType(LINE_STRING_SEGMENT); @@ -442,8 +442,8 @@ public Void visit(MultiLineString geometry) { @Override public Void visit(Polygon geometry) { boolean asPatch = options.contains(Options.POLYGON_AS_PATCH); - boolean useRing = options.contains(Options.USE_SURFACE_RING_CURVE); - if (useRing) { + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useSurfaceAndCurve) { writeStartTagObject(SURFACE, false); writeStartTagProperty(PATCHES); writeStartTagDataType(POLYGON_PATCH); @@ -459,7 +459,7 @@ public Void visit(Polygon geometry) { } else { writeStartTagProperty(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); } - if (useRing) { + if (useSurfaceAndCurve) { writeStartTagObject(RING, true); writeStartTagProperty(CURVE_MEMBER); writeStartTagDataType(LINE_STRING_SEGMENT); @@ -473,7 +473,7 @@ public Void visit(Polygon geometry) { writeEndTag(); writeEndTag(); } - if (useRing) { + if (useSurfaceAndCurve) { writeEndTag(); writeEndTag(); } @@ -535,31 +535,63 @@ public Void visit(GeometryCollection geometry) { @Override public Void visit(CompoundCurve geometry) { - writeStartTagObject(CURVE, false); - writeStartTagProperty(SEGMENTS); - for (int i = 0; i < geometry.getNumGeometries(); i++) { - SingleCurve curve = geometry.getValue().get(i); - curve.accept(encodeAsSegmentOrPatch.orElse(this)); + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useSurfaceAndCurve) { + writeStartTagObject(COMPOSITE_CURVE, false); + for (int i = 0; i < geometry.getNumGeometries(); i++) { + SingleCurve curve = geometry.getValue().get(i); + writeStartTagProperty(CURVE_MEMBER); + curve.accept(encodeAsEmbeddedGeometry.orElse(this)); + writeEndTag(); + } + writeEndTag(); + } else { + writeStartTagObject(CURVE, false); + writeStartTagProperty(SEGMENTS); + for (int i = 0; i < geometry.getNumGeometries(); i++) { + SingleCurve curve = geometry.getValue().get(i); + curve.accept(encodeAsSegmentOrPatch.orElse(this)); + } + writeEndTag(); + writeEndTag(); } - writeEndTag(); - writeEndTag(); return null; } @Override public Void visit(CurvePolygon geometry) { - writeStartTagObject(POLYGON, false); + boolean useSurfaceAndCurve = options.contains(Options.USE_SURFACE_RING_CURVE); + if (useSurfaceAndCurve) { + writeStartTagObject(SURFACE, false); + writeStartTagProperty(PATCHES); + writeStartTagDataType(POLYGON_PATCH); + } else { + writeStartTagObject(POLYGON, false); + } for (int i = 0; i < geometry.getNumRings(); i++) { Curve ring = geometry.getValue().get(i); if (i == 0) { - writeStartTagProperty(EXTERIOR); + writeStartTagProperty(version != GmlVersion.GML21 ? EXTERIOR : OUTER_BOUNDARY_IS); } else { - writeStartTagProperty(INTERIOR); + writeStartTagProperty(version != GmlVersion.GML21 ? INTERIOR : INNER_BOUNDARY_IS); } writeStartTagObject(RING, true); - writeStartTagProperty(CURVE_MEMBER); - ring.accept(encodeAsEmbeddedGeometry.orElse(this)); + if (useSurfaceAndCurve && ring instanceof CompoundCurve compoundCurve) { + for (int j = 0; j < compoundCurve.getNumGeometries(); j++) { + SingleCurve segment = compoundCurve.getValue().get(j); + writeStartTagProperty(CURVE_MEMBER); + segment.accept(encodeAsEmbeddedGeometry.orElse(this)); + writeEndTag(); + } + } else { + writeStartTagProperty(CURVE_MEMBER); + ring.accept(encodeAsEmbeddedGeometry.orElse(this)); + writeEndTag(); + } + writeEndTag(); writeEndTag(); + } + if (useSurfaceAndCurve) { writeEndTag(); writeEndTag(); } diff --git a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy index ff5dc2088..bc5acd08a 100644 --- a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy +++ b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySpec.groovy @@ -414,6 +414,99 @@ class GeometrySpec extends Specification { gmlOut == "0.0 0.0 1.0 1.0 0.0 2.0 -1.0 -1.0 0.0 0.0" } + def 'LINESTRING XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = LineString.of(new double[]{0.0, 0.0, 1.0, 1.0, 2.0, 0.0}) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.0 2.0 0.0" + } + + def 'POLYGON XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = Polygon.of(List.of( + PositionList.of(Axes.XY, new double[]{0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0}) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0" + } + + def 'CURVEPOLYGON XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = CurvePolygon.of(List.of( + CircularString.of(PositionList.of(Axes.XY, new double[]{0.0, 0.0, 1.0, 1.0, 0.0, 2.0, -1.0, -1.0, 0.0, 0.0})) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.0 0.0 2.0 -1.0 -1.0 0.0 0.0" + } + + def 'CURVEPOLYGON with CompoundCurve ring XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = CurvePolygon.of(List.of( + CompoundCurve.of(List.of( + LineString.of(new double[]{0.0, 0.0, 1.0, 1.0}), + CircularString.of(PositionList.of(Axes.XY, new double[]{1.0, 1.0, 2.0, 0.0, 3.0, 1.0})), + LineString.of(new double[]{3.0, 1.0, 0.0, 0.0}) + )) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0 3.0 1.03.0 1.0 0.0 0.0" + } + + def 'COMPOUNDCURVE XY with USE_SURFACE_RING_CURVE'() { + given: + Geometry geometry = CompoundCurve.of(List.of( + LineString.of(new double[]{0.0, 0.0, 1.0, 1.0}), + CircularString.of(PositionList.of(Axes.XY, new double[]{1.0, 1.0, 2.0, 0.0, 3.0, 1.0})) + )) + + when: + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def gmlEncoder = new GeometryEncoderGml(xmlWriter, GmlVersion.GML32, Set.of(GeometryEncoderGml.Options.USE_SURFACE_RING_CURVE), Optional.of("gml"), Optional.empty(), List.of()) + geometry.accept(gmlEncoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut == "0.0 0.0 1.0 1.01.0 1.0 2.0 0.0 3.0 1.0" + } + def 'MULTICURVE XY'() { given: Geometry geometry = MultiCurve.of(List.of( From 52197ada578e08e5a3c05bb743d92383e6bd8dc8 Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Mon, 11 May 2026 07:40:24 +0000 Subject: [PATCH 4/9] GML geometry encoder: improve srsName mapping behavior --- .../gml/domain/GeometryEncoderGml.java | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java index 72482eb57..5753006fd 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/domain/GeometryEncoderGml.java @@ -8,6 +8,7 @@ package de.ii.xtraplatform.features.gml.domain; import com.google.common.collect.ImmutableSet; +import de.ii.xtraplatform.crs.domain.EpsgCrs; import de.ii.xtraplatform.crs.domain.OgcCrs; import de.ii.xtraplatform.geometries.domain.Axes; import de.ii.xtraplatform.geometries.domain.CircularString; @@ -33,6 +34,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; @@ -131,6 +133,7 @@ public enum Options { private final Optional encodeAsSegmentOrPatch; private final Optional encodeAsEmbeddedGeometry; private final GmlVersion version; + private final Function srsNameMapper; private int nextGmlId = 0; private String srsName; @@ -140,6 +143,7 @@ public GeometryEncoderGml(XMLStreamWriter xmlWriter) { this.gmlIdPrefix = "geom_"; this.options = Set.of(); this.precision = null; + this.srsNameMapper = EpsgCrs::toUriString; this.encodeAsSegmentOrPatch = Optional.of( new GeometryEncoderGml( @@ -148,7 +152,8 @@ public GeometryEncoderGml(XMLStreamWriter xmlWriter) { Set.of(Options.LINE_STRING_AS_SEGMENT, Options.POLYGON_AS_PATCH), this.gmlPrefix, Optional.empty(), - List.of())); + List.of(), + this.srsNameMapper)); this.encodeAsEmbeddedGeometry = Optional.of( new GeometryEncoderGml( @@ -157,7 +162,8 @@ public GeometryEncoderGml(XMLStreamWriter xmlWriter) { Set.of(), this.gmlPrefix, Optional.empty(), - List.of())); + List.of(), + this.srsNameMapper)); this.srsName = null; this.version = GmlVersion.GML32; } @@ -169,10 +175,22 @@ public GeometryEncoderGml( Optional gmlPrefix, Optional gmlIdPrefix, List precision) { + this(xmlWriter, version, options, gmlPrefix, gmlIdPrefix, precision, EpsgCrs::toUriString); + } + + public GeometryEncoderGml( + XMLStreamWriter xmlWriter, + GmlVersion version, + Set options, + Optional gmlPrefix, + Optional gmlIdPrefix, + List precision, + Function srsNameMapper) { this.xmlWriter = xmlWriter; this.gmlPrefix = gmlPrefix; this.gmlIdPrefix = gmlIdPrefix.orElse("geom_"); this.options = options; + this.srsNameMapper = srsNameMapper; this.encodeAsSegmentOrPatch = options.contains(Options.LINE_STRING_AS_SEGMENT) && options.contains(Options.POLYGON_AS_PATCH) @@ -187,7 +205,8 @@ public GeometryEncoderGml( .build(), gmlPrefix, Optional.of(this.gmlIdPrefix + "seg_"), - precision)); + precision, + srsNameMapper)); this.encodeAsEmbeddedGeometry = options.contains(Options.WITH_SRS_NAME) ? Optional.of( @@ -202,7 +221,8 @@ public GeometryEncoderGml( .build(), gmlPrefix, Optional.of(this.gmlIdPrefix + "embed_"), - precision)) + precision, + srsNameMapper)) : Optional.empty(); this.precision = precision.stream().anyMatch(v -> v > 0) @@ -216,13 +236,13 @@ public GeometryEncoderGml( public Optional initAndCheckGeometry(Geometry geometry) { if (srsName == null) { srsName = - geometry - .getCrs() - .orElse( - geometry.getAxes() == Axes.XY || geometry.getAxes() == Axes.XYM - ? OgcCrs.CRS84 - : OgcCrs.CRS84h) - .toUriString(); + srsNameMapper.apply( + geometry + .getCrs() + .orElse( + geometry.getAxes() == Axes.XY || geometry.getAxes() == Axes.XYM + ? OgcCrs.CRS84 + : OgcCrs.CRS84h)); } return Optional.empty(); From aa5cc2ade7491dc6d797cc21fdec702f7c18df6f Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Mon, 11 May 2026 07:40:32 +0000 Subject: [PATCH 5/9] GML geometry encoder: add srsName test --- .../domain/GeometrySrsNameMapperSpec.groovy | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy diff --git a/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy new file mode 100644 index 000000000..ceee40a85 --- /dev/null +++ b/xtraplatform-features-gml/src/test/groovy/de/ii/xtraplatform/features/gml/domain/GeometrySrsNameMapperSpec.groovy @@ -0,0 +1,89 @@ +/* + * 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.gml.domain + +import de.ii.xtraplatform.crs.domain.EpsgCrs +import de.ii.xtraplatform.geometries.domain.Geometry +import de.ii.xtraplatform.geometries.domain.Point +import spock.lang.Specification + +import javax.xml.stream.XMLOutputFactory + +class GeometrySrsNameMapperSpec extends Specification { + + def 'TEMPLATE mapper rewrites srsName for matching CRS'() { + given: + EpsgCrs etrs89Utm32 = EpsgCrs.of(25832) + Geometry geometry = Point.of(389000.0, 5705000.0).withCrs(etrs89Utm32) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def encoder = new GeometryEncoderGml( + xmlWriter, + GmlVersion.GML32, + Set.of(GeometryEncoderGml.Options.WITH_SRS_NAME), + Optional.of("gml"), + Optional.empty(), + List.of(), + { EpsgCrs crs -> etrs89Utm32 == crs ? 'urn:adv:crs:ETRS89_UTM32' : crs.toUriString() } as java.util.function.Function) + + when: + geometry.accept(encoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut.contains('srsName="urn:adv:crs:ETRS89_UTM32"') + } + + def 'TEMPLATE mapper falls back to OGC URI for unmapped CRS'() { + given: + EpsgCrs etrs89Utm32 = EpsgCrs.of(25832) + EpsgCrs wgs84Utm32n = EpsgCrs.of(32632) + Geometry geometry = Point.of(389000.0, 5705000.0).withCrs(wgs84Utm32n) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def encoder = new GeometryEncoderGml( + xmlWriter, + GmlVersion.GML32, + Set.of(GeometryEncoderGml.Options.WITH_SRS_NAME), + Optional.of("gml"), + Optional.empty(), + List.of(), + { EpsgCrs crs -> etrs89Utm32 == crs ? 'urn:adv:crs:ETRS89_UTM32' : crs.toUriString() } as java.util.function.Function) + + when: + geometry.accept(encoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut.contains('srsName="http://www.opengis.net/def/crs/EPSG/0/32632"') + } + + def 'OGC default constructor preserves toUriString behavior'() { + given: + Geometry geometry = Point.of(389000.0, 5705000.0).withCrs(EpsgCrs.of(25832)) + def sw = new StringWriter() + def xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(sw) + def encoder = new GeometryEncoderGml( + xmlWriter, + GmlVersion.GML32, + Set.of(GeometryEncoderGml.Options.WITH_SRS_NAME), + Optional.of("gml"), + Optional.empty(), + List.of()) + + when: + geometry.accept(encoder) + xmlWriter.flush() + String gmlOut = sw.toString() + + then: + gmlOut.contains('srsName="http://www.opengis.net/def/crs/EPSG/0/25832"') + } +} From 77a980ea1b3cfec9844426fb61a3fecd8e53c2c7 Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Tue, 12 May 2026 12:07:49 +0200 Subject: [PATCH 6/9] add alias field to feature schema for encoding-time property renaming Introduce an optional `alias` property on FeatureSchema as an identifier-style alternative property name (distinct from the display-oriented `label`). Encoders that opt in via a `useAlias` flag receive the alias as the property name; explicit `rename` transformations still take precedence. Plumbed via a new 3-arg getSchemaTransformations(mapping, inCollection, useAlias) overload and a useAlias-aware constructor on SchemaTransformerChain. The chain injects a synthetic rename to the alias only when no explicit (non-pathOnly) rename exists at the property's exact path; pathOnly renames do not suppress the alias. Avoids the duplication of large `rename` transformation maps in per-format building-block configurations when an application schema (e.g. AdV NAS) has both a short and a mnemonic name per property. --- .../features/domain/FeatureSchema.java | 18 +++ .../transform/PropertyTransformations.java | 7 +- .../transform/SchemaTransformerChain.java | 36 ++++++ .../SchemaTransformerChainAliasSpec.groovy | 110 ++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy 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..93cba3fe9 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 @@ -53,6 +53,7 @@ "geometryType", "objectType", "label", + "alias", "description", "unit", "format", @@ -214,6 +215,23 @@ default Type getType() { */ Optional getLabel(); + /** + * @langEn An alternative property name used by feature encodings that opt in to alias mode (for + * example, GML with `useAlias: true`). Unlike `label` (which is free-text for display), the + * alias must satisfy the encoding's identifier constraints (e.g. an XML element name or a + * JSON property name). When alias mode is active and an alias is set, the encoded property + * name is the alias instead of the schema name; an explicit `rename` transformation still + * takes precedence over the alias. + * @langDe Ein alternativer Eigenschaftsname, der von Feature-Kodierungen verwendet wird, die den + * Alias-Modus aktiviert haben (z.B. GML mit `useAlias: true`). Anders als `label` (ein freier + * Anzeigetext) muss der Alias den Identifier-Regeln der Kodierung entsprechen (z.B. + * XML-Elementname oder JSON-Eigenschaftsname). Bei aktivem Alias-Modus und gesetztem Alias + * wird der Alias anstelle des Schemanamens als Eigenschaftsname kodiert; eine explizite + * `rename`-Transformation hat weiterhin Vorrang vor dem Alias. + * @default null + */ + Optional getAlias(); + /** * @langEn Description for the schema object, used for example in HTML representations or JSON * Schema. diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java index 9154eacbf..2c48d33d8 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java @@ -141,7 +141,12 @@ public PropertyTransformations mergeInto(PropertyTransformations source) { default SchemaTransformerChain getSchemaTransformations( SchemaMapping schemaMapping, boolean inCollection) { - return new SchemaTransformerChain(getTransformations(), schemaMapping, inCollection); + return getSchemaTransformations(schemaMapping, inCollection, false); + } + + default SchemaTransformerChain getSchemaTransformations( + SchemaMapping schemaMapping, boolean inCollection, boolean useAlias) { + return new SchemaTransformerChain(getTransformations(), schemaMapping, inCollection, useAlias); } default TokenSliceTransformerChain getTokenSliceTransformations( diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java index 5fb56f036..c54aa89c7 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java @@ -28,11 +28,21 @@ public class SchemaTransformerChain private final List currentParentProperties; private final Map> transformers; + private final boolean useAlias; public SchemaTransformerChain( Map> allTransformations, SchemaMapping schemaMapping, boolean inCollection) { + this(allTransformations, schemaMapping, inCollection, false); + } + + public SchemaTransformerChain( + Map> allTransformations, + SchemaMapping schemaMapping, + boolean inCollection, + boolean useAlias) { + this.useAlias = useAlias; this.currentParentProperties = new ArrayList<>(); this.transformers = allTransformations.entrySet().stream() @@ -103,9 +113,35 @@ public FeatureSchema transform(String path, FeatureSchema schema) { transformed = run(transformers, path, path, schema); + if (useAlias + && transformed != null + && schema.getAlias().isPresent() + && !hasExplicitRename(path)) { + transformed = + ImmutableFeaturePropertyTransformerRename.builder() + .propertyPath(path) + .parameter(schema.getAlias().get()) + .build() + .transform(path, transformed); + } + return transformed; } + private boolean hasExplicitRename(String path) { + List atPath = transformers.get(path); + if (atPath == null) { + return false; + } + for (FeaturePropertySchemaTransformer t : atPath) { + if (t instanceof FeaturePropertyTransformerRename + && !((FeaturePropertyTransformerRename) t).pathOnly()) { + return true; + } + } + return false; + } + @Override public boolean has(String path) { return transformers.containsKey(path); diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy new file mode 100644 index 000000000..08ef31f6c --- /dev/null +++ b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy @@ -0,0 +1,110 @@ +/* + * 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.transform + +import de.ii.xtraplatform.features.domain.FeatureSchema +import de.ii.xtraplatform.features.domain.ImmutableFeatureSchema +import de.ii.xtraplatform.features.domain.SchemaBase +import de.ii.xtraplatform.features.domain.SchemaMapping +import spock.lang.Specification + +class SchemaTransformerChainAliasSpec extends Specification { + + static FeatureSchema property(String name, String alias = null) { + def b = new ImmutableFeatureSchema.Builder() + .name(name) + .type(SchemaBase.Type.STRING) + .sourcePath(name) + if (alias != null) { + b.alias(alias) + } + return b.build() + } + + static FeatureSchema feature(FeatureSchema... properties) { + def b = new ImmutableFeatureSchema.Builder() + .name("test") + .type(SchemaBase.Type.OBJECT) + .sourcePath("/test") + properties.each { b.putPropertyMap(it.getName(), it) } + return b.build() + } + + static String firstPropertyName(FeatureSchema schema) { + return schema.getProperties().get(0).getName() + } + + def "no alias: property name unchanged regardless of useAlias"() { + given: + def schema = feature(property("anl")) + def chain = new SchemaTransformerChain(Map.of(), SchemaMapping.of(schema), false, useAlias) + + when: + def transformed = schema.accept(chain) + + then: + firstPropertyName(transformed) == "anl" + + where: + useAlias << [true, false] + } + + def "alias present, useAlias=false: property name unchanged"() { + given: + def schema = feature(property("anl", "anlass")) + def chain = new SchemaTransformerChain(Map.of(), SchemaMapping.of(schema), false, false) + + when: + def transformed = schema.accept(chain) + + then: + firstPropertyName(transformed) == "anl" + } + + def "alias present, useAlias=true: property name becomes alias"() { + given: + def schema = feature(property("anl", "anlass")) + def chain = new SchemaTransformerChain(Map.of(), SchemaMapping.of(schema), false, true) + + when: + def transformed = schema.accept(chain) + + then: + firstPropertyName(transformed) == "anlass" + } + + def "alias present, useAlias=true, explicit rename at same path: rename wins"() { + given: + def schema = feature(property("anl", "anlass")) + def transformations = Map.of( + "anl", + List.of(new ImmutablePropertyTransformation.Builder().rename("custom").build())) + def chain = new SchemaTransformerChain(transformations, SchemaMapping.of(schema), false, true) + + when: + def transformed = schema.accept(chain) + + then: + firstPropertyName(transformed) == "custom" + } + + def "alias present, useAlias=true, renamePathOnly at same path: alias still wins on name"() { + given: + def schema = feature(property("anl", "anlass")) + def transformations = Map.of( + "anl", + List.of(new ImmutablePropertyTransformation.Builder().renamePathOnly("custom").build())) + def chain = new SchemaTransformerChain(transformations, SchemaMapping.of(schema), false, true) + + when: + def transformed = schema.accept(chain) + + then: + firstPropertyName(transformed) == "anlass" + } +} From c1c846b4d6bd91aa1e523ef9eff5143efbe9f18c Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Tue, 12 May 2026 12:42:17 +0200 Subject: [PATCH 7/9] plumb useAlias flag through encoding pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a default boolean useAlias() predicate on PropertyTransformations (defaults to false) and have FeatureTokenTransformerMappings forward that predicate to the 3-arg getSchemaTransformations() overload added in the previous commit. This is the encoding-side wiring for the alias mechanism: only the feature-encoding pipeline reads useAlias(). Schema-derivation paths (WithTransformationsApplied for queryables, sortables, JSON Schema documents, etc.) call the 2-arg getSchemaTransformations() and so continue to use schema names regardless of any format configuration's alias preference. When useAlias=true, walk the feature schema and inject each property's alias as an explicit rename into propertyTransformations before applyRename. This re-keys every downstream transformation (wrap for virtual objects, auto-DATETIME formatter, value transformers, …) by the aliased path, so schema lookups, SchemaTransformerChain, and TokenSliceTransformerChain all see consistent aliased paths. useAlias() is propagated through PropertyTransformations.mergeInto and withSubstitutions. --- .../features/domain/FeatureStreamImpl.java | 27 +++++++++++++++++ .../FeatureTokenTransformerMappings.java | 3 +- .../transform/PropertyTransformations.java | 29 ++++++++++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) 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..4a3b05149 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 @@ -329,9 +329,36 @@ static PropertyTransformations getPropertyTransformations( .map(p -> p.mergeInto(providerTransformations)) .orElse(providerTransformations); + if (merged.useAlias()) { + merged = injectAliasRenames(merged, featureSchemas.get(typeQuery.getType())); + } + return applyRename(merged); } + private static PropertyTransformations injectAliasRenames( + PropertyTransformations propertyTransformations, FeatureSchema schema) { + Map> aliasRenames = new LinkedHashMap<>(); + collectAliasRenames(schema, aliasRenames); + if (aliasRenames.isEmpty()) { + return propertyTransformations; + } + return propertyTransformations.mergeInto(() -> aliasRenames); + } + + private static void collectAliasRenames( + FeatureSchema schema, Map> aliasRenames) { + schema + .getAlias() + .filter(alias -> !schema.getFullPath().isEmpty()) + .ifPresent( + alias -> + aliasRenames.put( + schema.getFullPathAsString(), + List.of(new ImmutablePropertyTransformation.Builder().rename(alias).build()))); + schema.getProperties().forEach(child -> collectAliasRenames(child, aliasRenames)); + } + private static PropertyTransformations applyRename( PropertyTransformations propertyTransformations) { if (propertyTransformations.getTransformations().values().stream() 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 27049e2b4..755f8bb17 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 @@ -74,7 +74,8 @@ public void onStart(ModifiableContext context) { .getSchemaTransformations( entry.getValue(), (!(context.query() instanceof FeatureQuery) - || !((FeatureQuery) context.query()).returnsSingleFeature())))) + || !((FeatureQuery) context.query()).returnsSingleFeature()), + propertyTransformations.get(entry.getKey()).useAlias()))) .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue)); this.sliceTransformerChains = diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java index 2c48d33d8..328edfd9e 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java @@ -79,6 +79,7 @@ default Map> withTransformation( default PropertyTransformations withSubstitutions(Map substitutions) { Map> transformations = this.getTransformations(); + boolean useAlias = this.useAlias(); return new PropertyTransformations() { @Override @@ -86,6 +87,11 @@ public Map> getTransformations() { return transformations; } + @Override + public boolean useAlias() { + return useAlias; + } + @Override public TransformerChain getValueTransformations( SchemaMapping schemaMapping, Map codelists, ZoneId defaultTimeZone) { @@ -139,6 +145,16 @@ public PropertyTransformations mergeInto(PropertyTransformations source) { }; } + /** + * Whether the encoding pipeline should substitute the alias declared on each feature-schema + * property in place of its schema name. Overridden by ldproxy's {@code AliasConfiguration} when a + * format configuration opts in. Only the feature-encoding pipeline reads this flag; schema + * derivation paths for queryables, sortables, JSON Schema, etc. always use schema names. + */ + default boolean useAlias() { + return false; + } + default SchemaTransformerChain getSchemaTransformations( SchemaMapping schemaMapping, boolean inCollection) { return getSchemaTransformations(schemaMapping, inCollection, false); @@ -196,6 +212,17 @@ default PropertyTransformations mergeInto(PropertyTransformations source) { } }); - return () -> mergedTransformations; + boolean mergedUseAlias = useAlias() || source.useAlias(); + return new PropertyTransformations() { + @Override + public Map> getTransformations() { + return mergedTransformations; + } + + @Override + public boolean useAlias() { + return mergedUseAlias; + } + }; } } From 824af10786349597c054b12927a2c4ebd877a611 Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Wed, 13 May 2026 12:28:49 +0200 Subject: [PATCH 8/9] fix: cascade renames when rebuilding propertyTransformations keys applyRename rebuilt the propertyTransformations map keyed by the rename target. For a top-level rename (e.g. qag -> qualitaetsangaben) the single-element rename matched the renamed full path. For nested renames the key was only the leaf rename, not the cumulative renamed path - so the lookup at runtime (which uses the full target path) missed any transformation registered for the inner property. Two symptoms when nested renames were configured: 1. The auto-added DATETIME formatter (provider-level, keyed by the original full path) was re-keyed by just the leaf rename. A DATETIME leaf inside a renamed object came through as the raw JDBC string instead of ISO-8601. 2. Wrap transformers for virtual objects (intermediate OBJECT levels without sourcePath) were similarly mis-keyed. The token-slice wrap step did not fire for those intermediates, so OBJECT/OBJECT_END tokens were never synthesized and the renamed inner objects disappeared from the output. Both symptoms have the same root cause and the same fix: walk the parent chain and apply each segment's rename so the new key is the full renamed path. --- .../features/domain/FeatureStreamImpl.java | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) 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 4a3b05149..237bff3db 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 @@ -25,7 +25,6 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import java.time.ZoneId; import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -361,43 +360,54 @@ private static void collectAliasRenames( 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); + // Collect every rename keyed by its original full path. + Map renames = new LinkedHashMap<>(); + propertyTransformations + .getTransformations() + .forEach( + (key, value) -> + value.stream() + .filter(pt -> pt.getRename().isPresent()) + .map(pt -> pt.getRename().get()) + .findFirst() + .ifPresent(rename -> renames.put(key, rename))); + + if (renames.isEmpty()) { + return propertyTransformations; } - return propertyTransformations; + // Re-key every transformation by the cumulative renamed full path so that lookups + // by the renamed target path (e.g. "qualitaetsangaben.herkunft.gmd:processStep.gmd:dateTime") + // still find the right transformation (auto DATETIME formatter, value transformers, + // wrap transformers, ...). + Map> renamed = new LinkedHashMap<>(); + propertyTransformations + .getTransformations() + .forEach( + (key, value) -> { + String newKey = renameFullPath(key, renames); + renamed.put(newKey, value); + }); + + return propertyTransformations.mergeInto(() -> renamed); + } + + private static String renameFullPath(String path, Map renames) { + String[] segments = path.split("\\."); + StringBuilder result = new StringBuilder(); + StringBuilder running = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + if (i > 0) { + running.append("."); + } + running.append(segments[i]); + String renamedSegment = renames.getOrDefault(running.toString(), segments[i]); + if (i > 0) { + result.append("."); + } + result.append(renamedSegment); + } + return result.toString(); } private static Map> getProviderTransformations( From 581067a73b5c666e3a18024adf5a054d0cbdfa31 Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Wed, 13 May 2026 17:01:44 +0200 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20replace=20useAlias=20plumbing?= =?UTF-8?q?=20with=20a=20one-shot=20alias=E2=86=92rename=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move alias handling out of PropertyTransformations and SchemaTransformerChain. Aliases are now converted to explicit rename transformations by a single utility, FeatureSchemaAliases.injectAliasRenames, called by ldproxy at the format-extension boundary. The xtraplatform-spatial pipeline downstream of that point no longer knows about aliases — it just sees regular renames and reuses the existing rename cascade machinery for everything else (wrap transformers on virtual objects, auto-DATETIME formatters, value transformers, ...). Removed: - PropertyTransformations.useAlias() predicate and its propagation through mergeInto and withSubstitutions - 3-arg PropertyTransformations.getSchemaTransformations overload - SchemaTransformerChain.useAlias field, 4-arg constructor, in-chain alias-injection branch, and hasExplicitRename helper Added: - FeatureSchemaAliases public utility with injectAliasRenames(pt, schema) - FeatureSchemaAliasesSpec covering nested paths, no-aliases, existing transformations, and a feature-type alias being ignored The cross-module boundary now carries only PropertyTransformations; the useAlias signal stays inside ldproxy's AliasConfiguration where it belongs. --- .../features/domain/FeatureSchemaAliases.java | 43 ++++++ .../features/domain/FeatureStreamImpl.java | 27 ---- .../FeatureTokenTransformerMappings.java | 3 +- .../transform/PropertyTransformations.java | 36 +---- .../transform/SchemaTransformerChain.java | 36 ----- .../domain/FeatureSchemaAliasesSpec.groovy | 127 ++++++++++++++++++ .../SchemaTransformerChainAliasSpec.groovy | 110 --------------- 7 files changed, 173 insertions(+), 209 deletions(-) create mode 100644 xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java create mode 100644 xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy delete mode 100644 xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java new file mode 100644 index 000000000..dbd35535e --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchemaAliases.java @@ -0,0 +1,43 @@ +/* + * 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.transform.ImmutablePropertyTransformation; +import de.ii.xtraplatform.features.domain.transform.PropertyTransformation; +import de.ii.xtraplatform.features.domain.transform.PropertyTransformations; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class FeatureSchemaAliases { + + private FeatureSchemaAliases() {} + + public static PropertyTransformations injectAliasRenames( + PropertyTransformations propertyTransformations, FeatureSchema schema) { + Map> aliasRenames = new LinkedHashMap<>(); + collectAliasRenames(schema, aliasRenames); + if (aliasRenames.isEmpty()) { + return propertyTransformations; + } + return propertyTransformations.mergeInto(() -> aliasRenames); + } + + private static void collectAliasRenames( + FeatureSchema schema, Map> aliasRenames) { + schema + .getAlias() + .filter(alias -> !schema.getFullPath().isEmpty()) + .ifPresent( + alias -> + aliasRenames.put( + schema.getFullPathAsString(), + List.of(new ImmutablePropertyTransformation.Builder().rename(alias).build()))); + schema.getProperties().forEach(child -> collectAliasRenames(child, aliasRenames)); + } +} 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 237bff3db..8464b52ff 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 @@ -328,36 +328,9 @@ static PropertyTransformations getPropertyTransformations( .map(p -> p.mergeInto(providerTransformations)) .orElse(providerTransformations); - if (merged.useAlias()) { - merged = injectAliasRenames(merged, featureSchemas.get(typeQuery.getType())); - } - return applyRename(merged); } - private static PropertyTransformations injectAliasRenames( - PropertyTransformations propertyTransformations, FeatureSchema schema) { - Map> aliasRenames = new LinkedHashMap<>(); - collectAliasRenames(schema, aliasRenames); - if (aliasRenames.isEmpty()) { - return propertyTransformations; - } - return propertyTransformations.mergeInto(() -> aliasRenames); - } - - private static void collectAliasRenames( - FeatureSchema schema, Map> aliasRenames) { - schema - .getAlias() - .filter(alias -> !schema.getFullPath().isEmpty()) - .ifPresent( - alias -> - aliasRenames.put( - schema.getFullPathAsString(), - List.of(new ImmutablePropertyTransformation.Builder().rename(alias).build()))); - schema.getProperties().forEach(child -> collectAliasRenames(child, aliasRenames)); - } - private static PropertyTransformations applyRename( PropertyTransformations propertyTransformations) { // Collect every rename keyed by its original full path. 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 755f8bb17..27049e2b4 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 @@ -74,8 +74,7 @@ public void onStart(ModifiableContext context) { .getSchemaTransformations( entry.getValue(), (!(context.query() instanceof FeatureQuery) - || !((FeatureQuery) context.query()).returnsSingleFeature()), - propertyTransformations.get(entry.getKey()).useAlias()))) + || !((FeatureQuery) context.query()).returnsSingleFeature())))) .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue)); this.sliceTransformerChains = diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java index 328edfd9e..9154eacbf 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/PropertyTransformations.java @@ -79,7 +79,6 @@ default Map> withTransformation( default PropertyTransformations withSubstitutions(Map substitutions) { Map> transformations = this.getTransformations(); - boolean useAlias = this.useAlias(); return new PropertyTransformations() { @Override @@ -87,11 +86,6 @@ public Map> getTransformations() { return transformations; } - @Override - public boolean useAlias() { - return useAlias; - } - @Override public TransformerChain getValueTransformations( SchemaMapping schemaMapping, Map codelists, ZoneId defaultTimeZone) { @@ -145,24 +139,9 @@ public PropertyTransformations mergeInto(PropertyTransformations source) { }; } - /** - * Whether the encoding pipeline should substitute the alias declared on each feature-schema - * property in place of its schema name. Overridden by ldproxy's {@code AliasConfiguration} when a - * format configuration opts in. Only the feature-encoding pipeline reads this flag; schema - * derivation paths for queryables, sortables, JSON Schema, etc. always use schema names. - */ - default boolean useAlias() { - return false; - } - default SchemaTransformerChain getSchemaTransformations( SchemaMapping schemaMapping, boolean inCollection) { - return getSchemaTransformations(schemaMapping, inCollection, false); - } - - default SchemaTransformerChain getSchemaTransformations( - SchemaMapping schemaMapping, boolean inCollection, boolean useAlias) { - return new SchemaTransformerChain(getTransformations(), schemaMapping, inCollection, useAlias); + return new SchemaTransformerChain(getTransformations(), schemaMapping, inCollection); } default TokenSliceTransformerChain getTokenSliceTransformations( @@ -212,17 +191,6 @@ default PropertyTransformations mergeInto(PropertyTransformations source) { } }); - boolean mergedUseAlias = useAlias() || source.useAlias(); - return new PropertyTransformations() { - @Override - public Map> getTransformations() { - return mergedTransformations; - } - - @Override - public boolean useAlias() { - return mergedUseAlias; - } - }; + return () -> mergedTransformations; } } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java index c54aa89c7..5fb56f036 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChain.java @@ -28,21 +28,11 @@ public class SchemaTransformerChain private final List currentParentProperties; private final Map> transformers; - private final boolean useAlias; public SchemaTransformerChain( Map> allTransformations, SchemaMapping schemaMapping, boolean inCollection) { - this(allTransformations, schemaMapping, inCollection, false); - } - - public SchemaTransformerChain( - Map> allTransformations, - SchemaMapping schemaMapping, - boolean inCollection, - boolean useAlias) { - this.useAlias = useAlias; this.currentParentProperties = new ArrayList<>(); this.transformers = allTransformations.entrySet().stream() @@ -113,35 +103,9 @@ public FeatureSchema transform(String path, FeatureSchema schema) { transformed = run(transformers, path, path, schema); - if (useAlias - && transformed != null - && schema.getAlias().isPresent() - && !hasExplicitRename(path)) { - transformed = - ImmutableFeaturePropertyTransformerRename.builder() - .propertyPath(path) - .parameter(schema.getAlias().get()) - .build() - .transform(path, transformed); - } - return transformed; } - private boolean hasExplicitRename(String path) { - List atPath = transformers.get(path); - if (atPath == null) { - return false; - } - for (FeaturePropertySchemaTransformer t : atPath) { - if (t instanceof FeaturePropertyTransformerRename - && !((FeaturePropertyTransformerRename) t).pathOnly()) { - return true; - } - } - return false; - } - @Override public boolean has(String path) { return transformers.containsKey(path); diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy new file mode 100644 index 000000000..f79ed4d0d --- /dev/null +++ b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/FeatureSchemaAliasesSpec.groovy @@ -0,0 +1,127 @@ +/* + * 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.transform.PropertyTransformations +import spock.lang.Specification + +class FeatureSchemaAliasesSpec extends Specification { + + static FeatureSchema property(String name, String alias = null) { + def b = new ImmutableFeatureSchema.Builder() + .name(name) + .type(SchemaBase.Type.STRING) + .sourcePath(name) + if (alias != null) { + b.alias(alias) + } + return b.build() + } + + static FeatureSchema object(String name, String alias, FeatureSchema... children) { + def b = new ImmutableFeatureSchema.Builder() + .name(name) + .type(SchemaBase.Type.OBJECT) + if (alias != null) { + b.alias(alias) + } + children.each { b.putPropertyMap(it.getName(), it) } + return b.build() + } + + static FeatureSchema feature(FeatureSchema... properties) { + def b = new ImmutableFeatureSchema.Builder() + .name("test") + .type(SchemaBase.Type.OBJECT) + .sourcePath("/test") + properties.each { b.putPropertyMap(it.getName(), it) } + return b.build() + } + + static PropertyTransformations base(Map transformations = [:]) { + return { -> transformations as Map } as PropertyTransformations + } + + static String renameAt(PropertyTransformations pt, String path) { + def entries = pt.transformations.get(path) + return entries == null ? null : entries.find { it.rename.present }?.rename?.orElse(null) + } + + def "schema with no aliases: transformations map unchanged"() { + given: + def schema = feature(property("anl")) + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + result.is(input) + } + + def "property alias: rename entry is added at the property's full path"() { + given: + def schema = feature(property("anl", "anlass")) + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + renameAt(result, "anl") == "anlass" + } + + def "nested aliases: rename entries are added at each level's full path"() { + given: + def schema = feature( + object("qag", "qualitaetsangaben", + object("dpl", "herkunft", + property("prs", "gmd:processStep")))) + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + renameAt(result, "qag") == "qualitaetsangaben" + renameAt(result, "qag.dpl") == "herkunft" + renameAt(result, "qag.dpl.prs") == "gmd:processStep" + } + + def "existing transformations are preserved alongside injected aliases"() { + given: + def schema = feature(property("anl", "anlass")) + def input = base(["other.path": []]) + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + result.transformations.containsKey("other.path") + renameAt(result, "anl") == "anlass" + } + + def "feature type's own alias is not injected (only properties)"() { + given: + def schema = new ImmutableFeatureSchema.Builder() + .name("test") + .type(SchemaBase.Type.OBJECT) + .sourcePath("/test") + .alias("ignored") + .putPropertyMap("anl", property("anl", "anlass")) + .build() + def input = base() + + when: + def result = FeatureSchemaAliases.injectAliasRenames(input, schema) + + then: + !result.transformations.containsKey("") + renameAt(result, "anl") == "anlass" + } +} diff --git a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy b/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy deleted file mode 100644 index 08ef31f6c..000000000 --- a/xtraplatform-features/src/test/groovy/de/ii/xtraplatform/features/domain/transform/SchemaTransformerChainAliasSpec.groovy +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.transform - -import de.ii.xtraplatform.features.domain.FeatureSchema -import de.ii.xtraplatform.features.domain.ImmutableFeatureSchema -import de.ii.xtraplatform.features.domain.SchemaBase -import de.ii.xtraplatform.features.domain.SchemaMapping -import spock.lang.Specification - -class SchemaTransformerChainAliasSpec extends Specification { - - static FeatureSchema property(String name, String alias = null) { - def b = new ImmutableFeatureSchema.Builder() - .name(name) - .type(SchemaBase.Type.STRING) - .sourcePath(name) - if (alias != null) { - b.alias(alias) - } - return b.build() - } - - static FeatureSchema feature(FeatureSchema... properties) { - def b = new ImmutableFeatureSchema.Builder() - .name("test") - .type(SchemaBase.Type.OBJECT) - .sourcePath("/test") - properties.each { b.putPropertyMap(it.getName(), it) } - return b.build() - } - - static String firstPropertyName(FeatureSchema schema) { - return schema.getProperties().get(0).getName() - } - - def "no alias: property name unchanged regardless of useAlias"() { - given: - def schema = feature(property("anl")) - def chain = new SchemaTransformerChain(Map.of(), SchemaMapping.of(schema), false, useAlias) - - when: - def transformed = schema.accept(chain) - - then: - firstPropertyName(transformed) == "anl" - - where: - useAlias << [true, false] - } - - def "alias present, useAlias=false: property name unchanged"() { - given: - def schema = feature(property("anl", "anlass")) - def chain = new SchemaTransformerChain(Map.of(), SchemaMapping.of(schema), false, false) - - when: - def transformed = schema.accept(chain) - - then: - firstPropertyName(transformed) == "anl" - } - - def "alias present, useAlias=true: property name becomes alias"() { - given: - def schema = feature(property("anl", "anlass")) - def chain = new SchemaTransformerChain(Map.of(), SchemaMapping.of(schema), false, true) - - when: - def transformed = schema.accept(chain) - - then: - firstPropertyName(transformed) == "anlass" - } - - def "alias present, useAlias=true, explicit rename at same path: rename wins"() { - given: - def schema = feature(property("anl", "anlass")) - def transformations = Map.of( - "anl", - List.of(new ImmutablePropertyTransformation.Builder().rename("custom").build())) - def chain = new SchemaTransformerChain(transformations, SchemaMapping.of(schema), false, true) - - when: - def transformed = schema.accept(chain) - - then: - firstPropertyName(transformed) == "custom" - } - - def "alias present, useAlias=true, renamePathOnly at same path: alias still wins on name"() { - given: - def schema = feature(property("anl", "anlass")) - def transformations = Map.of( - "anl", - List.of(new ImmutablePropertyTransformation.Builder().renamePathOnly("custom").build())) - def chain = new SchemaTransformerChain(transformations, SchemaMapping.of(schema), false, true) - - when: - def transformed = schema.accept(chain) - - then: - firstPropertyName(transformed) == "anlass" - } -}