From 2cd97ee5778555c1897774ef80a776c062b4fdb4 Mon Sep 17 00:00:00 2001 From: Felix Reichenbach <felix.reichenbach@mgm-tp.com> Date: Wed, 26 Mar 2025 15:49:36 +0100 Subject: [PATCH] OZG-7609 handle target path to list element --- .../transformation/AggregationMapping.java | 13 +- .../JSLTransformationService.java | 48 +++- .../AggregationMappingTestFactory.java | 2 +- .../FieldMappingTestFactory.java | 2 +- .../transformation/JSLTServiceITCase.java | 3 +- .../JSLTransformationServiceTest.java | 269 ++++++++++++++++++ 6 files changed, 316 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java b/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java index 9e6548d..d98c235 100644 --- a/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java +++ b/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java @@ -23,31 +23,32 @@ */ package de.ozgcloud.aggregation.transformation; -import java.util.ArrayList; import java.util.List; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; +import lombok.Singular; import lombok.ToString; @Getter -@Setter +@Builder @ToString public class AggregationMapping { private FormIdentifier formIdentifier; - private List<FieldMapping> fieldMappings = new ArrayList<>(); + @Singular + private List<FieldMapping> fieldMappings; @Getter - @Setter + @Builder public static class FormIdentifier { private String formEngineName; private String formId; } @Getter - @Setter + @Builder public static class FieldMapping { private String sourcePath; private String targetPath; diff --git a/src/main/java/de/ozgcloud/aggregation/transformation/JSLTransformationService.java b/src/main/java/de/ozgcloud/aggregation/transformation/JSLTransformationService.java index f5254de..819e415 100644 --- a/src/main/java/de/ozgcloud/aggregation/transformation/JSLTransformationService.java +++ b/src/main/java/de/ozgcloud/aggregation/transformation/JSLTransformationService.java @@ -27,15 +27,20 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.TextNode; import com.schibsted.spt.data.jslt.Expression; import com.schibsted.spt.data.jslt.Function; import com.schibsted.spt.data.jslt.filters.DefaultJsonFilter; +import com.schibsted.spt.data.jslt.impl.AbstractNode; +import com.schibsted.spt.data.jslt.impl.ArraySlicer; import com.schibsted.spt.data.jslt.impl.DotExpression; import com.schibsted.spt.data.jslt.impl.ExpressionImpl; import com.schibsted.spt.data.jslt.impl.ExpressionNode; @@ -55,8 +60,9 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class JSLTransformationService implements TransformationService { - private static final LetExpression[] EMPTY_TEMPLATES = new LetExpression[] {}; - private static final Map<String, Function> EMPTY_FUNCTIONS = Map.of(); + private static final Pattern LIST_ELEMENT_PATTERN = Pattern.compile("(.+)\\[(\\d+)\\]"); + static final LetExpression[] EMPTY_TEMPLATES = new LetExpression[] {}; + static final Map<String, Function> EMPTY_FUNCTIONS = Map.of(); private final ObjectMapper objectMapper; private final VorgangMapper vorgangMapper; @@ -86,29 +92,47 @@ public class JSLTransformationService implements TransformationService { return new ExpressionImpl(EMPTY_TEMPLATES, EMPTY_FUNCTIONS, buildObjectExpression(pairExpressions)); } - private ObjectExpression buildObjectExpression(PairExpression... fields) { + Optional<PairExpression> toPairExpression(Map.Entry<String, String> mapping) { + return transformToDotExpression(mapping.getValue()).map(path -> buildPairExpression(mapping.getKey(), path)); + } + + ObjectExpression buildObjectExpression(PairExpression... fields) { return new ObjectExpression(EMPTY_TEMPLATES, fields, null, null, new DefaultJsonFilter()); } - private Optional<PairExpression> toPairExpression(Map.Entry<String, String> mapping) { - return transformToDotExpression(mapping.getValue()).map(path -> buildPairExpression(mapping.getKey(), path)); + PairExpression buildPairExpression(String key, ExpressionNode expression) { + return new PairExpression(new LiteralExpression(new TextNode(key), null), expression, null); } - Optional<DotExpression> transformToDotExpression(String path) { + Optional<AbstractNode> transformToDotExpression(String path) { if (path == null) { return Optional.empty(); } else { - String[] fields = path.split("\\."); - DotExpression current = null; + var fields = path.split("\\."); + AbstractNode current = null; for (var field : fields) { - current = new DotExpression(field, current, null); + var listElementMatcher = LIST_ELEMENT_PATTERN.matcher(field); + if (listElementMatcher.matches()) { + current = getNodeForListElement(current, listElementMatcher); + } else { + current = new DotExpression(field, current, null); + } } - return Optional.ofNullable(current); + return Optional.of(current); } } - private PairExpression buildPairExpression(String key, ExpressionNode expression) { - return new PairExpression(new LiteralExpression(new TextNode(key), null), expression, null); + private AbstractNode getNodeForListElement(AbstractNode current, Matcher arrayMatcher) { + var index = Integer.parseInt(arrayMatcher.group(2)); + var name = arrayMatcher.group(1); + current = slicer(current, name, index); + return current; + } + + private ArraySlicer slicer(AbstractNode parent, String field, int index) { + var idx = new IntNode(index); + var node = new DotExpression(field, parent, null); + return new ArraySlicer(new LiteralExpression(idx, null), false, null, node, null); } } diff --git a/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java b/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java index a510e05..4be105f 100644 --- a/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java +++ b/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java @@ -39,6 +39,6 @@ public class AggregationMappingTestFactory { public static AggregationMappingBuilder createBuilder() { return AggregationMapping.builder() .formIdentifier(FORM_IDENTIFIER) - .mapping(MAPPING); + .fieldMapping(MAPPING); } } diff --git a/src/test/java/de/ozgcloud/aggregation/transformation/FieldMappingTestFactory.java b/src/test/java/de/ozgcloud/aggregation/transformation/FieldMappingTestFactory.java index e0ba4b0..fba8b2f 100644 --- a/src/test/java/de/ozgcloud/aggregation/transformation/FieldMappingTestFactory.java +++ b/src/test/java/de/ozgcloud/aggregation/transformation/FieldMappingTestFactory.java @@ -46,7 +46,7 @@ public class FieldMappingTestFactory { } public static Map<String, String> createAsMap() { - return Map.of(SOURCE_PATH, TARGET_PATH); + return Map.of(TARGET_PATH, SOURCE_PATH); } } diff --git a/src/test/java/de/ozgcloud/aggregation/transformation/JSLTServiceITCase.java b/src/test/java/de/ozgcloud/aggregation/transformation/JSLTServiceITCase.java index 8e3c1f1..007bf32 100644 --- a/src/test/java/de/ozgcloud/aggregation/transformation/JSLTServiceITCase.java +++ b/src/test/java/de/ozgcloud/aggregation/transformation/JSLTServiceITCase.java @@ -61,7 +61,8 @@ class JSLTServiceITCase { .build()); var service = new JSLTransformationService(objectMapper, vorgangMapper); - var transformation = service.load(null, AggregationMappingTestFactory.createBuilder().clearMappings().mappings(fieldMappings).build()); + var transformation = service.load(null, + AggregationMappingTestFactory.createBuilder().clearFieldMappings().fieldMappings(fieldMappings).build()); var vorgang = OzgCloudVorgangTestFactory.create(); diff --git a/src/test/java/de/ozgcloud/aggregation/transformation/JSLTransformationServiceTest.java b/src/test/java/de/ozgcloud/aggregation/transformation/JSLTransformationServiceTest.java index 28cc8d0..1fa74fd 100644 --- a/src/test/java/de/ozgcloud/aggregation/transformation/JSLTransformationServiceTest.java +++ b/src/test/java/de/ozgcloud/aggregation/transformation/JSLTransformationServiceTest.java @@ -39,12 +39,23 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.schibsted.spt.data.jslt.Expression; import com.schibsted.spt.data.jslt.impl.DotExpression; +import com.schibsted.spt.data.jslt.impl.ExpressionImpl; +import com.schibsted.spt.data.jslt.impl.ObjectExpression; +import com.schibsted.spt.data.jslt.impl.PairExpression; +import com.schibsted.spt.data.jslt.impl.Scope; import com.thedeanda.lorem.LoremIpsum; import de.ozgcloud.aggregation.transformation.AggregationMapping.FieldMapping; +import de.ozgcloud.apilib.vorgang.OzgCloudEingangHeaderTestFactory; +import de.ozgcloud.apilib.vorgang.OzgCloudVorgangEingangTestFactory; +import de.ozgcloud.apilib.vorgang.OzgCloudVorgangHeaderTestFactory; +import de.ozgcloud.apilib.vorgang.OzgCloudVorgangTestFactory; class JSLTransformationServiceTest { @@ -170,4 +181,262 @@ class JSLTransformationServiceTest { assertThat(map).containsExactlyInAnyOrderEntriesOf(expectedMap); } } + + @Nested + class TestCreateExpression { + + @Test + void shouldCallToPairExpression() { + createExpression(); + + verify(service).toPairExpression(Map.entry(FieldMappingTestFactory.TARGET_PATH, FieldMappingTestFactory.SOURCE_PATH)); + } + + @Test + void shouldMapProperty() { + + var expression = service.createExpression(Map.of("target", "vorgangName")); + + var mapped = expression.apply(getVorgangTree()); + assertThat(mapped).hasToString("{\"target\":\"%s\"}".formatted(OzgCloudVorgangTestFactory.VORGANG_NAME)); + } + + private JsonNode getVorgangTree() { + var mapper = new ObjectMapper().registerModules(new JavaTimeModule()); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); + var vorgangTree = mapper.valueToTree(OzgCloudVorgangTestFactory.create()); + return vorgangTree; + } + + @Nested + class TestOnPairExpressionPresent { + + @Mock + private PairExpression pairExpression; + @Mock + private ObjectExpression objectExpression; + + @BeforeEach + void mock() { + doReturn(Optional.of(pairExpression)).when(service).toPairExpression(any()); + doReturn(objectExpression).when(service).buildObjectExpression(any()); + } + + @Test + void shouldBuildObjectExpression() { + createExpression(); + + verify(service).buildObjectExpression(new PairExpression[] { pairExpression }); + } + + @Test + void shouldReturnObjectExpression() { + var expression = createExpression(); + + assertThat(expression).usingRecursiveComparison().isEqualTo( + new ExpressionImpl(JSLTransformationService.EMPTY_TEMPLATES, JSLTransformationService.EMPTY_FUNCTIONS, objectExpression)); + } + } + + @Nested + class TestOnPairExpressionNotPresent { + + @Mock + private ObjectExpression objectExpression; + + @BeforeEach + void mock() { + doReturn(Optional.empty()).when(service).toPairExpression(any()); + doReturn(objectExpression).when(service).buildObjectExpression(); + } + + @Test + void shouldBuildObjectExpression() { + createExpression(); + + verify(service).buildObjectExpression(); + } + + @Test + void shouldReturnObjectExpression() { + var expression = createExpression(); + expression.apply(null); + assertThat(expression).usingRecursiveComparison().isEqualTo( + new ExpressionImpl(JSLTransformationService.EMPTY_TEMPLATES, JSLTransformationService.EMPTY_FUNCTIONS, objectExpression)); + } + } + + private Expression createExpression() { + return service.createExpression(FieldMappingTestFactory.createAsMap()); + } + } + + @Nested + class TestToPairExpression { + + @Test + void shouldMapProperty() { + var expression = service.transformToDotExpression("vorgangName"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangTestFactory.VORGANG_NAME)); + } + + @Test + void shouldMapNestedProperty() { + + var expression = service.transformToDotExpression("header.aktenzeichen"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangHeaderTestFactory.AKTENZEICHEN)); + } + + @Test + void shouldMapListElement() { + + var expression = service.transformToDotExpression("eingangs[0]"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + + assertThat(mapped).hasToString(getEingangTree().toString()); + } + + @Test + void shouldMapListElementProperty() { + + var expression = service.transformToDotExpression("eingangs[0].header.formEngineName"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudEingangHeaderTestFactory.FORM_ENGINE_NAME)); + } + + private JsonNode getVorgangTree() { + var mapper = new ObjectMapper().registerModules(new JavaTimeModule()); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); + return mapper.valueToTree(OzgCloudVorgangTestFactory.create()); + } + + private JsonNode getEingangTree() { + var mapper = new ObjectMapper().registerModules(new JavaTimeModule()); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); + return mapper.valueToTree(OzgCloudVorgangEingangTestFactory.create()); + } + + @Nested + class TestOnDotExpressionPresent { + + @Mock + private DotExpression dotExpression; + @Mock + private PairExpression pairExpression; + + @BeforeEach + void mock() { + doReturn(Optional.of(dotExpression)).when(service).transformToDotExpression(any()); + doReturn(pairExpression).when(service).buildPairExpression(any(), any()); + } + + @Test + void shouldBuildPairExpression() { + toPairExpression(); + + verify(service).buildPairExpression(FieldMappingTestFactory.SOURCE_PATH, + dotExpression); + } + + @Test + void shouldReturnPairExpression() { + var returnedPairExpression = toPairExpression(); + + assertThat(returnedPairExpression).contains(pairExpression); + } + } + + @Nested + class TestOnDotExpressionNotPresent { + + @BeforeEach + void mock() { + doReturn(Optional.empty()).when(service).transformToDotExpression(any()); + } + + @Test + void shouldReturnEmpty() { + var returnedPairExpression = toPairExpression(); + + assertThat(returnedPairExpression).isEmpty(); + } + } + + private Optional<PairExpression> toPairExpression() { + return service.toPairExpression(Map.entry(FieldMappingTestFactory.SOURCE_PATH, + FieldMappingTestFactory.TARGET_PATH)); + } + } + + @Nested + class TestTransformToDotExpression { + + @Test + void shouldReturnEmptyOnNullPath() { + var dotExpression = service.transformToDotExpression(null); + + assertThat(dotExpression).isEmpty(); + } + + @Test + void shouldMapProperty() { + var expression = service.transformToDotExpression("vorgangName"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangTestFactory.VORGANG_NAME)); + } + + @Test + void shouldMapNestedProperty() { + + var expression = service.transformToDotExpression("header.aktenzeichen"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangHeaderTestFactory.AKTENZEICHEN)); + } + + @Test + void shouldMapListElement() { + + var expression = service.transformToDotExpression("eingangs[0]"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + + assertThat(mapped).hasToString(getEingangTree().toString()); + } + + @Test + void shouldMapListElementProperty() { + + var expression = service.transformToDotExpression("eingangs[0].header.formEngineName"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudEingangHeaderTestFactory.FORM_ENGINE_NAME)); + } + + private JsonNode getVorgangTree() { + var mapper = new ObjectMapper().registerModules(new JavaTimeModule()); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); + return mapper.valueToTree(OzgCloudVorgangTestFactory.create()); + } + + private JsonNode getEingangTree() { + var mapper = new ObjectMapper().registerModules(new JavaTimeModule()); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); + return mapper.valueToTree(OzgCloudVorgangEingangTestFactory.create()); + } + } } -- GitLab