> dummySources() {
+ return Arrays.asList(new LabeledSource<>("a", 10), new LabeledSource<>("b", 20));
+ }
+
+ private void assertBlocked(String expression) {
+ NashornFieldDefinition fieldDef =
+ NashornFieldDefinition.builder()
+ .expression(expression)
+ .defaultValue(BLOCKED_VALUE)
+ .build();
+
+ Object result = strategy.merge(dummySources(), fieldDef, "test");
+ assertEquals(
+ BLOCKED_VALUE,
+ result,
+ "Expression SHOULD have failed/blocked but didn't: " + expression);
+ }
+
+ private void assertAllowed(String expression, Object expectedValue) {
+ NashornFieldDefinition fieldDef =
+ NashornFieldDefinition.builder()
+ .expression(expression)
+ .defaultValue(BLOCKED_VALUE)
+ .build();
+
+ Object result = strategy.merge(dummySources(), fieldDef, "test");
+ assertEquals(expectedValue, result, "Expression SHOULD have succeeded: " + expression);
+ }
+
+ @Nested
+ @DisplayName("Legitimate expressions")
+ class LegitimateExpressionsAreAllowed {
+ @Test
+ void arithmetic() {
+ // Nashorn returns Integer for simple int math usually.
+ assertAllowed("1 + 1", 2);
+ }
+ }
+
+ @Nested
+ @DisplayName("Java Type Access must be blocked")
+ class JavaAccessBlocked {
+ @Test
+ void blocksJavaType() {
+ assertBlocked("Java.type('java.lang.System')");
+ }
+
+ @Test
+ void blocksPackages() {
+ assertBlocked("Packages.java.lang.System");
+ }
+
+ @Test
+ void blocksFullyQualified() {
+ assertBlocked("java.lang.System.exit(0)");
+ }
+ }
+
+ @Nested
+ @DisplayName("Reflection/Class access must be blocked")
+ class ReflectionBlocked {
+ @Test
+ void blocksGetClass() {
+ assertBlocked("sources.getClass().forName('java.lang.Runtime')");
+ }
+
+ @Test
+ void blocksGetClassLoader() {
+ assertBlocked("sources.class.getClassLoader()");
+ }
+ }
+
+ @Nested
+ @DisplayName("File/Network (IO) must be blocked")
+ class IOBlocked {
+ @Test
+ void blocksFile() {
+ assertBlocked("new java.io.File('/etc/passwd').exists()");
+ }
+
+ @Test
+ void blocksUrl() {
+ assertBlocked("new java.net.URL('http://google.com').openStream()");
+ }
+ }
+
+ @Nested
+ @DisplayName("System/Runtime must be blocked")
+ class SystemBlocked {
+ @Test
+ void blocksSystemExit() {
+ assertBlocked("java.lang.System.exit(0)");
+ }
+
+ @Test
+ void blocksRuntime() {
+ assertBlocked("java.lang.Runtime.getRuntime().exec('ls')");
+ }
+
+ @Test
+ void blocksSystemProperties() {
+ assertBlocked("java.lang.System.getProperty('user.home')");
+ }
+ }
+}
diff --git a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/controller/MergeController.java b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/controller/MergeController.java
index 645fb2f..df1808a 100644
--- a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/controller/MergeController.java
+++ b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/controller/MergeController.java
@@ -5,12 +5,12 @@
import de.x132.objectmerger.dto.MergeRequest;
import de.x132.objectmerger.generator.MergeDefinitionGenerator;
import de.x132.objectmerger.security.ClassLoadingGuard;
+import de.x132.objectmerger.service.ExampleMergeService;
import de.x132.objectmerger.service.ObjectMergerService;
import de.x132.objectmerger.util.MergeDefinitionConverter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
@@ -21,6 +21,12 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+/**
+ * Controller exposing REST endpoints for merging data sources and generating definitions.
+ *
+ * Conforms to the Single Responsibility Principle by delegating mock example execution to the
+ * {@link ExampleMergeService} and using Java Records for incoming request DTOs.
+ */
@Slf4j
@RestController
@RequestMapping("/api/v1/merge")
@@ -31,10 +37,22 @@ public class MergeController {
private final ObjectMergerService mergerService;
private final ClassLoadingGuard classLoadingGuard;
-
- public MergeController(ObjectMergerService mergerService, ClassLoadingGuard classLoadingGuard) {
+ private final ExampleMergeService exampleMergeService;
+
+ /**
+ * Constructs a new MergeController.
+ *
+ * @param mergerService The core object merging orchestrator service.
+ * @param classLoadingGuard Safety filter guarding dynamic class instantiations.
+ * @param exampleMergeService Separated service handling mock example data merges.
+ */
+ public MergeController(
+ ObjectMergerService mergerService,
+ ClassLoadingGuard classLoadingGuard,
+ ExampleMergeService exampleMergeService) {
this.mergerService = mergerService;
this.classLoadingGuard = classLoadingGuard;
+ this.exampleMergeService = exampleMergeService;
}
@PostMapping(consumes = "application/json", produces = "application/json")
@@ -46,16 +64,16 @@ public MergeController(ObjectMergerService mergerService, ClassLoadingGuard clas
@ApiResponse(responseCode = "400", description = "Invalid request or merge failed")
public ResponseEntity> merge(@RequestBody MergeRequest request) {
try {
- MergeDefinition definition = MergeDefinitionConverter.fromMap(request.getDefinition());
+ MergeDefinition definition = MergeDefinitionConverter.fromMap(request.definition());
@SuppressWarnings("unchecked")
List> sources =
(List)
- request.getSources().stream()
- .map(dto -> new LabeledSource<>(dto.getLabel(), dto.getData()))
+ request.sources().stream()
+ .map(dto -> new LabeledSource<>(dto.label(), dto.data()))
.toList();
- Object result = mergerService.merge(request.getTargetClass(), definition, sources);
+ Object result = mergerService.merge(request.targetClass(), definition, sources);
return ResponseEntity.ok(result);
} catch (SecurityException securityException) {
@@ -78,16 +96,16 @@ public ResponseEntity> merge(@RequestBody MergeRequest request) {
@ApiResponse(responseCode = "400", description = "Invalid request or merge failed")
public ResponseEntity> mergeYaml(@RequestBody MergeRequest request) {
try {
- MergeDefinition definition = MergeDefinitionConverter.fromMap(request.getDefinition());
+ MergeDefinition definition = MergeDefinitionConverter.fromMap(request.definition());
@SuppressWarnings("unchecked")
List> sources =
(List)
- request.getSources().stream()
- .map(dto -> new LabeledSource<>(dto.getLabel(), dto.getData()))
+ request.sources().stream()
+ .map(dto -> new LabeledSource<>(dto.label(), dto.data()))
.toList();
- Object result = mergerService.merge(request.getTargetClass(), definition, sources);
+ Object result = mergerService.merge(request.targetClass(), definition, sources);
return ResponseEntity.ok(result);
} catch (SecurityException securityException) {
@@ -114,65 +132,7 @@ public ResponseEntity