com.github.fge.jackson.JsonNumEquals Maven / Gradle / Ivy
/*
* Copyright (c) 2014, Francis Galiegue ([email protected])
*
* 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.jackson;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Equivalence;
import com.google.common.collect.Sets;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* An {@link Equivalence} strategy for JSON Schema equality
*
* {@link JsonNode} does a pretty good job of obeying the {@link
* Object#equals(Object) equals()}/{@link Object#hashCode() hashCode()}
* contract. And in fact, it does it too well for JSON Schema.
*
* For instance, it considers numeric nodes {@code 1} and {@code 1.0} to be
* different nodes, which is true. But some IETF RFCs and drafts (among them,
* JSON Schema and JSON Patch) mandate that numeric JSON values be considered
* equal if their mathematical value is the same. This class implements this
* kind of equality.
*/
public final class JsonNumEquals
extends Equivalence
{
private static final Equivalence INSTANCE
= new JsonNumEquals();
private JsonNumEquals()
{
}
public static Equivalence getInstance()
{
return INSTANCE;
}
@Override
protected boolean doEquivalent(final JsonNode a, final JsonNode b)
{
/*
* If both are numbers, delegate to the helper method
*/
if (a.isNumber() && b.isNumber())
return numEquals(a, b);
final NodeType typeA = NodeType.getNodeType(a);
final NodeType typeB = NodeType.getNodeType(b);
/*
* If they are of different types, no dice
*/
if (typeA != typeB)
return false;
/*
* For all other primitive types than numbers, trust JsonNode
*/
if (!a.isContainerNode())
return a.equals(b);
/*
* OK, so they are containers (either both arrays or objects due to the
* test on types above). They are obviously not equal if they do not
* have the same number of elements/members.
*/
if (a.size() != b.size())
return false;
/*
* Delegate to the appropriate method according to their type.
*/
return typeA == NodeType.ARRAY ? arrayEquals(a, b) : objectEquals(a, b);
}
@Override
protected int doHash(final JsonNode t)
{
/*
* If this is a numeric node, we want the same hashcode for the same
* mathematical values. Go with double, its range is good enough for
* 99+% of use cases.
*/
if (t.isNumber())
return Double.valueOf(t.doubleValue()).hashCode();
/*
* If this is a primitive type (other than numbers, handled above),
* delegate to JsonNode.
*/
if (!t.isContainerNode())
return t.hashCode();
/*
* The following hash calculations work, yes, but they are poor at best.
* And probably slow, too.
*
* TODO: try and figure out those hash classes from Guava
*/
int ret = 0;
/*
* If the container is empty, just return
*/
if (t.size() == 0)
return ret;
/*
* Array
*/
if (t.isArray()) {
for (final JsonNode element: t)
ret = 31 * ret + doHash(element);
return ret;
}
/*
* Not an array? An object.
*/
final Iterator> iterator = t.fields();
Map.Entry entry;
while (iterator.hasNext()) {
entry = iterator.next();
ret = 31 * ret
+ (entry.getKey().hashCode() ^ doHash(entry.getValue()));
}
return ret;
}
private static boolean numEquals(final JsonNode a, final JsonNode b)
{
/*
* If both numbers are integers, delegate to JsonNode.
*/
if (a.isIntegralNumber() && b.isIntegralNumber())
return a.equals(b);
/*
* Otherwise, compare decimal values.
*/
return a.decimalValue().compareTo(b.decimalValue()) == 0;
}
private boolean arrayEquals(final JsonNode a, final JsonNode b)
{
/*
* We are guaranteed here that arrays are the same size.
*/
final int size = a.size();
for (int i = 0; i < size; i++)
if (!doEquivalent(a.get(i), b.get(i)))
return false;
return true;
}
private boolean objectEquals(final JsonNode a, final JsonNode b)
{
/*
* Grab the key set from the first node
*/
final Set keys = Sets.newHashSet(a.fieldNames());
/*
* Grab the key set from the second node, and see if both sets are the
* same. If not, objects are not equal, no need to check for children.
*/
final Set set = Sets.newHashSet(b.fieldNames());
if (!set.equals(keys))
return false;
/*
* Test each member individually.
*/
for (final String key: keys)
if (!doEquivalent(a.get(key), b.get(key)))
return false;
return true;
}
}