diff --git a/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java b/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java index e5f01dca7ed7361496a2e3ba39d526190709c83c..b639b4bd91893f94bef366a4f16122a7a8265103 100644 --- a/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java +++ b/src/main/java/de/ozgcloud/aggregation/TransformationProperties.java @@ -26,14 +26,18 @@ package de.ozgcloud.aggregation; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; import de.ozgcloud.aggregation.transformation.AggregationMapping; import lombok.Getter; import lombok.Setter; +import lombok.extern.log4j.Log4j2; @ConfigurationProperties(prefix = "ozgcloud.aggregation") +@Configuration @Getter @Setter +@Log4j2 public class TransformationProperties { /* diff --git a/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java b/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java index 74a0c54bbbd6724854a2bf9b2f2d5857950f335e..d98c2350ef04f3920dc8d30f8ea4100236991d65 100644 --- a/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java +++ b/src/main/java/de/ozgcloud/aggregation/transformation/AggregationMapping.java @@ -28,26 +28,28 @@ import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.Singular; +import lombok.ToString; -@Builder @Getter +@Builder +@ToString public class AggregationMapping { private FormIdentifier formIdentifier; @Singular - private List<FieldMapping> mappings; + private List<FieldMapping> fieldMappings; - @Builder @Getter - static class FormIdentifier { + @Builder + public static class FormIdentifier { private String formEngineName; private String formId; } - @Builder @Getter - static class FieldMapping { + @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 51a81b9f543d4beae28614c8e8f969a61ed26a6f..b835dc0aaa6e553dcb9b90fe9fa371e134676658 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; @@ -66,13 +72,13 @@ public class JSLTransformationService implements TransformationService { return JSLTransformation.builder() .vorgangMapper(vorgangMapper) .objectMapper(objectMapper) - .identifier(transformToDotExpression(identifier)) + .identifier(transformToAbstractNode(identifier)) .script(createExpression(transformToMap(getFieldMappings(mapping)))) .build(); } private List<FieldMapping> getFieldMappings(AggregationMapping mapping) { - return Optional.ofNullable(mapping).map(AggregationMapping::getMappings).orElse(Collections.emptyList()); + return Optional.ofNullable(mapping).map(AggregationMapping::getFieldMappings).orElse(Collections.emptyList()); } Map<String, String> transformToMap(List<FieldMapping> mappings) { @@ -86,29 +92,46 @@ 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 transformToAbstractNode(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> transformToAbstractNode(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); + return slicer(current, name, index); + } + + 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/main/resources/application-local.yml b/src/main/resources/application-local.yml index 28eabe297a80efb78b6a30da9274e875c39c3212..5757dad77b6cf41d257b0604abf036ac0cd2db2e 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,16 +1,17 @@ logging: level: ROOT: WARN - '[de.ozgcloud]': DEBUG, + '[de.ozgcloud]': DEBUG + '[org.springframework.cloud.config]': DEBUG '[org.springframework.security]': WARN config: classpath:log4j2-local.xml spring: - data: - mongodb: - host: localhost - port: 27017 - database: aggregation-manager + data: + mongodb: + host: localhost + port: 27017 + database: aggregation-manager ozgcloud: vorgang-manager: @@ -19,3 +20,4 @@ ozgcloud: aggregation-manager: fetching-batch-size: 5 + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a3b81b429aec9ac6029ea59b458a0baee244b83d..10ba491e28a6e9b6f87c6acb015035784a31d3fb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -36,6 +36,8 @@ grpc: spring: config: import: optional:configserver:${ozgcloud_administration_address}/configserver/ + application: + name: OzgCloud_AggregationManager aggregation-manager: fetching-batch-size: 100 diff --git a/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java b/src/test/java/de/ozgcloud/aggregation/transformation/AggregationMappingTestFactory.java index a510e056c94044cbacaafce9d0041bfc24852a2c..4be105f1284cafaef138f184ae54625242ad50e5 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 e0ba4b0cd9a3af725061606836c64d3685b019f7..fba8b2f97625e7d60bb2f68e0331c93cbda75f64 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 8e3c1f1fa652e569e26e2e5cd34d8d3b629ae8ce..007bf3210a52d35a049e35bc035de572c08034ad 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 28cc8d0040d093fefaa02700f90a6617fcca950d..3b4f811b7393192983c658016c4f10d4a49b98e5 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 { @@ -73,7 +84,7 @@ class JSLTransformationServiceTest { void mock() { doReturn(mapping).when(service).transformToMap(any()); doReturn(expression).when(service).createExpression(any()); - doReturn(identifierExpression).when(service).transformToDotExpression(any()); + doReturn(identifierExpression).when(service).transformToAbstractNode(any()); } @Test @@ -115,7 +126,7 @@ class JSLTransformationServiceTest { void mock() { doReturn(mapping).when(service).transformToMap(any()); doReturn(expression).when(service).createExpression(any()); - doReturn(identifierExpression).when(service).transformToDotExpression(any()); + doReturn(identifierExpression).when(service).transformToAbstractNode(any()); } @Test @@ -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.transformToAbstractNode("vorgangName"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangTestFactory.VORGANG_NAME)); + } + + @Test + void shouldMapNestedProperty() { + + var expression = service.transformToAbstractNode("header.aktenzeichen"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangHeaderTestFactory.AKTENZEICHEN)); + } + + @Test + void shouldMapListElement() { + + var expression = service.transformToAbstractNode("eingangs[0]"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + + assertThat(mapped).hasToString(getEingangTree().toString()); + } + + @Test + void shouldMapListElementProperty() { + + var expression = service.transformToAbstractNode("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).transformToAbstractNode(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).transformToAbstractNode(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 TestTransformToAbstractNode { + + @Test + void shouldReturnEmptyOnNullPath() { + var dotExpression = service.transformToAbstractNode(null); + + assertThat(dotExpression).isEmpty(); + } + + @Test + void shouldMapProperty() { + var expression = service.transformToAbstractNode("vorgangName"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangTestFactory.VORGANG_NAME)); + } + + @Test + void shouldMapNestedProperty() { + + var expression = service.transformToAbstractNode("header.aktenzeichen"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + assertThat(mapped).hasToString("\"%s\"".formatted(OzgCloudVorgangHeaderTestFactory.AKTENZEICHEN)); + } + + @Test + void shouldMapListElement() { + + var expression = service.transformToAbstractNode("eingangs[0]"); + + var mapped = expression.get().apply(Scope.getRoot(2), getVorgangTree()); + + assertThat(mapped).hasToString(getEingangTree().toString()); + } + + @Test + void shouldMapListElementProperty() { + + var expression = service.transformToAbstractNode("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()); + } + } }