Skip to content

Commit be17136

Browse files
akloebergarethahealy
authored andcommitted
Added transaction support to MappedFieldsTracker (#772)
This fixes #739. Side-effects in MappedFieldsTracker, that occur when creating a comparison object for list mapping with relationship-type="non-cumulative", can now be isolated in a transaction and individually rollbacked if a matching item is found and should be used instead.
1 parent a0ce338 commit be17136

File tree

9 files changed

+291
-18
lines changed

9 files changed

+291
-18
lines changed

core/src/main/java/com/github/dozermapper/core/MapIdField.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public void put(String mapId, Object value) {
3535
mappedObjects.put(mapId, value);
3636
}
3737

38+
public void remove(String mapId) {
39+
mappedObjects.remove(mapId);
40+
}
41+
3842
public Object get(String mapId) {
3943
return mappedObjects.get(mapId);
4044
}

core/src/main/java/com/github/dozermapper/core/MappedFieldsTracker.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
*/
1616
package com.github.dozermapper.core;
1717

18+
import java.util.ArrayList;
1819
import java.util.HashMap;
1920
import java.util.IdentityHashMap;
21+
import java.util.Iterator;
22+
import java.util.List;
2023
import java.util.Map;
24+
import java.util.SortedMap;
25+
import java.util.TreeMap;
26+
import java.util.concurrent.atomic.AtomicInteger;
2127

2228
/**
2329
* Keeps track of mapped object during this mapping process execution.
@@ -26,10 +32,107 @@
2632
*/
2733
public class MappedFieldsTracker {
2834

35+
private static class UndoLogEntry {
36+
37+
private final MapIdField destMapIdField;
38+
private final String mapId;
39+
40+
UndoLogEntry(MapIdField destMapIdField, String mapId) {
41+
this.destMapIdField = destMapIdField;
42+
this.mapId = mapId;
43+
}
44+
45+
void revert() {
46+
destMapIdField.remove(mapId);
47+
}
48+
}
49+
50+
private static class UndoLog {
51+
52+
private List<UndoLogEntry> ops = new ArrayList<>();
53+
54+
void track(MapIdField destMapIdField, String mapId) {
55+
this.ops.add(new UndoLogEntry(destMapIdField, mapId));
56+
}
57+
58+
void revert() {
59+
for (UndoLogEntry op : this.ops) {
60+
op.revert();
61+
}
62+
}
63+
}
64+
65+
private static final int NO_TX_ID = -1;
66+
67+
// Counter used for generation of transaction IDs.
68+
private final AtomicInteger txId = new AtomicInteger(NO_TX_ID);
69+
2970
// Hash Code is ignored as it can serve application specific needs
3071
// <srcObject, <hashCodeOfDestination, mappedDestinationMapIdField>>
3172
private final Map<Object, Map<Integer, MapIdField>> mappedFields = new IdentityHashMap<>();
3273

74+
// Map with the Undo-Logs of pending transactions indexed and sorted by transaction IDs.
75+
private SortedMap<Integer, UndoLog> pendingTransactions = new TreeMap<>();
76+
77+
/**
78+
* Start a new transaction which supports commit or rollback. Nested transaction are also supported and may be
79+
* individually rollbacked. Even if nested transactions are commited they may still be rollbacked by the rollback
80+
* of an outer transaction until the root transaction is committed as well.
81+
* @return transaction ID that can be used for commit or rollback of the transaction
82+
* @see #commitTransaction(Integer)
83+
* @see #rollbackTransaction(Integer)
84+
*/
85+
public Integer startTransaction() {
86+
int curTxId = this.txId.incrementAndGet();
87+
this.pendingTransactions.put(curTxId, new UndoLog());
88+
return curTxId;
89+
}
90+
91+
/**
92+
* Commit transaction with the given ID. The operations executed under this transaction my still be reverted by the
93+
* rollback of a parent transaction (if any).
94+
* @param txId - transaction ID as returned by {@link #startTransaction()}
95+
*/
96+
public void commitTransaction(Integer txId) {
97+
final UndoLog undoLog = pendingTransactions.get(txId);
98+
if (undoLog == null) {
99+
throw new IllegalStateException("No transaction with ID " + txId);
100+
}
101+
if (pendingTransactions.firstKey().equals(txId)) {
102+
// commit of root transaction
103+
pendingTransactions.clear();
104+
}
105+
}
106+
107+
/**
108+
* Rollback transaction with the given ID. The operations of this transactions as well as those of nested
109+
* transactions are reverted.
110+
* @param txId - transaction ID as returned by {@link #startTransaction()}
111+
*/
112+
public void rollbackTransaction(Integer txId) {
113+
final UndoLog undoLog = pendingTransactions.get(txId);
114+
if (undoLog == null) {
115+
throw new IllegalStateException("No transaction with ID " + txId);
116+
}
117+
118+
// rollback nested transactions
119+
SortedMap<Integer, UndoLog> undoLogs = pendingTransactions.tailMap(txId);
120+
Iterator<UndoLog> undoLogIterator = undoLogs.values().iterator();
121+
while (undoLogIterator.hasNext()) {
122+
UndoLog curUndoLog = undoLogIterator.next();
123+
curUndoLog.revert();
124+
undoLogIterator.remove();
125+
}
126+
}
127+
128+
/**
129+
* Checks if there is a transaction active.
130+
* @return <code>true</code> if a transaction is active, <code>false</code> otherwise
131+
*/
132+
public boolean hasTransaction() {
133+
return pendingTransactions.size() > 0;
134+
}
135+
33136
public void put(Object src, Object dest, String mapId) {
34137
int destId = System.identityHashCode(dest);
35138

@@ -47,6 +150,10 @@ public void put(Object src, Object dest, String mapId) {
47150

48151
if (!destMapIdField.containsMapId(mapId)) {
49152
destMapIdField.put(mapId, dest);
153+
if (hasTransaction()) {
154+
UndoLog undoLog = this.pendingTransactions.get(txId.get());
155+
undoLog.track(destMapIdField, mapId);
156+
}
50157
}
51158
}
52159

core/src/main/java/com/github/dozermapper/core/MappingProcessor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ private Set<?> addToSet(Object srcObj, FieldMap fieldMap, Collection<?> srcColle
811811
destEntryType = determineCollectionItemType(fieldMap, destObj, srcValue, prevDestEntryType);
812812
}
813813

814+
Integer tx = mappedFields.startTransaction();
814815
CopyByReferenceContainer copyByReferences = globalConfiguration.getCopyByReferences();
815816
if (srcValue != null && copyByReferences.contains(srcValue.getClass())) {
816817
destValue = srcValue;
@@ -821,6 +822,7 @@ private Set<?> addToSet(Object srcObj, FieldMap fieldMap, Collection<?> srcColle
821822

822823
if (RelationshipType.NON_CUMULATIVE.equals(fieldMap.getRelationshipType())
823824
&& result.contains(destValue)) {
825+
mappedFields.rollbackTransaction(tx); // rollback side effects of dry-run
824826
List<Object> resultAsList = new ArrayList<>(result);
825827
int index = resultAsList.indexOf(destValue);
826828
// perform an update if complex type - can't map strings
@@ -831,6 +833,7 @@ private Set<?> addToSet(Object srcObj, FieldMap fieldMap, Collection<?> srcColle
831833
mappedElements.add(obj);
832834
}
833835
} else {
836+
mappedFields.commitTransaction(tx);
834837
if (destValue != null || fieldMap.isDestMapNull()) {
835838
result.add(destValue);
836839
}
@@ -874,6 +877,7 @@ private List<?> addOrUpdateToList(Object srcObj, FieldMap fieldMap, Collection<?
874877
destEntryType = determineCollectionItemType(fieldMap, destObj, srcValue, prevDestEntryType);
875878
}
876879

880+
Integer tx = mappedFields.startTransaction();
877881
CopyByReferenceContainer copyByReferences = globalConfiguration.getCopyByReferences();
878882
if (srcValue != null && copyByReferences.contains(srcValue.getClass())) {
879883
destValue = srcValue;
@@ -884,6 +888,7 @@ private List<?> addOrUpdateToList(Object srcObj, FieldMap fieldMap, Collection<?
884888

885889
if (RelationshipType.NON_CUMULATIVE.equals(fieldMap.getRelationshipType())
886890
&& result.contains(destValue)) {
891+
mappedFields.rollbackTransaction(tx); // rollback side effects of dry-run
887892
int index = result.indexOf(destValue);
888893
// perform an update if complex type - can't map strings
889894
Object obj = result.get(index);
@@ -893,6 +898,7 @@ private List<?> addOrUpdateToList(Object srcObj, FieldMap fieldMap, Collection<?
893898
mappedElements.add(obj);
894899
}
895900
} else {
901+
mappedFields.commitTransaction(tx);
896902
// respect null mappings
897903
if (destValue != null || fieldMap.isDestMapNull()) {
898904
result.add(destValue);

core/src/test/java/com/github/dozermapper/core/MapIdFieldTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,11 @@ public void testContainsMapId() {
5252
assertFalse(mapIdField.containsMapId(null));
5353
}
5454

55+
@Test
56+
public void testRemove() {
57+
mapIdField.put("aMapId", "aMapIdValue");
58+
mapIdField.remove("aMapId");
59+
assertFalse(mapIdField.containsMapId("aMapId"));
60+
}
61+
5562
}

core/src/test/java/com/github/dozermapper/core/MappedFieldsTrackerTest.java

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,21 @@
1919
import java.util.Set;
2020

2121
import org.junit.Before;
22+
import org.junit.Rule;
2223
import org.junit.Test;
24+
import org.junit.rules.ExpectedException;
2325

2426
import static org.junit.Assert.assertEquals;
27+
import static org.junit.Assert.assertFalse;
2528
import static org.junit.Assert.assertNull;
2629
import static org.junit.Assert.assertSame;
30+
import static org.junit.Assert.assertTrue;
2731

2832
public class MappedFieldsTrackerTest extends AbstractDozerTest {
2933

34+
@Rule
35+
public final ExpectedException exception = ExpectedException.none();
36+
3037
private MappedFieldsTracker tracker;
3138

3239
@Before
@@ -89,4 +96,142 @@ public boolean equals(Object obj) {
8996
}
9097
}
9198

99+
@Test
100+
public void testTransaction_commit() {
101+
tracker.put("1", "1");
102+
103+
assertFalse(tracker.hasTransaction());
104+
Integer txId = tracker.startTransaction();
105+
assertTrue(tracker.hasTransaction());
106+
assertEquals(Integer.valueOf(0), txId);
107+
108+
tracker.put("2", "2");
109+
assertEquals("1", tracker.getMappedValue("1", String.class));
110+
assertEquals("2", tracker.getMappedValue("2", String.class));
111+
112+
tracker.commitTransaction(txId);
113+
assertFalse(tracker.hasTransaction());
114+
assertEquals("1", tracker.getMappedValue("1", String.class));
115+
assertEquals("2", tracker.getMappedValue("2", String.class));
116+
}
117+
118+
@Test
119+
public void testTransaction_rollback() {
120+
tracker.put("1", "1");
121+
122+
assertFalse(tracker.hasTransaction());
123+
Integer txId = tracker.startTransaction();
124+
assertTrue(tracker.hasTransaction());
125+
assertEquals(Integer.valueOf(0), txId);
126+
127+
tracker.put("2", "2");
128+
assertEquals("1", tracker.getMappedValue("1", String.class));
129+
assertEquals("2", tracker.getMappedValue("2", String.class));
130+
131+
tracker.rollbackTransaction(txId);
132+
assertFalse(tracker.hasTransaction());
133+
assertEquals("1", tracker.getMappedValue("1", String.class));
134+
assertNull(tracker.getMappedValue("2", String.class));
135+
}
136+
137+
@Test
138+
public void testTransaction_nestedRollback() {
139+
tracker.put("1", "1");
140+
141+
// start root transaction
142+
Integer txIdRoot = tracker.startTransaction();
143+
assertTrue(tracker.hasTransaction());
144+
tracker.put("2", "2");
145+
146+
// start nested transaction
147+
Integer txIdNested = tracker.startTransaction();
148+
assertTrue(tracker.hasTransaction());
149+
tracker.put("3", "3");
150+
151+
// rollback nested transaction
152+
tracker.rollbackTransaction(txIdNested);
153+
assertTrue(tracker.hasTransaction());
154+
assertNull(tracker.getMappedValue("3", String.class));
155+
assertEquals("2", tracker.getMappedValue("2", String.class));
156+
assertEquals("1", tracker.getMappedValue("1", String.class));
157+
158+
// commit root transaction
159+
tracker.commitTransaction(txIdRoot);
160+
assertFalse(tracker.hasTransaction());
161+
assertEquals("2", tracker.getMappedValue("2", String.class));
162+
assertEquals("1", tracker.getMappedValue("1", String.class));
163+
}
164+
165+
@Test
166+
public void testTransaction_nestedCommit() {
167+
tracker.put("1", "1");
168+
169+
// start root transaction
170+
Integer txIdRoot = tracker.startTransaction();
171+
assertTrue(tracker.hasTransaction());
172+
tracker.put("2", "2");
173+
174+
// start nested transaction
175+
Integer txIdNested = tracker.startTransaction();
176+
assertTrue(tracker.hasTransaction());
177+
tracker.put("3", "3");
178+
179+
// rollback nested transaction
180+
tracker.commitTransaction(txIdNested);
181+
assertTrue(tracker.hasTransaction());
182+
assertEquals("3", tracker.getMappedValue("3", String.class));
183+
assertEquals("2", tracker.getMappedValue("2", String.class));
184+
assertEquals("1", tracker.getMappedValue("1", String.class));
185+
186+
// commit root transaction
187+
tracker.commitTransaction(txIdRoot);
188+
assertFalse(tracker.hasTransaction());
189+
assertEquals("3", tracker.getMappedValue("3", String.class));
190+
assertEquals("2", tracker.getMappedValue("2", String.class));
191+
assertEquals("1", tracker.getMappedValue("1", String.class));
192+
}
193+
194+
@Test
195+
public void testTransaction_nestedOuterRollback() {
196+
tracker.put("1", "1");
197+
198+
// start root transaction
199+
Integer txIdRoot = tracker.startTransaction();
200+
assertTrue(tracker.hasTransaction());
201+
tracker.put("2", "2");
202+
203+
// start nested transaction
204+
Integer txIdNested = tracker.startTransaction();
205+
assertTrue(tracker.hasTransaction());
206+
tracker.put("3", "3");
207+
208+
// rollback nested transaction
209+
tracker.commitTransaction(txIdNested);
210+
assertTrue(tracker.hasTransaction());
211+
assertEquals("3", tracker.getMappedValue("3", String.class));
212+
assertEquals("2", tracker.getMappedValue("2", String.class));
213+
assertEquals("1", tracker.getMappedValue("1", String.class));
214+
215+
// commit root transaction
216+
tracker.rollbackTransaction(txIdRoot);
217+
assertFalse(tracker.hasTransaction());
218+
assertNull(tracker.getMappedValue("3", String.class));
219+
assertNull(tracker.getMappedValue("2", String.class));
220+
assertEquals("1", tracker.getMappedValue("1", String.class));
221+
}
222+
223+
@Test
224+
public void testTransaction_unknownCommit() {
225+
exception.expect(IllegalStateException.class);
226+
exception.expectMessage("No transaction with ID 0");
227+
tracker.commitTransaction(0);
228+
}
229+
230+
@Test
231+
public void testTransaction_unknownRollback() {
232+
exception.expect(IllegalStateException.class);
233+
exception.expectMessage("No transaction with ID 0");
234+
tracker.rollbackTransaction(0);
235+
}
236+
92237
}

0 commit comments

Comments
 (0)