Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
13e6c3d
refactor: extract mvel support into separate module objectmerger-mvel
lokimax Feb 18, 2026
383f0ce
refactor: extract mvel support into separate module objectmerger-mvel
lokimax Feb 18, 2026
c697aa5
style: apply spotless formatting
lokimax Feb 18, 2026
f3e984e
refactor: extract conditional strategy to mvel module
lokimax Feb 18, 2026
4495333
refactor: drop expression evaluator interface, move conditional strat…
lokimax Feb 18, 2026
c24e0b7
refactor: remove fully qualified class names and use imports
lokimax Feb 18, 2026
c5ef0b4
refactor: use dynamic strategy lookup in MergeDefinitionConverter for…
lokimax Feb 18, 2026
603d353
fix: ensure ServiceLoader can find services during tests by copying M…
lokimax Feb 18, 2026
cfdb8f7
refactor: use imports instead of FQNs in MergeDefinitionConverter
lokimax Feb 18, 2026
cceb5d3
refactor: use TypeAdapterFactory to prevent infinite recursion in des…
lokimax Feb 18, 2026
6318fe2
refactor: remove FQNs
lokimax Feb 18, 2026
533877d
fix: remove syntax error in MergeDefinitionConverter
lokimax Feb 18, 2026
5fb2546
clean: remove comments in MergeDefinitionConverter
lokimax Feb 18, 2026
7f5c300
refactor: extract FieldDefinitionTypeAdapterFactory from MergeDefinit…
lokimax Feb 18, 2026
2954de6
clean: refactor inner class in TypeAdapterFactory
lokimax Feb 18, 2026
85df8b8
build: optimize POMs by reordering dependencies
lokimax Feb 19, 2026
bf1624a
style: apply spotless formatting to FieldDefinitionTypeAdapterFactory
lokimax Feb 19, 2026
adabbc6
build: change spotless style to AOSP for wider lines (approx 100 chars)
lokimax Feb 19, 2026
c80c46e
build: configure spotless to prevent wildcard imports
lokimax Feb 19, 2026
d7b8263
build: add checkstyle plugin to parent pom with google_checks.xml
lokimax Feb 19, 2026
bb93fbb
build: remove checkstyle plugin (using spotless instead)
lokimax Feb 19, 2026
1644b40
clean: remove wildcard imports from tests
lokimax Feb 19, 2026
f850a16
style: apply final spotless formatting
lokimax Feb 19, 2026
6123f78
security: harden MVEL sandbox against FQN object creation and T() usage
lokimax Feb 19, 2026
7ae4d2c
build: bind spotless:apply to process-sources phase
lokimax Feb 19, 2026
4812b05
build: enforce spotless globally for all modules
lokimax Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions objectmerger-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<packaging>jar</packaging>

<dependencies>
<!-- Compile dependencies -->
<dependency>
<groupId>de.x132</groupId>
<artifactId>objectmerger</artifactId>
Expand All @@ -28,11 +29,15 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

<!-- Provided dependencies -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@

public class FieldDefinitionDeserializer implements JsonDeserializer<FieldDefinition<?>> {

@Override
public FieldDefinition<?> deserialize(
JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
@Override
public FieldDefinition<?> deserialize(
JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {

JsonObject jsonObject = json.getAsJsonObject();
String strategyName =
jsonObject.has("strategy") ? jsonObject.get("strategy").getAsString() : "standard";
JsonObject jsonObject = json.getAsJsonObject();
String strategyName =
jsonObject.has("strategy") ? jsonObject.get("strategy").getAsString() : "standard";

MergeStrategy<?, ?> strategy = StrategyRegistry.getInstance().getStrategy(strategyName);
MergeStrategy<?, ?> strategy = StrategyRegistry.getInstance().getStrategy(strategyName);

// The configuration class for the strategy
Class<? extends FieldDefinition> configClass = strategy.getConfigurationClass();
// The configuration class for the strategy
Class<? extends FieldDefinition> configClass = strategy.getConfigurationClass();

// Prevent infinite recursion if the strategy returns the abstract base class
if (configClass == FieldDefinition.class) {
configClass = StandardFieldDefinition.class;
}
// Prevent infinite recursion if the strategy returns the abstract base class
if (configClass == FieldDefinition.class) {
configClass = StandardFieldDefinition.class;
}

return context.deserialize(json, configClass);
}
return context.deserialize(json, configClass);
}
}
70 changes: 35 additions & 35 deletions objectmerger-cli/src/main/java/de/x132/cli/GenerateCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,41 @@
@Command(name = "generate", description = "Generate a merge definition from a sample JSON file")
public class GenerateCommand implements Callable<Integer> {

@Parameters(index = "0", description = "Path to sample JSON file")
private Path samplePath;

@Option(
names = {"-o", "--output"},
description = "Output JSON file; defaults to stdout")
private Path outputPath;

private final Gson gson = new GsonBuilder().setPrettyPrinting().create();

@Override
public Integer call() throws Exception {
try {
Map<String, Object> sampleData;
try (FileReader reader = new FileReader(samplePath.toFile())) {
sampleData = gson.fromJson(reader, Map.class);
}

MergeDefinition definition = MergeDefinitionGenerator.generate(sampleData);

String jsonOut = gson.toJson(definition);
if (outputPath != null) {
Files.createDirectories(outputPath.toAbsolutePath().getParent());
try (FileWriter writer = new FileWriter(outputPath.toFile())) {
writer.write(jsonOut);
@Parameters(index = "0", description = "Path to sample JSON file")
private Path samplePath;

@Option(
names = {"-o", "--output"},
description = "Output JSON file; defaults to stdout")
private Path outputPath;

private final Gson gson = new GsonBuilder().setPrettyPrinting().create();

@Override
public Integer call() throws Exception {
try {
Map<String, Object> sampleData;
try (FileReader reader = new FileReader(samplePath.toFile())) {
sampleData = gson.fromJson(reader, Map.class);
}

MergeDefinition definition = MergeDefinitionGenerator.generate(sampleData);

String jsonOut = gson.toJson(definition);
if (outputPath != null) {
Files.createDirectories(outputPath.toAbsolutePath().getParent());
try (FileWriter writer = new FileWriter(outputPath.toFile())) {
writer.write(jsonOut);
}
} else {
System.out.println(jsonOut);
}
return 0;

} catch (Exception e) {
System.err.println("Generation failed: " + e.getMessage());
e.printStackTrace();
return 1;
}
} else {
System.out.println(jsonOut);
}
return 0;

} catch (Exception e) {
System.err.println("Generation failed: " + e.getMessage());
e.printStackTrace();
return 1;
}
}
}
123 changes: 62 additions & 61 deletions objectmerger-cli/src/main/java/de/x132/cli/MergeCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,74 +22,75 @@
@Command(name = "merge", description = "Merge multiple JSON sources")
public class MergeCommand implements Callable<Integer> {

@Option(
names = {"-d", "--definition"},
required = true,
description = "Path to merge definition JSON file")
private Path definitionPath;
@Option(
names = {"-d", "--definition"},
required = true,
description = "Path to merge definition JSON file")
private Path definitionPath;

@Option(
names = {"-s", "--source"},
required = true,
arity = "1..*",
description = "Source in format label=path/to.json (repeat for multiple sources)")
private List<String> sources;
@Option(
names = {"-s", "--source"},
required = true,
arity = "1..*",
description = "Source in format label=path/to.json (repeat for multiple sources)")
private List<String> sources;

@Option(
names = {"-o", "--output"},
description = "Output JSON file; defaults to stdout")
private Path outputPath;
@Option(
names = {"-o", "--output"},
description = "Output JSON file; defaults to stdout")
private Path outputPath;

private final Gson gson =
new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(FieldDefinition.class, new FieldDefinitionDeserializer())
.create();
private final Gson gson =
new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(FieldDefinition.class, new FieldDefinitionDeserializer())
.create();

@Override
public Integer call() throws Exception {
try {
MergeDefinition mergeDefinition;
try (FileReader reader = new FileReader(definitionPath.toFile())) {
mergeDefinition = gson.fromJson(reader, MergeDefinition.class);
}
@Override
public Integer call() throws Exception {
try {
MergeDefinition mergeDefinition;
try (FileReader reader = new FileReader(definitionPath.toFile())) {
mergeDefinition = gson.fromJson(reader, MergeDefinition.class);
}

List<LabeledSource<Map<String, Object>>> labeledSources = new ArrayList<>();
for (String src : sources) {
int eq = src.indexOf('=');
if (eq <= 0 || eq == src.length() - 1) {
throw new IllegalArgumentException("Invalid --source format: " + src);
}
String label = src.substring(0, eq);
Path path = Path.of(src.substring(eq + 1));
try (FileReader reader = new FileReader(path.toFile())) {
Map<String, Object> obj = gson.fromJson(reader, Map.class);
labeledSources.add(new LabeledSource<>(label, obj));
}
}
List<LabeledSource<Map<String, Object>>> labeledSources = new ArrayList<>();
for (String src : sources) {
int eq = src.indexOf('=');
if (eq <= 0 || eq == src.length() - 1) {
throw new IllegalArgumentException("Invalid --source format: " + src);
}
String label = src.substring(0, eq);
Path path = Path.of(src.substring(eq + 1));
try (FileReader reader = new FileReader(path.toFile())) {
Map<String, Object> obj = gson.fromJson(reader, Map.class);
labeledSources.add(new LabeledSource<>(label, obj));
}
}

@SuppressWarnings("unchecked")
Map<String, Object> merged =
ObjectMerger.merge(mergeDefinition, labeledSources.toArray(LabeledSource[]::new));
@SuppressWarnings("unchecked")
Map<String, Object> merged =
ObjectMerger.merge(
mergeDefinition, labeledSources.toArray(LabeledSource[]::new));

String jsonOut = gson.toJson(merged);
if (outputPath != null) {
Files.createDirectories(outputPath.toAbsolutePath().getParent());
try (FileWriter writer = new FileWriter(outputPath.toFile())) {
writer.write(jsonOut);
}
} else {
System.out.println(jsonOut);
}
return 0;
String jsonOut = gson.toJson(merged);
if (outputPath != null) {
Files.createDirectories(outputPath.toAbsolutePath().getParent());
try (FileWriter writer = new FileWriter(outputPath.toFile())) {
writer.write(jsonOut);
}
} else {
System.out.println(jsonOut);
}
return 0;

} catch (JsonSyntaxException | JsonIOException e) {
System.err.println("Failed to parse JSON: " + e.getMessage());
return 3;
} catch (Exception e) {
System.err.println("Merge failed: " + e.getMessage());
e.printStackTrace();
return 1;
} catch (JsonSyntaxException | JsonIOException e) {
System.err.println("Failed to parse JSON: " + e.getMessage());
return 3;
} catch (Exception e) {
System.err.println("Merge failed: " + e.getMessage());
e.printStackTrace();
return 1;
}
}
}
}
18 changes: 9 additions & 9 deletions objectmerger-cli/src/main/java/de/x132/cli/ObjectMergerCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import picocli.CommandLine.Command;

@Command(
name = "objectmerger",
mixinStandardHelpOptions = true,
version = "0.1.0",
description = "ObjectMerger CLI tool",
subcommands = {MergeCommand.class, GenerateCommand.class})
name = "objectmerger",
mixinStandardHelpOptions = true,
version = "0.1.0",
description = "ObjectMerger CLI tool",
subcommands = {MergeCommand.class, GenerateCommand.class})
public class ObjectMergerCli {

public static void main(String[] args) {
int exit = new CommandLine(new ObjectMergerCli()).execute(args);
System.exit(exit);
}
public static void main(String[] args) {
int exit = new CommandLine(new ObjectMergerCli()).execute(args);
System.exit(exit);
}
}
79 changes: 41 additions & 38 deletions objectmerger-cli/src/test/java/de/x132/cli/GenerateCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,47 @@

class GenerateCommandTest {

private Path tempDir;
private Path sampleFile;
private Path outputFile;

@BeforeEach
void setUp() throws IOException {
tempDir = Files.createTempDirectory("objectmerger-cli-test");
sampleFile = tempDir.resolve("sample.json");
outputFile = tempDir.resolve("output.json");

try (FileWriter writer = new FileWriter(sampleFile.toFile())) {
writer.write("{\"name\": \"test\", \"value\": 123}");
private Path tempDir;
private Path sampleFile;
private Path outputFile;

@BeforeEach
void setUp() throws IOException {
tempDir = Files.createTempDirectory("objectmerger-cli-test");
sampleFile = tempDir.resolve("sample.json");
outputFile = tempDir.resolve("output.json");

try (FileWriter writer = new FileWriter(sampleFile.toFile())) {
writer.write("{\"name\": \"test\", \"value\": 123}");
}
}
}

@AfterEach
void tearDown() throws IOException {
Files.walk(tempDir).sorted((a, b) -> b.compareTo(a)).map(Path::toFile).forEach(File::delete);
}

@Test
void testGenerate() {
GenerateCommand cmd = new GenerateCommand();
CommandLine commandLine = new CommandLine(cmd);

int exitCode = commandLine.execute(sampleFile.toString(), "-o", outputFile.toString());

assertEquals(0, exitCode);
assertTrue(Files.exists(outputFile));

try {
String content = Files.readString(outputFile);
Gson gson = new Gson();
Map map = gson.fromJson(content, Map.class);
assertTrue(((Map) map.get("definitions")).containsKey("name"));
assertTrue(((Map) map.get("definitions")).containsKey("value"));
} catch (IOException e) {
throw new RuntimeException(e);

@AfterEach
void tearDown() throws IOException {
Files.walk(tempDir)
.sorted((a, b) -> b.compareTo(a))
.map(Path::toFile)
.forEach(File::delete);
}

@Test
void testGenerate() {
GenerateCommand cmd = new GenerateCommand();
CommandLine commandLine = new CommandLine(cmd);

int exitCode = commandLine.execute(sampleFile.toString(), "-o", outputFile.toString());

assertEquals(0, exitCode);
assertTrue(Files.exists(outputFile));

try {
String content = Files.readString(outputFile);
Gson gson = new Gson();
Map map = gson.fromJson(content, Map.class);
assertTrue(((Map) map.get("definitions")).containsKey("name"));
assertTrue(((Map) map.get("definitions")).containsKey("value"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
Loading