Skip to content

Commit e36d740

Browse files
author
Piotr Bugara
committed
Merge branch 'master' into feature/48_sonar_issues
# Conflicts: # src/main/java/com/gravity9/jsonpatch/diff/DiffProcessor.java
2 parents ecb36e1 + 7acf941 commit e36d740

15 files changed

+642
-9
lines changed

src/main/java/com/gravity9/jsonpatch/TestOperation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import com.fasterxml.jackson.annotation.JsonCreator;
2323
import com.fasterxml.jackson.annotation.JsonProperty;
2424
import com.fasterxml.jackson.databind.JsonNode;
25-
import com.github.fge.jackson.JsonNumEquals;
25+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
2626
import com.jayway.jsonpath.JsonPath;
2727

2828
/**

src/main/java/com/gravity9/jsonpatch/diff/DiffProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
package com.gravity9.jsonpatch.diff;
2121

2222
import com.fasterxml.jackson.databind.JsonNode;
23-
import com.github.fge.jackson.JsonNumEquals;
2423
import com.github.fge.jackson.jsonpointer.JsonPointer;
2524
import com.gravity9.jsonpatch.JsonPatch;
2625
import com.gravity9.jsonpatch.JsonPatchOperation;
26+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
27+
2728

2829
import javax.annotation.Nullable;
2930
import java.util.ArrayList;

src/main/java/com/gravity9/jsonpatch/diff/JsonDiff.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.fasterxml.jackson.databind.node.ArrayNode;
2525
import com.fasterxml.jackson.databind.node.ObjectNode;
2626
import com.github.fge.jackson.JacksonUtils;
27-
import com.github.fge.jackson.JsonNumEquals;
2827
import com.github.fge.jackson.NodeType;
2928
import com.github.fge.jackson.jsonpointer.JsonPointer;
3029
import com.github.fge.msgsimple.bundle.MessageBundle;
@@ -34,6 +33,7 @@
3433
import com.gravity9.jsonpatch.JsonPatchMessages;
3534
import com.gravity9.jsonpatch.JsonPatchOperation;
3635
import com.gravity9.jsonpatch.RemoveOperation;
36+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
3737
import com.jayway.jsonpath.PathNotFoundException;
3838

3939
import javax.annotation.ParametersAreNonnullByDefault;
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package com.gravity9.jsonpatch.jackson;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.github.fge.jackson.NodeType;
5+
6+
import javax.annotation.Nullable;
7+
import java.util.HashSet;
8+
import java.util.Iterator;
9+
import java.util.Map;
10+
import java.util.Set;
11+
12+
/**
13+
* An {@code com.google.common.base.Equivalence} like strategy for JSON Schema equality
14+
*
15+
* <p>{@link JsonNode} does a pretty good job of obeying the {@link
16+
* Object#equals(Object) equals()}/{@link Object#hashCode() hashCode()}
17+
* contract. And in fact, it does it too well for JSON Schema.</p>
18+
*
19+
* <p>For instance, it considers numeric nodes {@code 1} and {@code 1.0} to be
20+
* different nodes, which is true. But some IETF RFCs and drafts (among them,
21+
* JSON Schema and JSON Patch) mandate that numeric JSON values be considered
22+
* equal if their mathematical value is the same. This class implements this
23+
* kind of equality.</p>
24+
*/
25+
public final class JsonNumEquals
26+
{
27+
private static final com.gravity9.jsonpatch.jackson.JsonNumEquals INSTANCE
28+
= new com.gravity9.jsonpatch.jackson.JsonNumEquals();
29+
30+
private JsonNumEquals()
31+
{
32+
}
33+
34+
public static com.gravity9.jsonpatch.jackson.JsonNumEquals getInstance()
35+
{
36+
return INSTANCE;
37+
}
38+
39+
@SuppressWarnings("ReferenceEquality")
40+
public final boolean equivalent(@Nullable JsonNode a, @Nullable JsonNode b) {
41+
if (a == b) {
42+
return true;
43+
}
44+
if (a == null || b == null) {
45+
return false;
46+
}
47+
return doEquivalent(a, b);
48+
}
49+
50+
private boolean doEquivalent(final JsonNode a, final JsonNode b)
51+
{
52+
/*
53+
* If both are numbers, delegate to the helper method
54+
*/
55+
if (a.isNumber() && b.isNumber())
56+
return numEquals(a, b);
57+
58+
final NodeType typeA = NodeType.getNodeType(a);
59+
final NodeType typeB = NodeType.getNodeType(b);
60+
61+
/*
62+
* If they are of different types, no dice
63+
*/
64+
if (typeA != typeB)
65+
return false;
66+
67+
/*
68+
* For all other primitive types than numbers, trust JsonNode
69+
*/
70+
if (!a.isContainerNode())
71+
return a.equals(b);
72+
73+
/*
74+
* OK, so they are containers (either both arrays or objects due to the
75+
* test on types above). They are obviously not equal if they do not
76+
* have the same number of elements/members.
77+
*/
78+
if (a.size() != b.size())
79+
return false;
80+
81+
/*
82+
* Delegate to the appropriate method according to their type.
83+
*/
84+
return typeA == NodeType.ARRAY ? arrayEquals(a, b) : objectEquals(a, b);
85+
}
86+
87+
public int hash(@Nullable JsonNode t) {
88+
if (t == null) {
89+
return 0;
90+
}
91+
return doHash(t);
92+
}
93+
94+
private int doHash(final JsonNode t)
95+
{
96+
/*
97+
* If this is a numeric node, we want the same hashcode for the same
98+
* mathematical values. Go with double, its range is good enough for
99+
* 99+% of use cases.
100+
*/
101+
if (t.isNumber())
102+
return Double.valueOf(t.doubleValue()).hashCode();
103+
104+
/*
105+
* If this is a primitive type (other than numbers, handled above),
106+
* delegate to JsonNode.
107+
*/
108+
if (!t.isContainerNode())
109+
return t.hashCode();
110+
111+
/*
112+
* The following hash calculations work, yes, but they are poor at best.
113+
* And probably slow, too.
114+
*
115+
* TODO: try and figure out those hash classes from Guava
116+
*/
117+
int ret = 0;
118+
119+
/*
120+
* If the container is empty, just return
121+
*/
122+
if (t.size() == 0)
123+
return ret;
124+
125+
/*
126+
* Array
127+
*/
128+
if (t.isArray()) {
129+
for (final JsonNode element: t)
130+
ret = 31 * ret + hash(element);
131+
return ret;
132+
}
133+
134+
/*
135+
* Not an array? An object.
136+
*/
137+
final Iterator<Map.Entry<String, JsonNode>> iterator = t.fields();
138+
139+
Map.Entry<String, JsonNode> entry;
140+
141+
while (iterator.hasNext()) {
142+
entry = iterator.next();
143+
ret = 31 * ret
144+
+ (entry.getKey().hashCode() ^ hash(entry.getValue()));
145+
}
146+
147+
return ret;
148+
}
149+
150+
// private static boolean numEquals(final JsonNode a, final JsonNode b)
151+
// {
152+
// /*
153+
// * If both numbers are integers, delegate to JsonNode.
154+
// */
155+
// if (a.isIntegralNumber() && b.isIntegralNumber())
156+
// return a.equals(b);
157+
//
158+
// /*
159+
// * Otherwise, compare decimal values.
160+
// */
161+
// return a.decimalValue().compareTo(b.decimalValue()) == 0;
162+
// }
163+
164+
private boolean numEquals(JsonNode a, JsonNode b) {
165+
return a.isNumber() && b.isNumber()
166+
? areNumberNodesNumericallyEqual(a, b)
167+
: a.equals(b);
168+
}
169+
170+
private boolean areNumberNodesNumericallyEqual(JsonNode a, JsonNode b) {
171+
if (a.isIntegralNumber() && b.isIntegralNumber()) {
172+
return a.canConvertToLong() && b.canConvertToLong()
173+
? a.longValue() == b.longValue()
174+
: a.bigIntegerValue().equals(b.bigIntegerValue());
175+
}
176+
177+
if(a.isFloatingPointNumber() && b.isFloatingPointNumber()){
178+
return a.isFloat() || b.isFloat() ?
179+
Float.compare(a.floatValue(), b.floatValue()) == 0
180+
: Double.compare(a.doubleValue(), b.doubleValue()) == 0;
181+
}
182+
183+
return a.decimalValue().compareTo(b.decimalValue()) == 0;
184+
}
185+
186+
private boolean arrayEquals(final JsonNode a, final JsonNode b)
187+
{
188+
/*
189+
* We are guaranteed here that arrays are the same size.
190+
*/
191+
final int size = a.size();
192+
193+
for (int i = 0; i < size; i++)
194+
if (!equivalent(a.get(i), b.get(i)))
195+
return false;
196+
197+
return true;
198+
}
199+
200+
private boolean objectEquals(final JsonNode a, final JsonNode b)
201+
{
202+
/*
203+
* Grab the key set from the first node
204+
*/
205+
final Set<String> keys = new HashSet<>();
206+
Iterator<String> iterator1 = a.fieldNames();
207+
while (iterator1.hasNext()) {
208+
final String next = iterator1.next();
209+
if (next != null) {
210+
keys.add(next);
211+
} else {
212+
throw new NullPointerException();
213+
}
214+
}
215+
216+
/*
217+
* Grab the key set from the second node, and see if both sets are the
218+
* same. If not, objects are not equal, no need to check for children.
219+
*/
220+
final Set<String> set = new HashSet<>();
221+
Iterator<String> iterator2 = b.fieldNames();
222+
while (iterator2.hasNext()) {
223+
final String next = iterator2.next();
224+
if (next != null) {
225+
set.add(next);
226+
} else {
227+
throw new NullPointerException();
228+
}
229+
}
230+
231+
if (!set.equals(keys))
232+
return false;
233+
234+
/*
235+
* Test each member individually.
236+
*/
237+
for (final String key: keys)
238+
if (!equivalent(a.get(key), b.get(key)))
239+
return false;
240+
241+
return true;
242+
}
243+
}

src/test/java/com/gravity9/jsonpatch/JsonPatchOperationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323
import com.fasterxml.jackson.databind.ObjectReader;
2424
import com.github.fge.jackson.JacksonUtils;
2525
import com.github.fge.jackson.JsonLoader;
26-
import com.github.fge.jackson.JsonNumEquals;
2726
import com.google.common.collect.Lists;
2827
import java.io.IOException;
2928
import java.util.Iterator;
3029
import java.util.List;
30+
31+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
3132
import org.testng.annotations.DataProvider;
3233
import org.testng.annotations.Test;
3334

src/test/java/com/gravity9/jsonpatch/TestOperationTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,46 @@
1919

2020
package com.gravity9.jsonpatch;
2121

22+
import com.fasterxml.jackson.databind.JsonNode;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import org.testng.annotations.Test;
25+
2226
import java.io.IOException;
2327

2428
public final class TestOperationTest extends JsonPatchOperationTest {
2529

30+
private static final TestDomain DOMAIN_TO_PATCH = new TestDomain(2022.4321f);
31+
private static final String testOperation = "[{\"op\":\"test\",\"path\":\"/myValue\",\"value\":2022.4321}]";
32+
2633
public TestOperationTest()
2734
throws IOException {
2835
super("test");
2936
}
37+
38+
@Test
39+
void testPatchValueIsDoubleDomainValueIsFloat() throws Exception {
40+
JsonPatch jsonPatch = new ObjectMapper().readValue(testOperation, JsonPatch.class);
41+
42+
JsonNode jsonNode = new ObjectMapper().valueToTree(DOMAIN_TO_PATCH);
43+
44+
jsonPatch.apply(jsonNode);
45+
}
46+
47+
@SuppressWarnings("UnusedMethod")
48+
private static class TestDomain {
49+
50+
private Float myValue;
51+
52+
public TestDomain(Float myValue) {
53+
this.myValue = myValue;
54+
}
55+
56+
public Float getMyValue() {
57+
return myValue;
58+
}
59+
60+
public void setMyValue(Float myValue) {
61+
this.myValue = myValue;
62+
}
63+
}
3064
}

src/test/java/com/gravity9/jsonpatch/diff/JsonDiffTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@
2323
import com.fasterxml.jackson.databind.JsonNode;
2424
import com.fasterxml.jackson.databind.ObjectMapper;
2525
import com.github.fge.jackson.JsonLoader;
26-
import com.github.fge.jackson.JsonNumEquals;
2726
import com.google.common.collect.Lists;
2827
import com.gravity9.jsonpatch.JsonPatch;
2928
import com.gravity9.jsonpatch.JsonPatchException;
3029
import java.io.IOException;
3130
import java.util.ArrayList;
3231
import java.util.Iterator;
3332
import java.util.List;
33+
34+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
3435
import org.testng.annotations.DataProvider;
3536
import org.testng.annotations.Test;
3637

src/test/java/com/gravity9/jsonpatch/mergepatch/NonObjectMergePatchTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.fasterxml.jackson.databind.JsonNode;
2323
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
2424
import com.github.fge.jackson.JsonLoader;
25-
import com.github.fge.jackson.JsonNumEquals;
2625
import com.github.fge.msgsimple.bundle.MessageBundle;
2726
import com.github.fge.msgsimple.load.MessageBundles;
2827
import com.google.common.collect.Lists;
@@ -31,6 +30,8 @@
3130
import java.io.IOException;
3231
import java.util.Iterator;
3332
import java.util.List;
33+
34+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
3435
import org.testng.annotations.DataProvider;
3536
import org.testng.annotations.Test;
3637

src/test/java/com/gravity9/jsonpatch/mergepatch/ObjectMergePatchTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.fasterxml.jackson.databind.JsonNode;
2323
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
2424
import com.github.fge.jackson.JsonLoader;
25-
import com.github.fge.jackson.JsonNumEquals;
2625
import com.github.fge.msgsimple.bundle.MessageBundle;
2726
import com.github.fge.msgsimple.load.MessageBundles;
2827
import com.google.common.collect.Lists;
@@ -31,6 +30,8 @@
3130
import java.io.IOException;
3231
import java.util.Iterator;
3332
import java.util.List;
33+
34+
import com.gravity9.jsonpatch.jackson.JsonNumEquals;
3435
import org.testng.annotations.DataProvider;
3536
import org.testng.annotations.Test;
3637

0 commit comments

Comments
 (0)