Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 67 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This document describes the available merge strategies in ObjectMerger. Each sec
- [MVEL Strategy](#mvel-strategy)
- [List Strategy](#list-strategy)
- [Map Strategy](#map-strategy)
- [Nested Strategy](#nested-strategy)

---

Expand Down Expand Up @@ -612,5 +613,71 @@ itemsDef.setItemMergeDefinition(itemDef);
#### Java Code Example
```java
MapFieldDefinition<Map<Object, Object>> transDef = MapFieldDefinition.<Map<Object, Object>>builder()
---

### Nested Strategy
**Strategy Name:** `nested`
**Description:** Recursively merges nested POJO objects based on a nested definition. This allows for granular control over sub-field merging.

**Example Scenario:** Merging an address where the street comes from an API source (json2) and the zip code from a database source (json1).

#### Input Data
**json1 (db)**
```json
{
"address": {
"street": "Old St",
"zip": "12345"
}
}
```
**json2 (api)**
```json
{
"address": {
"street": "New St",
"zip": "99999"
}
}
```

#### Merge Definition
```json
{
"definitions": {
"address": {
"strategy": "nested",
"nestedDefinition": {
"definitions": {
"street": {
"strategy": "priority",
"priority": {"json2": 1, "json1": 2}
},
"zip": {
"strategy": "priority",
"priority": {"json1": 1, "json2": 2}
}
}
}
}
}
}
```

#### Result
```json
{
"address": {
"street": "New St",
"zip": "12345"
}
}
```

#### Java Code Example
```java
NestedFieldDefinition<Address> addressDef = NestedFieldDefinition.<Address>builder()
.nestedDefinition(nestedMergeDefinition)
.build();
```

50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ This builds all modules. The resulting artifacts are located in `target/` of the
| **concatenate**| Joins strings. | `{"strategy": "concatenate"}` |
| **mergeList** | Merges lists by ID. Supports Template/Intersection. | `{"strategy": "mergeList", "identifyBy": "id", "keyOriginLabels": ["A"], "requirePresenceInAllKeyOrigins": true}` |
| **mergeMap** | Vereinigt Maps (Union oder Template) | `{"strategy": "mergeMap"}` |
| **nested**| Deep merge of POJOs using nested definition. | `{"strategy": "nested", "nestedDefinition": {...}}` |
| **mvel** | Execute custom scripts. | `{"strategy": "mvel", "expression": "return 1;"}` |

### 8.2 Map Template Logic
Expand Down Expand Up @@ -240,6 +241,55 @@ The `conditional` strategy acts as a wrapper that routes to different strategies
}
```

### 8.9 Nested POJO Merging
Allows deep merging of nested POJO objects instead of replacing them wholesale. This enables granular control over nested fields.

**Configuration:**
- `strategy`: "nested"
- `nestedDefinition`: A full `MergeDefinition` for the nested object.

**Example:**
```json
{
"address": {
"strategy": "nested",
"nestedDefinition": {
"definitions": {
"street": { "strategy": "priority", "priority": {"api": 1} },
"zip": { "strategy": "priority", "priority": {"db": 1} }
}
}
}
}
```

### 8.10 Synergy: Conditional + Nested
Combine strategies to validate data before deep merging.

**Example:**
```json
{
"address": {
"strategy": "conditional",
"cases": [
{
"condition": "values['api'].isValid == true",
"useStrategy": {
"strategy": "nested",
"nestedDefinition": {
"templateSourceLabel": "api",
"definitions": {
"street": { "strategy": "priority", "priority": {"api": 1} }
}
}
}
}
],
"defaultStrategy": { "strategy": "priority", "priority": {"db": 1} }
}
}
```

## 9. Glossary

| Term | Definition |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public class MergeDefinitionConverter {
case "mvel":
targetClass = de.x132.objectmerger.strategy.mvel.MvelFieldDefinition.class;
break;
case "nested":
targetClass = de.x132.objectmerger.strategy.nested.NestedFieldDefinition.class;
break;
case "conditional":
targetClass =
de.x132.objectmerger.strategy.conditional.ConditionalFieldDefinition.class;
break;
default:
targetClass =
de.x132.objectmerger.strategy.standard.StandardFieldDefinition.class;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.x132.objectmerger.strategy.nested;

import de.x132.objectmerger.MergeDefinition;
import de.x132.objectmerger.strategy.FieldDefinition;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class NestedFieldDefinition<T> extends FieldDefinition<T> {
@Builder.Default private MergeDefinition nestedDefinition = new MergeDefinition();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package de.x132.objectmerger.strategy.nested;

import de.x132.objectmerger.LabeledSource;
import de.x132.objectmerger.MergeDefinition;
import de.x132.objectmerger.ObjectMerger;
import de.x132.objectmerger.exception.MergeExecutionException;
import de.x132.objectmerger.strategy.MergeStrategy;
import java.util.List;

public class NestedMergeStrategy<T> implements MergeStrategy<T, NestedFieldDefinition<T>> {

public static final String NAME = "nested";

@Override
public String getName() {
return NAME;
}

@SuppressWarnings("unchecked")
@Override
public Class<NestedFieldDefinition<T>> getConfigurationClass() {
return (Class) NestedFieldDefinition.class;
}

@Override
@SuppressWarnings("unchecked")
public T merge(
List<LabeledSource<?>> sources, NestedFieldDefinition<T> fieldDef, String fieldName) {
if (sources.isEmpty()) {
return fieldDef.getDefaultValue();
}

// Extract field values from sources
List<LabeledSource<Object>> nestedSources =
sources.stream()
.map(
s -> {
Object val = ObjectMerger.getFieldValue(s.getSource(), fieldName);
return new LabeledSource<>(s.getLabel(), val);
})
.collect(java.util.stream.Collectors.toList());

// Determine target class from the first non-null nested source
Object firstNonNull =
nestedSources.stream()
.map(LabeledSource::getSource)
.filter(java.util.Objects::nonNull)
.findFirst()
.orElse(null);

if (firstNonNull == null) {
return fieldDef.getDefaultValue();
}

Class<?> targetClass = firstNonNull.getClass();

// Prepare nested definition
MergeDefinition nestedDef = fieldDef.getNestedDefinition();

// If nested definition is empty, we must rely on Template Mode for discovery!
// We can synthesize a Template Source Label if we want to force one source to
// be the template?
// Or we just let ObjectMerger handle it.
// Wait, if 'nestedDef' is empty, ObjectMerger will look for 'definitions'.
// If that is empty, it returns empty object (unless template mode is ON).

// Feature Idea:
// If nestedDefinition has NO templateSource set, but we have sources here,
// should we imply one?
// Let's explicitly support Template Mode in Recursive Field Def?
// Or just say: If you want auto-discovery, set 'templateSourceLabel' in
// 'nestedDefinition'.
// That's cleaner.

// HOWEVER: We have sources interacting dynamically here.
// The main ObjectMerger loop doesn't pass 'definitions' down automatically
// unless we do it here.
// But 'nestedDef' IS that definition.

// Cast sources to LabeledSource<T>
LabeledSource<T>[] castSources =
(LabeledSource<T>[])
nestedSources.stream()
.map(s -> new LabeledSource<>(s.getLabel(), (T) s.getSource()))
.toArray(LabeledSource[]::new);

try {
Class<T> typedClass = (Class<T>) targetClass;
return ObjectMerger.merge(typedClass, nestedDef, castSources);
} catch (Exception e) {
throw new MergeExecutionException("Recursive merge failed for field " + fieldName, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ de.x132.objectmerger.strategy.minimum.MinimumValueStrategy
de.x132.objectmerger.strategy.maximum.MaximumValueStrategy
de.x132.objectmerger.strategy.mvel.MvelMergeStrategy
de.x132.objectmerger.strategy.conditional.ConditionalMergeStrategy
de.x132.objectmerger.strategy.nested.NestedMergeStrategy
Loading