diff --git a/.gitignore b/.gitignore index 9ecee35..7211c18 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ # OS .DS_Store + +# Skills Portfolio +/Skills.md +/skills/ + diff --git a/benchmark-throwaway/pom.xml b/benchmark-throwaway/pom.xml new file mode 100644 index 0000000..acdbf12 --- /dev/null +++ b/benchmark-throwaway/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + de.x132 + objectmerger-parent + 0.2.0 + ../pom.xml + + de.x132.benchmark + benchmark-throwaway + 0.2.0 + + + + de.x132 + objectmerger-graaljs + 0.2.0 + + + de.x132 + objectmerger-mvel + 0.2.0 + + + de.x132 + objectmerger-nashorn + 0.2.0 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/benchmark-throwaway/src/test/java/de/x132/benchmark/BenchmarkPlatformTest.java b/benchmark-throwaway/src/test/java/de/x132/benchmark/BenchmarkPlatformTest.java new file mode 100644 index 0000000..7ed2155 --- /dev/null +++ b/benchmark-throwaway/src/test/java/de/x132/benchmark/BenchmarkPlatformTest.java @@ -0,0 +1,84 @@ +package de.x132.benchmark; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.strategy.graaljs.GraalJsFieldDefinition; +import de.x132.objectmerger.strategy.graaljs.GraalJsMergeStrategy; +import de.x132.objectmerger.strategy.mvel.MvelFieldDefinition; +import de.x132.objectmerger.strategy.mvel.MvelMergeStrategy; +import de.x132.objectmerger.strategy.nashorn.NashornFieldDefinition; +import de.x132.objectmerger.strategy.nashorn.NashornMergeStrategy; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class BenchmarkPlatformTest { + + @Test + public void benchmarkGraalJsVsMvelVsNashorn() { + System.setProperty("polyglot.engine.WarnInterpreterOnly", "false"); + + GraalJsMergeStrategy graalStrategy = new GraalJsMergeStrategy(); + MvelMergeStrategy mvelStrategy = new MvelMergeStrategy(); + NashornMergeStrategy nashornStrategy = new NashornMergeStrategy(); + + LabeledSource source = new LabeledSource<>("test", Map.of("val", "value")); + List> sources = Collections.singletonList(source); + + // GraalJS Setup + GraalJsFieldDefinition graalConfig = + GraalJsFieldDefinition.builder().expression("sources.test.val").build(); + + // MVEL Setup + MvelFieldDefinition mvelConfig = + MvelFieldDefinition.builder().expression("sources['test']['val']").build(); + + // Nashorn Setup + NashornFieldDefinition nashornConfig = + NashornFieldDefinition.builder().expression("sources.test.val").build(); + + // Warmup (to trigger JIT before real measurements) + System.out.println("Warming up engines..."); + for (int i = 0; i < 20000; i++) { + graalStrategy.merge(sources, graalConfig, "testField"); + mvelStrategy.merge(sources, mvelConfig, "testField"); + nashornStrategy.merge(sources, nashornConfig, "testField"); + } + + int[] sizes = { + 10_000, 50_000, 100_000 + }; // Reduced size so it doesn't take forever during testing. 1 mil took too long for + // GraalJS/Nashorn + + System.out.println("\n=== Benchmark Data (Total Time in ms) ==="); + System.out.println("Size,MVEL,GraalJS,Nashorn"); + + for (int size : sizes) { + // Measure MVEL + long startMvel = System.nanoTime(); + for (int i = 0; i < size; i++) { + mvelStrategy.merge(sources, mvelConfig, "testField"); + } + long durationMvel = System.nanoTime() - startMvel; + double msMvel = durationMvel / 1_000_000.0; + + // Measure GraalJS + long startGraal = System.nanoTime(); + for (int i = 0; i < size; i++) { + graalStrategy.merge(sources, graalConfig, "testField"); + } + long durationGraal = System.nanoTime() - startGraal; + double msGraal = durationGraal / 1_000_000.0; + + // Measure Nashorn + long startNashorn = System.nanoTime(); + for (int i = 0; i < size; i++) { + nashornStrategy.merge(sources, nashornConfig, "testField"); + } + long durationNashorn = System.nanoTime() - startNashorn; + double msNashorn = durationNashorn / 1_000_000.0; + + System.out.printf("%d,%.2f,%.2f,%.2f%n", size, msMvel, msGraal, msNashorn); + } + } +} diff --git a/objectmerger-graaljs/src/test/java/de/x132/objectmerger/strategy/graaljs/GraalJsBenchmarkTest.java b/objectmerger-graaljs/src/test/java/de/x132/objectmerger/strategy/graaljs/GraalJsBenchmarkTest.java new file mode 100644 index 0000000..6d78672 --- /dev/null +++ b/objectmerger-graaljs/src/test/java/de/x132/objectmerger/strategy/graaljs/GraalJsBenchmarkTest.java @@ -0,0 +1,55 @@ +package de.x132.objectmerger.strategy.graaljs; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.strategy.standard.StandardMergeStrategy; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class GraalJsBenchmarkTest { + + @Test + public void benchmarkGraalJsVsStandard() { + GraalJsMergeStrategy graalStrategy = new GraalJsMergeStrategy(); + StandardMergeStrategy standardStrategy = new StandardMergeStrategy(); + + LabeledSource source = new LabeledSource<>("test", Map.of("field", "value")); + List> sources = Collections.singletonList(source); + + // GraalJS Setup + GraalJsFieldDefinition graalConfig = + GraalJsFieldDefinition.builder().expression("sources.test.field").build(); + + // Standard Setup (Identity) + // StandardMergeStrategy configuration is generic, usually relies on field + // presence. + // But let's assume standard behavior is just getting the value. + // Actually StandardMergeStrategy logic depends on configuration, let's just + // test GraalJS overhead. + + // Warmup GraalJS + System.out.println("Warming up GraalJS..."); + for (int i = 0; i < 100; i++) { + graalStrategy.merge(sources, graalConfig, "field"); + } + + // Measure GraalJS + System.out.println("Measuring GraalJS (1000 iterations)..."); + long start = System.nanoTime(); + int iterations = 1000; + for (int i = 0; i < iterations; i++) { + graalStrategy.merge(sources, graalConfig, "field"); + } + long duration = System.nanoTime() - start; + double avgMs = (double) duration / iterations / 1_000_000.0; + + System.out.printf("GraalJS Average Merge Time: %.4f ms per call%n", avgMs); + System.out.printf("Total Time for %d calls: %.2f ms%n", iterations, duration / 1_000_000.0); + + if (avgMs > 1.0) { + System.out.println( + "WARNING: Performance is potentially slow (> 1ms per call) due to Context creation overhead."); + } + } +} diff --git a/objectmerger-nashorn/pom.xml b/objectmerger-nashorn/pom.xml new file mode 100644 index 0000000..cc95ad1 --- /dev/null +++ b/objectmerger-nashorn/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + de.x132 + objectmerger-parent + 0.2.0 + + + objectmerger-nashorn + ObjectMerger Nashorn Extension + Nashorn integration for ObjectMerger (alternative to MVEL) + + + + + de.x132 + objectmerger + ${project.version} + + + org.openjdk.nashorn + nashorn-core + 15.4 + + + org.slf4j + slf4j-api + + + + + org.projectlombok + lombok + provided + + + + + com.google.code.gson + gson + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/config/NashornConfig.java b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/config/NashornConfig.java new file mode 100644 index 0000000..ad2d12b --- /dev/null +++ b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/config/NashornConfig.java @@ -0,0 +1,5 @@ +package de.x132.objectmerger.strategy.config; + +public interface NashornConfig { + String getExpression(); +} diff --git a/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornConditionalStrategy.java b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornConditionalStrategy.java new file mode 100644 index 0000000..2aa40a5 --- /dev/null +++ b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornConditionalStrategy.java @@ -0,0 +1,116 @@ +package de.x132.objectmerger.strategy.nashorn; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.ObjectMerger; +import de.x132.objectmerger.exception.ConfigurationException; +import de.x132.objectmerger.registry.StrategyRegistry; +import de.x132.objectmerger.strategy.FieldDefinition; +import de.x132.objectmerger.strategy.MergeStrategy; +import de.x132.objectmerger.strategy.conditional.ConditionCase; +import de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import lombok.extern.slf4j.Slf4j; + +/** + * Conditional MergeStrategy using Nashorn expressions. + * + * @param the type of the result + */ +@Slf4j +public class NashornConditionalStrategy + implements MergeStrategy> { + + public static final String NAME = "conditional"; + + @Override + public String getName() { + return NAME; + } + + @SuppressWarnings("unchecked") + @Override + public Class> getConfigurationClass() { + return (Class) ConditionalFieldDefinition.class; + } + + @Override + @SuppressWarnings("unchecked") + public T merge( + List> sources, + ConditionalFieldDefinition fieldDef, + String fieldName) { + return (T) mergeInternal(sources, fieldDef, fieldName); + } + + private Object mergeInternal( + List> sources, + ConditionalFieldDefinition fieldDef, + String fieldName) { + + Map values = new HashMap<>(); + for (LabeledSource s : sources) { + values.put(s.getLabel(), ObjectMerger.getFieldValue(s.getSource(), fieldName)); + } + + try { + ScriptEngine engine = NashornHelper.createSecureEngine(); + + Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); + bindings.put("values", values); + bindings.put("sources", sources); + + if (fieldDef.getCases() != null) { + for (ConditionCase c : fieldDef.getCases()) { + try { + Object result = engine.eval(c.getCondition()); + + if (result instanceof Boolean && (Boolean) result) { + log.debug( + "Condition '{}' matched for field '{}'", + c.getCondition(), + fieldName); + return executeSubStrategy(c.getUseStrategy(), sources, fieldName); + } + } catch (Exception e) { + log.warn( + "Failed to evaluate Nashorn condition '{}' for field '{}': {}", + c.getCondition(), + fieldName, + e.getMessage()); + } + } + } + + } catch (Exception e) { + log.error( + "Error setting up Nashorn context for conditional strategy: {}", + e.getMessage()); + } + + if (fieldDef.getDefaultStrategy() != null) { + log.debug("No condition matched for field '{}', using default strategy", fieldName); + return executeSubStrategy(fieldDef.getDefaultStrategy(), sources, fieldName); + } + + return null; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Object executeSubStrategy( + FieldDefinition def, List> sources, String fieldName) { + String strategyName = def.getStrategy() != null ? def.getStrategy() : "standard"; + MergeStrategy strategy = StrategyRegistry.getInstance().getStrategy(strategyName); + + if (strategy == null) { + throw new ConfigurationException( + "Unknown strategy in conditional case: " + strategyName); + } + + return strategy.merge(sources, def, fieldName); + } +} diff --git a/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornFieldDefinition.java b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornFieldDefinition.java new file mode 100644 index 0000000..a13c523 --- /dev/null +++ b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornFieldDefinition.java @@ -0,0 +1,18 @@ +package de.x132.objectmerger.strategy.nashorn; + +import de.x132.objectmerger.strategy.FieldDefinition; +import de.x132.objectmerger.strategy.config.NashornConfig; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class NashornFieldDefinition extends FieldDefinition implements NashornConfig { + private String expression; +} diff --git a/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornHelper.java b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornHelper.java new file mode 100644 index 0000000..b9fa6f7 --- /dev/null +++ b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornHelper.java @@ -0,0 +1,46 @@ +package de.x132.objectmerger.strategy.nashorn; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; + +/** Helper class to create secure Nashorn contexts. */ +public final class NashornHelper { + + private NashornHelper() {} + + /** + * Creates a new secure Nashorn engine. + * + * @return a new {@link ScriptEngine}. + */ + public static ScriptEngine createSecureEngine() { + org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory factory = + new org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory(); + + ScriptEngine engine = + factory.getScriptEngine( + new String[] {"--no-java", "--no-syntax-extensions"}, + Thread.currentThread().getContextClassLoader(), + className -> false // Restrict ALL Java class loading + ); + + if (engine == null) { + throw new IllegalStateException("Nashorn ScriptEngine not found!"); + } + + sanitize(engine); + return engine; + } + + private static void sanitize(ScriptEngine engine) { + Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); + bindings.put("Packages", null); + bindings.put("java", null); + bindings.put("javax", null); + bindings.put("org", null); + bindings.put("com", null); + bindings.put("net", null); + bindings.put("io", null); + } +} diff --git a/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornMergeStrategy.java b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornMergeStrategy.java new file mode 100644 index 0000000..c8b6f18 --- /dev/null +++ b/objectmerger-nashorn/src/main/java/de/x132/objectmerger/strategy/nashorn/NashornMergeStrategy.java @@ -0,0 +1,77 @@ +package de.x132.objectmerger.strategy.nashorn; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.strategy.MergeStrategy; +import de.x132.objectmerger.strategy.config.NashornConfig; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import lombok.extern.slf4j.Slf4j; + +/** MergeStrategy implementation using Nashorn. */ +@Slf4j +public class NashornMergeStrategy implements MergeStrategy { + + public static final String NAME = "nashorn"; + + @Override + public String getName() { + return NAME; + } + + @Override + public Class getConfigurationClass() { + return NashornFieldDefinition.class; + } + + @Override + public Object merge( + List> sources, NashornFieldDefinition fieldDef, String fieldName) { + return merge(sources, (NashornConfig) fieldDef, fieldName, fieldDef.getDefaultValue()); + } + + private Object merge( + List> sources, + NashornConfig fieldDef, + String fieldName, + Object defaultValue) { + if (fieldDef == null + || fieldDef.getExpression() == null + || fieldDef.getExpression().isEmpty()) { + log.warn( + "Nashorn strategy invoked for field '{}' but no expression provided.", + fieldName); + return defaultValue; + } + + try { + ScriptEngine engine = NashornHelper.createSecureEngine(); + + Map simpleSources = new HashMap<>(); + for (LabeledSource source : sources) { + simpleSources.put(source.getLabel(), source.getSource()); + } + + Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); + bindings.put("sources", simpleSources); + bindings.put("labeledSources", sources); + + Object result = engine.eval(fieldDef.getExpression()); + + // Unpack Nashorn specific wrapper if needed, but Nashorn evaluates to standard Java + // objects mapping mostly. + + return result; + + } catch (Exception e) { + log.error( + "Error executing Nashorn expression for field '{}': {}", + fieldName, + e.getMessage()); + return defaultValue; + } + } +} diff --git a/objectmerger-nashorn/src/main/resources/META-INF/services/de.x132.objectmerger.strategy.MergeStrategy b/objectmerger-nashorn/src/main/resources/META-INF/services/de.x132.objectmerger.strategy.MergeStrategy new file mode 100644 index 0000000..42f266d --- /dev/null +++ b/objectmerger-nashorn/src/main/resources/META-INF/services/de.x132.objectmerger.strategy.MergeStrategy @@ -0,0 +1,2 @@ +de.x132.objectmerger.strategy.nashorn.NashornMergeStrategy +de.x132.objectmerger.strategy.nashorn.NashornConditionalStrategy diff --git a/objectmerger-nashorn/src/test/java/de/x132/doc/NashornDocumentationVerificationTest.java b/objectmerger-nashorn/src/test/java/de/x132/doc/NashornDocumentationVerificationTest.java new file mode 100644 index 0000000..0bf3e4a --- /dev/null +++ b/objectmerger-nashorn/src/test/java/de/x132/doc/NashornDocumentationVerificationTest.java @@ -0,0 +1,120 @@ +package de.x132.doc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.MergeDefinition; +import de.x132.objectmerger.ObjectMerger; +import de.x132.objectmerger.strategy.FieldDefinition; +import de.x132.objectmerger.strategy.conditional.ConditionCase; +import de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition; +import de.x132.objectmerger.strategy.nashorn.NashornConditionalStrategy; +import de.x132.objectmerger.strategy.nashorn.NashornFieldDefinition; +import de.x132.objectmerger.strategy.nashorn.NashornMergeStrategy; +import de.x132.objectmerger.strategy.priority.PriorityFieldDefinition; +import de.x132.objectmerger.strategy.priority.PriorityMergeStrategy; +import de.x132.objectmerger.strategy.standard.StandardFieldDefinition; +import de.x132.objectmerger.strategy.standard.StandardMergeStrategy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Nashorn Documentation Verification") +public class NashornDocumentationVerificationTest { + + @Test + @DisplayName("Verify Nashorn Strategy Example from Doc") + void verifyNashornStrategy() { + Map source1 = new HashMap<>(); + source1.put("price", 100); + source1.put("discount", 0.1); + + Map source2 = new HashMap<>(); // Empty + + NashornFieldDefinition finalPriceDef = + NashornFieldDefinition.builder() + .strategy(NashornMergeStrategy.NAME) + .expression("sources.json1.price * (1.0 - sources.json1.discount)") + .defaultValue(0.0) + .build(); + + Map> definitions = new HashMap<>(); + definitions.put("finalPrice", finalPriceDef); + + MergeDefinition mergeDefinition = new MergeDefinition(definitions); + + Map result = + ObjectMerger.merge( + mergeDefinition, + new LabeledSource<>("json1", source1), + new LabeledSource<>("json2", source2)); + + Object val = result.get("finalPrice"); + + // Nashorn usually returns Double for floating point arithmetic + assertEquals(90.0, ((Number) val).doubleValue(), 0.01); + } + + @Test + @DisplayName("Verify Conditional Strategy (Nashorn) Example from Doc") + void verifyConditionalStrategy() { + // Documented Example: "values.json1 == 'adult'" + + Map source1 = new HashMap<>(); + source1.put("age", 18); + source1.put("category", "adult"); + + Map source2 = new HashMap<>(); + source2.put("age", 18); + source2.put("category", "minor"); + + ConditionCase adultCase = new ConditionCase<>(); + // JS condition syntax + adultCase.setCondition("values.json1 == 'adult'"); + + Map p = new HashMap<>(); + p.put("json1", 1); + p.put("json2", 2); + + PriorityFieldDefinition priorityDef = + PriorityFieldDefinition.builder() + .strategy(PriorityMergeStrategy.NAME) + .priority(p) + .build(); + + adultCase.setUseStrategy(priorityDef); + + List> cases = new ArrayList<>(); + cases.add(adultCase); + + ConditionalFieldDefinition categoryDef = + ConditionalFieldDefinition.builder() + .strategy(NashornConditionalStrategy.NAME) // "conditional" + .defaultValue("unknown") + .cases(cases) + .build(); + + StandardFieldDefinition defaultDef = + StandardFieldDefinition.builder() + .strategy(StandardMergeStrategy.NAME) + .defaultValue("fallback") + .build(); + categoryDef.setDefaultStrategy(defaultDef); + + Map> definitions = new HashMap<>(); + definitions.put("category", categoryDef); + + MergeDefinition mergeDefinition = new MergeDefinition(definitions); + + Map result = + ObjectMerger.merge( + mergeDefinition, + new LabeledSource<>("json1", source1), + new LabeledSource<>("json2", source2)); + + assertEquals("adult", result.get("category")); + } +} diff --git a/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornBenchmarkTest.java b/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornBenchmarkTest.java new file mode 100644 index 0000000..8022b8b --- /dev/null +++ b/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornBenchmarkTest.java @@ -0,0 +1,42 @@ +package de.x132.objectmerger.strategy.nashorn; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.strategy.standard.StandardMergeStrategy; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class NashornBenchmarkTest { + + @Test + public void benchmarkNashornVsStandard() { + NashornMergeStrategy nashornStrategy = new NashornMergeStrategy(); + StandardMergeStrategy standardStrategy = new StandardMergeStrategy(); + + LabeledSource source = new LabeledSource<>("test", Map.of("field", "value")); + List> sources = Collections.singletonList(source); + + NashornFieldDefinition nashornConfig = + NashornFieldDefinition.builder().expression("sources.test.field").build(); + + // Warmup Nashorn + System.out.println("Warming up Nashorn..."); + for (int i = 0; i < 100; i++) { + nashornStrategy.merge(sources, nashornConfig, "field"); + } + + // Measure Nashorn + System.out.println("Measuring Nashorn (1000 iterations)..."); + long start = System.nanoTime(); + int iterations = 1000; + for (int i = 0; i < iterations; i++) { + nashornStrategy.merge(sources, nashornConfig, "field"); + } + long duration = System.nanoTime() - start; + double avgMs = (double) duration / iterations / 1_000_000.0; + + System.out.printf("Nashorn Average Merge Time: %.4f ms per call%n", avgMs); + System.out.printf("Total Time for %d calls: %.2f ms%n", iterations, duration / 1_000_000.0); + } +} diff --git a/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornMergeStrategyTest.java b/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornMergeStrategyTest.java new file mode 100644 index 0000000..ed1d0f6 --- /dev/null +++ b/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornMergeStrategyTest.java @@ -0,0 +1,121 @@ +package de.x132.objectmerger.strategy.nashorn; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import de.x132.objectmerger.LabeledSource; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class NashornMergeStrategyTest { + + private final NashornMergeStrategy strategy = new NashornMergeStrategy(); + + @Test + @DisplayName("Should execute simple arithmetic expression") + void arithmeticExpression() { + NashornFieldDefinition fieldDef = + NashornFieldDefinition.builder().expression("sources.a + sources.b").build(); + + List> sources = + Arrays.asList(new LabeledSource<>("a", 10), new LabeledSource<>("b", 20)); + + Object result = strategy.merge(sources, fieldDef, "testField"); + // Nashorn returns Number types, int or double depending on operations + assertEquals(30, ((Number) result).intValue()); + } + + @Test + @DisplayName("Should execute conditional logic") + void conditionalLogic() { + NashornFieldDefinition fieldDef = + NashornFieldDefinition.builder() + .expression("sources.prio > 100 ? sources.prio : sources.backup") + .build(); + + // Case 1: Prio > 100 + List> sourcesHigh = + Arrays.asList(new LabeledSource<>("prio", 150), new LabeledSource<>("backup", 10)); + assertEquals(150, ((Number) strategy.merge(sourcesHigh, fieldDef, "testField")).intValue()); + + // Case 2: Prio <= 100 + List> sourcesLow = + Arrays.asList(new LabeledSource<>("prio", 50), new LabeledSource<>("backup", 10)); + assertEquals(10, ((Number) strategy.merge(sourcesLow, fieldDef, "testField")).intValue()); + } + + @Test + @DisplayName("Should return default value for null/empty expression") + void emptyExpression() { + NashornFieldDefinition fieldDef = NashornFieldDefinition.builder().build(); + assertNull(strategy.merge(List.of(), fieldDef, "testField")); + + fieldDef.setExpression(""); + assertNull(strategy.merge(List.of(), fieldDef, "testField")); + } + + @Test + @DisplayName("Should handle missing sources gracefully (JS behavior)") + void missingSources() { + NashornFieldDefinition fieldDef = + NashornFieldDefinition.builder() + .expression("sources.missing == null ? 'not found' : 'found'") + .build(); + + List> sources = List.of(new LabeledSource<>("existing", 1)); + assertEquals("not found", strategy.merge(sources, fieldDef, "testField")); + } + + @Test + @DisplayName("Should execute conditional strategy logic") + void conditionalStrategy() { + NashornConditionalStrategy conditionalStrategy = new NashornConditionalStrategy<>(); + + // Use Map sources to allow field extraction + List> mapSources = + Arrays.asList( + new LabeledSource<>("s1", java.util.Map.of("amount", 150)), + new LabeledSource<>("s2", java.util.Map.of("amount", 50))); + + de.x132.objectmerger.strategy.conditional.ConditionCase case1 = + new de.x132.objectmerger.strategy.conditional.ConditionCase<>( + "values.s1 > 100", + de.x132.objectmerger.strategy.standard.StandardFieldDefinition.builder() + .build()); + + de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition def = + de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition.builder() + .cases(java.util.List.of(case1)) + .defaultStrategy( + de.x132.objectmerger.strategy.standard.StandardFieldDefinition + .builder() + .build()) + .build(); + + // Condition s1(150) > 100 is true + // Standard merge on "amount" returns 150 (from s1) + Object result = conditionalStrategy.merge(mapSources, def, "amount"); + assertEquals(150, result); + + // Test false condition + de.x132.objectmerger.strategy.conditional.ConditionCase case2 = + new de.x132.objectmerger.strategy.conditional.ConditionCase<>( + "values.s1 > 200", + de.x132.objectmerger.strategy.standard.StandardFieldDefinition.builder() + .build()); + + de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition def2 = + de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition.builder() + .cases(java.util.List.of(case2)) + .defaultStrategy( + de.x132.objectmerger.strategy.standard.StandardFieldDefinition + .builder() + .build()) + .build(); + + // Condition s1 > 200 is false -> default strategy -> 150 + assertEquals(150, conditionalStrategy.merge(mapSources, def2, "amount")); + } +} diff --git a/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornSandboxTest.java b/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornSandboxTest.java new file mode 100644 index 0000000..df58df2 --- /dev/null +++ b/objectmerger-nashorn/src/test/java/de/x132/objectmerger/strategy/nashorn/NashornSandboxTest.java @@ -0,0 +1,124 @@ +package de.x132.objectmerger.strategy.nashorn; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.x132.objectmerger.LabeledSource; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("Nashorn Sandbox Security Tests") +class NashornSandboxTest { + + private final NashornMergeStrategy strategy = new NashornMergeStrategy(); + + // Default value to expect when expression is blocked + private static final String BLOCKED_VALUE = "BLOCKED"; + + private List> 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> health() { "Example merge of a Person object from three sources (database, crm, analytics)") public ResponseEntity mergePersonExample() { try { - Map> defMap = new LinkedHashMap<>(); - - defMap.put( - "name", - Map.of( - "strategy", - "priority", - "priority", - Map.of("database", 1, "crm", 2, "analytics", 3))); - - defMap.put("age", Map.of("strategy", "maximum", "defaultValue", 0)); - - defMap.put( - "email", - Map.of( - "strategy", - "priority", - "priority", - Map.of("database", 1, "crm", 2, "analytics", 3))); - - defMap.put( - "phone", - Map.of( - "strategy", - "priority", - "priority", - Map.of("analytics", 1, "crm", 2, "database", 3))); - - MergeDefinition definition = MergeDefinitionConverter.fromMap(defMap); - - Map dbData = new LinkedHashMap<>(); - dbData.put("name", "Max Müller"); - dbData.put("age", 30); - dbData.put("email", "max@example.com"); - dbData.put("phone", null); - - Map crmData = new LinkedHashMap<>(); - crmData.put("name", "Maximilian Müller"); - crmData.put("age", 25); - crmData.put("email", null); - crmData.put("phone", "030-123456"); - - Map analyticsData = new LinkedHashMap<>(); - analyticsData.put("name", null); - analyticsData.put("age", 35); - analyticsData.put("email", "max.mueller@example.de"); - analyticsData.put("phone", "030-654321"); - - @SuppressWarnings("unchecked") - List> sources = - (List) - List.of( - new LabeledSource<>("database", dbData), - new LabeledSource<>("crm", crmData), - new LabeledSource<>("analytics", analyticsData)); - - Object result = - mergerService.merge("de.x132.objectmerger.model.Person", definition, sources); - + Object result = exampleMergeService.mergePersonExample(); return ResponseEntity.ok(result); } catch (Exception e) { log.error("Example merge failed", e); diff --git a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/LabeledSourceDTO.java b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/LabeledSourceDTO.java index 058d157..326bce2 100644 --- a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/LabeledSourceDTO.java +++ b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/LabeledSourceDTO.java @@ -1,19 +1,16 @@ package de.x132.objectmerger.dto; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -@Data -@NoArgsConstructor -@AllArgsConstructor +/** + * Data Transfer Object representing a single labeled source object. + * + *

Uses a Java Record to minimize boilerplate and enforce immutability for input payloads. + * + * @param label The label/identifier for the source. + * @param data The source object data. + */ @Schema(description = "A labeled source for merging") -public class LabeledSourceDTO { - - @Schema(description = "Label/identifier for the source", example = "database") - private String label; - - @Schema(description = "The source object data") - private Object data; -} +public record LabeledSourceDTO( + @Schema(description = "Label/identifier for the source", example = "database") String label, + @Schema(description = "The source object data") Object data) {} diff --git a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/MergeRequest.java b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/MergeRequest.java index a455dac..4a4fabc 100644 --- a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/MergeRequest.java +++ b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/dto/MergeRequest.java @@ -4,21 +4,25 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import java.util.Map; -import lombok.Data; -@Data +/** + * Data Transfer Object representing the request payload for merging object sources. + * + *

Uses a Java Record to satisfy immutability requirements and reduce Lombok boilerplate. + * + * @param targetClass Fully qualified name of the target class to instantiate or map. + * @param definition The configuration details for merging fields (strategies, priorities, etc.). + * @param sources List of input data objects labeled by their origins. + */ @Schema(description = "Request to merge multiple sources into a single object") -public class MergeRequest { - - @JsonProperty("targetClass") - @Schema(description = "Fully qualified target class name", example = "de.x132.cli.Person") - private String targetClass; - - @JsonProperty("definition") - @Schema(description = "Merge definition with strategies and priorities") - private Map> definition; - - @JsonProperty("sources") - @Schema(description = "Labeled sources to merge") - private List sources; -} +public record MergeRequest( + @JsonProperty("targetClass") + @Schema( + description = "Fully qualified target class name", + example = "de.x132.cli.Person") + String targetClass, + @JsonProperty("definition") + @Schema(description = "Merge definition with strategies and priorities") + Map> definition, + @JsonProperty("sources") @Schema(description = "Labeled sources to merge") + List sources) {} diff --git a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/service/ExampleMergeService.java b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/service/ExampleMergeService.java new file mode 100644 index 0000000..23e6341 --- /dev/null +++ b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/service/ExampleMergeService.java @@ -0,0 +1,19 @@ +package de.x132.objectmerger.service; + +/** + * Service abstraction for executing sample merge requests. + * + *

Follows the Dependency Inversion Principle by defining an interface for the example merge + * service. + */ +public interface ExampleMergeService { + + /** + * Runs the pre-configured Person merge example using mock data from database, crm, and + * analytics sources. + * + * @return The merged result object (typically a Person instance). + * @throws Exception if class loading, conversion, or merge orchestration fails. + */ + Object mergePersonExample() throws Exception; +} diff --git a/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/service/ExampleMergeServiceImpl.java b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/service/ExampleMergeServiceImpl.java new file mode 100644 index 0000000..64b1e28 --- /dev/null +++ b/objectmerger-spring-boot/src/main/java/de/x132/objectmerger/service/ExampleMergeServiceImpl.java @@ -0,0 +1,99 @@ +package de.x132.objectmerger.service; + +import de.x132.objectmerger.LabeledSource; +import de.x132.objectmerger.MergeDefinition; +import de.x132.objectmerger.util.MergeDefinitionConverter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Service; + +/** + * Implementation of ExampleMergeService. + * + *

Encapsulates mock data preparation and merges it using the core merger service. Keeping this + * logic here satisfies the Single Responsibility Principle by decoupling controller endpoints from + * mock data structures. + */ +@Service +public class ExampleMergeServiceImpl implements ExampleMergeService { + + private final ObjectMergerService mergerService; + + /** + * Constructs a new ExampleMergeServiceImpl. + * + * @param mergerService The underlying ObjectMergerService to delegate merge logic to. + */ + public ExampleMergeServiceImpl(ObjectMergerService mergerService) { + this.mergerService = mergerService; + } + + @Override + @SuppressWarnings("unchecked") + public Object mergePersonExample() throws Exception { + Map> defMap = new LinkedHashMap<>(); + + // Priority rules for 'name' + defMap.put( + "name", + Map.of( + "strategy", + "priority", + "priority", + Map.of("database", 1, "crm", 2, "analytics", 3))); + + // Maximum rule for 'age' + defMap.put("age", Map.of("strategy", "maximum", "defaultValue", 0)); + + // Priority rules for 'email' + defMap.put( + "email", + Map.of( + "strategy", + "priority", + "priority", + Map.of("database", 1, "crm", 2, "analytics", 3))); + + // Priority rules for 'phone' + defMap.put( + "phone", + Map.of( + "strategy", + "priority", + "priority", + Map.of("analytics", 1, "crm", 2, "database", 3))); + + MergeDefinition definition = MergeDefinitionConverter.fromMap(defMap); + + // Database source mock data + Map dbData = new LinkedHashMap<>(); + dbData.put("name", "Max Müller"); + dbData.put("age", 30); + dbData.put("email", "max@example.com"); + dbData.put("phone", null); + + // CRM source mock data + Map crmData = new LinkedHashMap<>(); + crmData.put("name", "Maximilian Müller"); + crmData.put("age", 25); + crmData.put("email", null); + crmData.put("phone", "030-123456"); + + // Analytics source mock data + Map analyticsData = new LinkedHashMap<>(); + analyticsData.put("name", null); + analyticsData.put("age", 35); + analyticsData.put("email", "max.mueller@example.de"); + analyticsData.put("phone", "030-654321"); + + List> sources = + (List) + List.of( + new LabeledSource<>("database", dbData), + new LabeledSource<>("crm", crmData), + new LabeledSource<>("analytics", analyticsData)); + + return mergerService.merge("de.x132.objectmerger.model.Person", definition, sources); + } +} diff --git a/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerIntegrationTest.java b/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerIntegrationTest.java index 50e94d7..4707253 100644 --- a/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerIntegrationTest.java +++ b/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerIntegrationTest.java @@ -52,10 +52,6 @@ void testExampleEndpoint() throws Exception { @Test @DisplayName("Should merge person with priority strategy") void testMergeWithPriorityStrategy() throws Exception { - // Prepare request - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - // Define merge definition with priority Map> definition = new LinkedHashMap<>(); definition.put( @@ -64,7 +60,6 @@ void testMergeWithPriorityStrategy() throws Exception { definition.put("age", Map.of("strategy", "maximum")); definition.put("email", Map.of("strategy", "priority", "priority", Map.of("database", 1))); definition.put("phone", Map.of("strategy", "priority", "priority", Map.of("crm", 1))); - request.setDefinition(definition); // Define sources Map dbSource = new LinkedHashMap<>(); @@ -83,7 +78,10 @@ void testMergeWithPriorityStrategy() throws Exception { Arrays.asList( new LabeledSourceDTO("database", dbSource), new LabeledSourceDTO("crm", crmSource)); - request.setSources(sources); + + // Prepare request using record constructor + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); // Execute request mockMvc.perform( @@ -100,15 +98,11 @@ void testMergeWithPriorityStrategy() throws Exception { @Test @DisplayName("Should merge with maximum strategy") void testMergeWithMaximumStrategy() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - Map> definition = new LinkedHashMap<>(); definition.put("name", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("age", Map.of("strategy", "maximum")); definition.put("email", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("phone", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); - request.setDefinition(definition); Map source1 = new LinkedHashMap<>(); source1.put("name", "John"); @@ -133,7 +127,9 @@ void testMergeWithMaximumStrategy() throws Exception { new LabeledSourceDTO("source1", source1), new LabeledSourceDTO("source2", source2), new LabeledSourceDTO("source3", source3)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); mockMvc.perform( post("/api/v1/merge") @@ -146,15 +142,11 @@ void testMergeWithMaximumStrategy() throws Exception { @Test @DisplayName("Should merge with minimum strategy") void testMergeWithMinimumStrategy() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - Map> definition = new LinkedHashMap<>(); definition.put("name", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("age", Map.of("strategy", "minimum")); definition.put("email", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("phone", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); - request.setDefinition(definition); Map source1 = new LinkedHashMap<>(); source1.put("name", "Person1"); @@ -172,7 +164,9 @@ void testMergeWithMinimumStrategy() throws Exception { Arrays.asList( new LabeledSourceDTO("source1", source1), new LabeledSourceDTO("source2", source2)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); mockMvc.perform( post("/api/v1/merge") @@ -185,15 +179,11 @@ void testMergeWithMinimumStrategy() throws Exception { @Test @DisplayName("Should merge with average strategy") void testMergeWithAverageStrategy() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - Map> definition = new LinkedHashMap<>(); definition.put("name", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("age", Map.of("strategy", "average")); definition.put("email", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("phone", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); - request.setDefinition(definition); Map source1 = new LinkedHashMap<>(); source1.put("name", "Person1"); @@ -218,7 +208,9 @@ void testMergeWithAverageStrategy() throws Exception { new LabeledSourceDTO("source1", source1), new LabeledSourceDTO("source2", source2), new LabeledSourceDTO("source3", source3)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); mockMvc.perform( post("/api/v1/merge") @@ -231,15 +223,11 @@ void testMergeWithAverageStrategy() throws Exception { @Test @DisplayName("Should merge with sum strategy") void testMergeWithSumStrategy() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - Map> definition = new LinkedHashMap<>(); definition.put("name", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("age", Map.of("strategy", "sum")); definition.put("email", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); definition.put("phone", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); - request.setDefinition(definition); Map source1 = new LinkedHashMap<>(); source1.put("name", "Person1"); @@ -264,7 +252,9 @@ void testMergeWithSumStrategy() throws Exception { new LabeledSourceDTO("source1", source1), new LabeledSourceDTO("source2", source2), new LabeledSourceDTO("source3", source3)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); mockMvc.perform( post("/api/v1/merge") @@ -277,19 +267,17 @@ void testMergeWithSumStrategy() throws Exception { @Test @DisplayName("Should return 403 when target class is not in allowed packages") void testMergeWithBlockedTargetClass() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("com.invalid.NonExistentClass"); - Map> definition = new LinkedHashMap<>(); definition.put("name", Map.of("strategy", "priority", "priority", Map.of("source1", 1))); - request.setDefinition(definition); Map source = new LinkedHashMap<>(); source.put("name", "Test"); List sources = Collections.singletonList(new LabeledSourceDTO("source1", source)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("com.invalid.NonExistentClass", definition, sources); mockMvc.perform( post("/api/v1/merge") @@ -302,9 +290,6 @@ void testMergeWithBlockedTargetClass() throws Exception { @Test @DisplayName("Should handle null values in sources") void testMergeWithNullValues() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - Map> definition = new LinkedHashMap<>(); definition.put( "name", @@ -318,7 +303,6 @@ void testMergeWithNullValues() throws Exception { definition.put( "phone", Map.of("strategy", "priority", "priority", Map.of("source1", 1, "source2", 2))); - request.setDefinition(definition); // source1 has nulls Map source1 = new LinkedHashMap<>(); @@ -338,7 +322,9 @@ void testMergeWithNullValues() throws Exception { Arrays.asList( new LabeledSourceDTO("source1", source1), new LabeledSourceDTO("source2", source2)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); mockMvc.perform( post("/api/v1/merge") @@ -354,9 +340,6 @@ void testMergeWithNullValues() throws Exception { @Test @DisplayName("Should merge with multiple sources and complex priority") void testMergeWithComplexPriority() throws Exception { - MergeRequest request = new MergeRequest(); - request.setTargetClass("de.x132.objectmerger.model.Person"); - Map> definition = new LinkedHashMap<>(); definition.put( "name", @@ -380,7 +363,6 @@ void testMergeWithComplexPriority() throws Exception { "priority", "priority", Map.of("crm", 1, "database", 2, "analytics", 3))); - request.setDefinition(definition); Map dbSource = new LinkedHashMap<>(); dbSource.put("name", "DB Name"); @@ -405,7 +387,9 @@ void testMergeWithComplexPriority() throws Exception { new LabeledSourceDTO("database", dbSource), new LabeledSourceDTO("crm", crmSource), new LabeledSourceDTO("analytics", analyticsSource)); - request.setSources(sources); + + MergeRequest request = + new MergeRequest("de.x132.objectmerger.model.Person", definition, sources); mockMvc.perform( post("/api/v1/merge") diff --git a/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerTest.java b/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerTest.java index eb20158..dc805a0 100644 --- a/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerTest.java +++ b/objectmerger-spring-boot/src/test/java/de/x132/objectmerger/controller/MergeControllerTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.x132.objectmerger.security.ClassLoadingGuard; +import de.x132.objectmerger.service.ExampleMergeService; import de.x132.objectmerger.service.ObjectMergerService; import java.util.Map; import org.junit.jupiter.api.Test; @@ -24,6 +25,8 @@ class MergeControllerTest { @MockBean private ObjectMergerService objectMergerService; + @MockBean private ExampleMergeService exampleMergeService; + @Test void testGenerateFromClass() throws Exception { Map request = Map.of("className", "Person"); diff --git a/pom.xml b/pom.xml index cbd9b64..3c25ba7 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,8 @@ objectmerger-spring-boot objectmerger-mvel objectmerger-graaljs + objectmerger-nashorn + benchmark-throwaway @@ -57,6 +59,13 @@ + + + org.openjdk.nashorn + nashorn-core + 15.4 + + de.x132 @@ -73,6 +82,11 @@ objectmerger-mvel ${project.version} + + de.x132 + objectmerger-nashorn + ${project.version} +