1 /*
2 * Copyright 2017 LINE Corporation
3 *
4 * LINE Corporation licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 /*
17 * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
18 *
19 * This software is dual-licensed under:
20 *
21 * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
22 * later version;
23 * - the Apache Software License (ASL) version 2.0.
24 *
25 * The text of this file and of both licenses is available at the root of this
26 * project or, if you have the jar distribution, in directory META-INF/, under
27 * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
28 *
29 * Direct link to the sources:
30 *
31 * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
32 * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
33 */
34
35 package com.linecorp.centraldogma.internal.jsonpatch;
36
37 import java.util.Iterator;
38 import java.util.Map;
39 import java.util.Set;
40
41 import com.fasterxml.jackson.databind.JsonNode;
42 import com.fasterxml.jackson.databind.node.JsonNodeType;
43 import com.google.common.base.Equivalence;
44 import com.google.common.collect.Sets;
45
46 /**
47 * An {@link Equivalence} strategy for JSON Schema equality.
48 *
49 * <p>{@link JsonNode} does a pretty good job of obeying the {@link
50 * Object#equals(Object) equals()}/{@link Object#hashCode() hashCode()}
51 * contract. And in fact, it does it too well for JSON Schema.</p>
52 *
53 * <p>For instance, it considers numeric nodes {@code 1} and {@code 1.0} to be
54 * different nodes, which is true. But some IETF RFCs and drafts (among them,
55 * JSON Schema and JSON Patch) mandate that numeric JSON values be considered
56 * equal if their mathematical value is the same. This class implements this
57 * kind of equality.</p>
58 */
59 public final class JsonNumEquals extends Equivalence<JsonNode> {
60
61 private static final Equivalence<JsonNode> INSTANCE = new JsonNumEquals();
62
63 public static Equivalence<JsonNode> getInstance() {
64 return INSTANCE;
65 }
66
67 private JsonNumEquals() {}
68
69 @Override
70 protected boolean doEquivalent(final JsonNode a, final JsonNode b) {
71 /*
72 * If both are numbers, delegate to the helper method
73 */
74 if (a.isNumber() && b.isNumber()) {
75 return numEquals(a, b);
76 }
77
78 final JsonNodeType typeA = a.getNodeType();
79 final JsonNodeType typeB = b.getNodeType();
80
81 /*
82 * If they are of different types, no dice
83 */
84 if (typeA != typeB) {
85 return false;
86 }
87
88 /*
89 * For all other primitive types than numbers, trust JsonNode
90 */
91 if (!a.isContainerNode()) {
92 return a.equals(b);
93 }
94
95 /*
96 * OK, so they are containers (either both arrays or objects due to the
97 * test on types above). They are obviously not equal if they do not
98 * have the same number of elements/members.
99 */
100 if (a.size() != b.size()) {
101 return false;
102 }
103
104 /*
105 * Delegate to the appropriate method according to their type.
106 */
107 return typeA == JsonNodeType.ARRAY ? arrayEquals(a, b) : objectEquals(a, b);
108 }
109
110 @Override
111 protected int doHash(final JsonNode t) {
112 /*
113 * If this is a numeric node, we want the same hashcode for the same
114 * mathematical values. Go with double, its range is good enough for
115 * 99+% of use cases.
116 */
117 if (t.isNumber()) {
118 return Double.valueOf(t.doubleValue()).hashCode();
119 }
120
121 /*
122 * If this is a primitive type (other than numbers, handled above),
123 * delegate to JsonNode.
124 */
125 if (!t.isContainerNode()) {
126 return t.hashCode();
127 }
128
129 /*
130 * The following hash calculations work, yes, but they are poor at best.
131 * And probably slow, too.
132 *
133 * TODO: try and figure out those hash classes from Guava
134 */
135 int ret = 0;
136
137 /*
138 * If the container is empty, just return
139 */
140 if (t.size() == 0) {
141 return ret;
142 }
143
144 /*
145 * Array
146 */
147 if (t.isArray()) {
148 for (final JsonNode element : t) {
149 ret = 31 * ret + doHash(element);
150 }
151 return ret;
152 }
153
154 /*
155 * Not an array? An object.
156 */
157 final Iterator<Map.Entry<String, JsonNode>> iterator = t.fields();
158
159 Map.Entry<String, JsonNode> entry;
160
161 while (iterator.hasNext()) {
162 entry = iterator.next();
163 ret = 31 * ret + (entry.getKey().hashCode() ^ doHash(entry.getValue()));
164 }
165
166 return ret;
167 }
168
169 private static boolean numEquals(final JsonNode a, final JsonNode b) {
170 /*
171 * If both numbers are integers, delegate to JsonNode.
172 */
173 if (a.isIntegralNumber() && b.isIntegralNumber()) {
174 return a.equals(b);
175 }
176
177 /*
178 * Otherwise, compare decimal values.
179 */
180 return a.decimalValue().compareTo(b.decimalValue()) == 0;
181 }
182
183 private boolean arrayEquals(final JsonNode a, final JsonNode b) {
184 /*
185 * We are guaranteed here that arrays are the same size.
186 */
187 final int size = a.size();
188
189 for (int i = 0; i < size; i++) {
190 if (!doEquivalent(a.get(i), b.get(i))) {
191 return false;
192 }
193 }
194
195 return true;
196 }
197
198 private boolean objectEquals(final JsonNode a, final JsonNode b) {
199 /*
200 * Grab the key set from the first node
201 */
202 final Set<String> keys = Sets.newHashSet(a.fieldNames());
203
204 /*
205 * Grab the key set from the second node, and see if both sets are the
206 * same. If not, objects are not equal, no need to check for children.
207 */
208 final Set<String> set = Sets.newHashSet(b.fieldNames());
209 if (!set.equals(keys)) {
210 return false;
211 }
212
213 /*
214 * Test each member individually.
215 */
216 for (final String key : keys) {
217 if (!doEquivalent(a.get(key), b.get(key))) {
218 return false;
219 }
220 }
221
222 return true;
223 }
224 }