-
Notifications
You must be signed in to change notification settings - Fork 196
Expand file tree
/
Copy pathDiffFactorizer.java
More file actions
321 lines (296 loc) · 12.4 KB
/
DiffFactorizer.java
File metadata and controls
321 lines (296 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/*
* Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
*
* This software is dual-licensed under:
*
* - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
* later version;
* - the Apache Software License (ASL) version 2.0.
*
* The text of this file and of both licenses is available at the root of this
* project or, if you have the jar distribution, in directory META-INF/, under
* the names LGPL-3.0.txt and ASL-2.0.txt respectively.
*
* Direct link to the sources:
*
* - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
* - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
*/
package com.github.fge.jsonpatch.diff;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jackson.JsonNumEquals;
import com.github.fge.jackson.jsonpointer.JsonPointer;
import com.google.common.base.Equivalence;
import com.google.common.collect.Lists;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
final class DiffFactorizer
{
private static final Equivalence<JsonNode> EQUIVALENCE
= JsonNumEquals.getInstance();
private DiffFactorizer()
{
}
/**
* Factorize list of ordered differences. Where removed values are
* equivalent to added values, merge add and remove to move
* differences. Because remove differences are relocated in the
* process of merging, other differences can be side effected.
* Add differences with equivalent values to previous add
* differences are converted to copy differences.
*
* @param diffs list of ordered differences.
*/
public static void factorizeDiffs(final List<Diff> diffs)
{
findPairs(diffs);
factorizePairs(diffs);
// Factorize add diffs with equivalent non-empty object or array
// values into copy diffs; from paths for copy diffs can be set using
// previous add diff paths and/or array paths because diff order is
// acyclic and immutable for this factorization. The only exception
// to this rule are adds that append to arrays: these have no concrete
// path that can serve as a copy diff from path.
final List<Diff> addDiffs = Lists.newArrayList();
for (final Diff diff: diffs) {
/*
* Ignore non add operations
*/
if (diff.operation != DiffOperation.ADD)
continue;
/*
* Skip value nodes or empty objects/arrays
*/
if (diff.value.size() == 0)
continue;
// check add diff value against list of previous add diffs
Diff addDiff = null;
for (final Diff testAddDiff : addDiffs)
if (EQUIVALENCE.equivalent(diff.value, testAddDiff.value)) {
addDiff = testAddDiff;
break;
}
// if not found previously, save add diff, (if not appending
// to an array which can have no concrete from path), and continue
if (addDiff == null) {
if (diff.arrayPath == null || diff.secondArrayIndex != -1)
addDiffs.add(diff);
continue;
}
// previous add diff found by value: convert add diff to copy
// diff with from path set to concrete add diff path
diff.operation = DiffOperation.COPY;
diff.fromPath = addDiff.arrayPath != null
? addDiff.getSecondArrayPath() : addDiff.path;
}
}
/**
* Find additions/removal pairs
*
* <p>Find addition operations which can be paired value-wise with removal
* operations.</p>
*
* <p>Note that only the first pair is considered.</p>
*
* @param diffs the list of diffs
*/
private static void findPairs(final List<Diff> diffs)
{
final int diffsSize = diffs.size();
Collection<Diff> alreadyPaired = new HashSet<Diff>();
Diff addition, removal;
for (int addIndex = 0; addIndex < diffsSize; addIndex++) {
addition = diffs.get(addIndex);
if (addition.operation != DiffOperation.ADD)
continue;
/*
* Found an addition: try and find a matching removal
*/
for (int removeIndex = 0; removeIndex < diffsSize; removeIndex++) {
removal = diffs.get(removeIndex);
if (removal.operation != DiffOperation.REMOVE || alreadyPaired.contains(removal))
continue;
if (!EQUIVALENCE.equivalent(removal.value, addition.value))
continue;
/*
* Found a pair: record it
*/
addition.pairedDiff = removal;
addition.firstOfPair = addIndex < removeIndex;
removal.pairedDiff = addition;
removal.firstOfPair = removeIndex < addIndex;
alreadyPaired.add(removal);
alreadyPaired.add(addition);
break;
}
}
}
/**
* Factorize additions/removals
*
* <p>Removals, when paired with additions, are removed from the list.</p>
*
* <p>Special care must be taken for additions/removal pairs happening
* within the same array, so that array indices can be adjusted properly.
* </p>
*
* @param diffs the list of diffs
*/
private static void factorizePairs(final List<Diff> diffs)
{
/*
* Those two arrays will hold array removals seen before or after their
* paired additions.
*/
final List<Diff> seenBefore = Lists.newArrayList();
final List<Diff> seenAfter = Lists.newArrayList();
final Iterator<Diff> iterator = diffs.iterator();
Diff diff;
while (iterator.hasNext()) {
diff = iterator.next();
if (diff.pairedDiff != null) {
switch (diff.operation) {
case REMOVE:
/*
* If removal is from an array and we reach this point,
* it means the matching addition has not been seen yet.
* Add this diff to the relevant list.
*/
if (diff.arrayPath != null && diff.firstOfPair)
seenBefore.add(diff);
// remove paired remove and continue
iterator.remove();
continue;
case ADD:
/*
* Turn paired additions into move operations
*/
transformAddition(seenBefore, seenAfter, diff);
break;
/*
* Note: a paired diff cannot be of any other type
*/
default:
throw new IllegalStateException();
}
}
// adjust secondary index for all array diffs with matching
// deferred array removes; note: all non remove array diffs
// have a valid second array index
if (diff.arrayPath != null)
diff.secondArrayIndex = adjustSecondArrayIndex(seenBefore,
diff.arrayPath, diff.secondArrayIndex);
}
}
private static void transformAddition(final List<Diff> seenBefore,
final List<Diff> seenAfter, final Diff diff)
{
final Diff removal = diff.pairedDiff;
// convert paired add diff into a move
diff.operation = DiffOperation.MOVE;
diff.pairedDiff = null;
/*
* Compute the "from" path of this move operation
*/
if (removal.arrayPath == null) {
/*
* If removal was not from an array, we just need to grab
* the path of this remove operation as the origin path
* for this move
*/
diff.fromPath = removal.path;
return;
}
if (diff.firstOfPair) {
// move diff is first of pair: remove will be advanced
// and will use original first indexes into array
int removeIndex = removal.firstArrayIndex;
// adjust remove index for operations on arrays with
// matching advanced array removes
removeIndex = adjustFirstArrayIndex(seenAfter, removal.arrayPath,
removeIndex);
// if move diff and remove diff are from the same array,
// remove index must be based on an original index offset
// from the move diff secondary index; this is reflecting
// the fact that array diff operations up to the move diff
// have been applied, but those following the move diff to
// the remove diff have not and thus require original
// first array index adjustments
if (removal.arrayPath.equals(diff.arrayPath)) {
final int moveSecondArrayIndex = adjustSecondArrayIndex(
seenBefore, diff.arrayPath, diff.secondArrayIndex);
final int moveFirstArrayIndex = adjustFirstArrayIndex(seenAfter,
diff.arrayPath, diff.firstArrayIndex);
removeIndex += moveSecondArrayIndex - moveFirstArrayIndex;
}
// set move diff from using adjusted remove index
diff.fromPath = removal.arrayPath.append(removeIndex);
// track advanced array removes
seenAfter.add(removal);
} else {
// remove diff is first of pair: remove has been deferred
// for this move; remove tracked deferred array remove
seenBefore.remove(removal);
// remove can now be moved using second index
int removeIndex = removal.secondArrayIndex;
// adjust remove index for operations on arrays with
// matching deferred array removes
removeIndex = adjustSecondArrayIndex(seenBefore, removal.arrayPath,
removeIndex);
// set move diff from using adjusted remove index
diff.fromPath = removal.arrayPath.append(removeIndex);
}
}
/**
* Adjust array index based on array removes seen before their matching
* additions. Missing second array indexes (-1) are not adjusted.
*
* @param seenAfter list of removals seen before their matching additions
* @param arrayPath array path of array index to adjust
* @param arrayIndex index to adjust and upper range of removes
* @return index adjusted by advanced array moves in range
*/
private static int adjustFirstArrayIndex(final List<Diff> seenAfter,
final JsonPointer arrayPath, final int arrayIndex)
{
/*
* Adjust index of removal operations on arrays with matching advanced
* array removes: for each advanced remove, decrement the index assuming
* remove will have been done before remaining diffs on array
*/
int arrayRemoves = 0;
for (final Diff removal: seenAfter)
if (arrayPath.equals(removal.arrayPath)
&& arrayIndex > removal.firstArrayIndex)
arrayRemoves++;
return arrayIndex - arrayRemoves;
}
/**
* Adjust array index of removal operations seen after their matching
* additions. Missing second array indexes (-1) are not adjusted.
*
* @param seenAfter list of removals seen before their matching additions
* @param arrayPath array path of array index to adjust
* @param arrayIndex index to adjust and upper range of moves
* @return index adjusted by deferred array moves in range
*/
private static int adjustSecondArrayIndex(final List<Diff> seenAfter,
final JsonPointer arrayPath, final int arrayIndex)
{
if (arrayIndex == -1)
return arrayIndex;
/*
* adjust secondary index for operations on arrays with matching
* deferred array removes: for each deferred remove, increment the index
* assuming remove will not be done until the move diff is performed
*/
int arrayRemoves = 0;
for (final Diff removal: seenAfter)
if (arrayPath.equals(removal.arrayPath)
&& arrayIndex >= removal.secondArrayIndex)
arrayRemoves++;
return arrayIndex + arrayRemoves;
}
}