Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for sorted trees #3995

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.util.RawValue;
Expand Down Expand Up @@ -49,9 +52,32 @@ public class JsonNodeFactory
*/
protected final static int MAX_ELEMENT_INDEX_FOR_INSERT = 9999;

/**
* Default factory for the {@link Map} to use for children of {@link ObjectNode}.
*
* @since 2.16
*/
public static final SerializableSupplier<Map<String, JsonNode>> DEFAULT_OBJECT_NODE_CHILDREN_FACTORY = LinkedHashMap::new;

/**
* Factory for the {@link Map} to use for children of {@link ObjectNode} which sorts the entries by name.
*
* @since 2.16
*/
public static final SerializableSupplier<Map<String, JsonNode>> SORTED_OBJECT_NODE_CHILDREN_FACTORY = TreeMap::new;

@Deprecated // as of 2.15
private final boolean _cfgBigDecimalExact;

/**
* A factory for the {@link Map} to use for children of {@link ObjectNode}.
*
* <p>The default is a map which keeps the order in which children were added.
*
* @since 2.16
*/
private final SerializableSupplier<Map<String, JsonNode>> _objectNodeChildrenFactory;

/**
* Default singleton instance that construct "standard" node instances:
* given that this class is stateless, a globally shared singleton
Expand All @@ -66,7 +92,7 @@ public class JsonNodeFactory
*/
public JsonNodeFactory(boolean bigDecimalExact)
{
_cfgBigDecimalExact = bigDecimalExact;
this(bigDecimalExact, DEFAULT_OBJECT_NODE_CHILDREN_FACTORY);
}

/**
Expand All @@ -76,7 +102,22 @@ public JsonNodeFactory(boolean bigDecimalExact)
*/
protected JsonNodeFactory()
{
this(false);
this(false, DEFAULT_OBJECT_NODE_CHILDREN_FACTORY);
}

/**
* @since 2.16
*/
public JsonNodeFactory(SerializableSupplier<Map<String, JsonNode>> objectNodeChildrenFactory) {
this(false, objectNodeChildrenFactory);
}

/**
* @since 2.16
*/
public JsonNodeFactory(boolean bigDecimalExact, SerializableSupplier<Map<String, JsonNode>> objectNodeChildrenFactory) {
_cfgBigDecimalExact = bigDecimalExact;
_objectNodeChildrenFactory = objectNodeChildrenFactory;
}

/**
Expand Down Expand Up @@ -350,8 +391,13 @@ public BinaryNode binaryNode(byte[] data, int offset, int length) {
* Factory method for constructing an empty JSON Object ("struct") node
*/
@Override
public ObjectNode objectNode() { return new ObjectNode(this); }
public ObjectNode objectNode() { return new ObjectNode(this, _objectNodeChildrenFactory.get()); }

/** @since 2.16 */
public Map<String, JsonNode> objectNodeChildren() {
return _objectNodeChildrenFactory.get();
}

/**
* Factory method for constructing a wrapper for POJO
* ("Plain Old Java Object") objects; these will get serialized
Expand All @@ -378,5 +424,25 @@ protected boolean _inIntRange(long l)
long l2 = (long) i;
return (l2 == l);
}

/**
* Alternate method to copy trees of JsonNode. This method here applies the configuration of this
* {@link JsonNodeFactory} to the copied nodes.
*
* @since 2.16
*/
public JsonNode deepCopy(JsonNode orig) {
if (orig instanceof ObjectNode) {
ObjectNode result = objectNode();

for(Map.Entry<String, JsonNode> entry: orig.properties())
result.set(entry.getKey(), deepCopy(entry.getValue()));

return result;
}

// No other node has any special handling, so we can let the other factory copy them.
return orig.deepCopy();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class ObjectNode

public ObjectNode(JsonNodeFactory nc) {
super(nc);
_children = new LinkedHashMap<String, JsonNode>();
_children = nc == null ? JsonNodeFactory.DEFAULT_OBJECT_NODE_CHILDREN_FACTORY.get() : nc.objectNodeChildren();
}

/**
Expand All @@ -53,7 +53,7 @@ protected JsonNode _at(JsonPointer ptr) {
@Override
public ObjectNode deepCopy()
{
ObjectNode ret = new ObjectNode(_nodeFactory);
ObjectNode ret = objectNode();

for (Map.Entry<String, JsonNode> entry: _children.entrySet())
ret._children.put(entry.getKey(), entry.getValue().deepCopy());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.fasterxml.jackson.databind.node;

import java.io.Serializable;
import java.util.function.Supplier;

public interface SerializableSupplier<T> extends Supplier<T>, Serializable {
// empty
}
4 changes: 4 additions & 0 deletions src/test/java/com/fasterxml/jackson/databind/BaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ public String quote(String str) {
protected static String a2q(String json) {
return json.replace("'", "\"");
}

protected static String q2a(String json) {
return json.replace("\"", "'");
}

@Deprecated // use a2q
protected static String aposToQuotes(String json) {
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/com/fasterxml/jackson/databind/ObjectWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.io.SerializedString;
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
Expand All @@ -35,6 +37,10 @@ public void close() throws IOException {

final ObjectMapper MAPPER = new ObjectMapper();

private final ObjectMapper SORTED_MAPPER = JsonMapper.builder()
.nodeFactory(new JsonNodeFactory(JsonNodeFactory.SORTED_OBJECT_NODE_CHILDREN_FACTORY))
.build();

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
static class PolyBase {
}
Expand Down Expand Up @@ -119,6 +125,21 @@ public void testPolymorphicWithTyping() throws Exception
json = writer.writeValueAsString(new ImplB(-5));
assertEquals(a2q("{'type':'B','b':-5}"), json);
}

public void testJsonTypeInfoWithSorting() throws Exception
{
assertSerialization("{'type':'A','value':3}", new ImplA(3), SORTED_MAPPER);
}

public void testJsonTypeInfoWithSorting2() throws Exception
{
assertSerialization("{'b':-5,'type':'B'}", new ImplB(-5), SORTED_MAPPER);
}

private void assertSerialization(String expected, Object value, ObjectMapper mapper) throws Exception {
JsonNode json = mapper.valueToTree(value);
assertEquals(a2q(expected), mapper.writeValueAsString(json));
}

public void testCanSerialize() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.json.JsonMapper;

/**
* Additional tests for {@link ObjectNode} container class.
Expand Down Expand Up @@ -64,6 +65,10 @@ static class MyValue
*/

private final ObjectMapper MAPPER = newJsonMapper();

private final ObjectMapper SORTED_MAPPER = JsonMapper.builder()
.nodeFactory(new JsonNodeFactory(JsonNodeFactory.SORTED_OBJECT_NODE_CHILDREN_FACTORY))
.build();

public void testSimpleObject() throws Exception
{
Expand Down Expand Up @@ -120,6 +125,26 @@ public void testSimpleObject() throws Exception
assertNull(root.get("b"));
}

public void testSortedReadTree() throws Exception
{
String JSON = "{ \"key\" : 1, \"b\" : \"x\" }";
JsonNode root = SORTED_MAPPER.readTree(JSON);
assertKeys("b, key", root);
}

private void assertKeys(String expected, JsonNode node) {
assertTrue(node instanceof ObjectNode);
String actual = keysOf(node).stream()
.collect(Collectors.joining(", "));
assertEquals(expected, actual);
}

private List<String> keysOf(JsonNode node) {
List<String> result = new ArrayList<>();
node.fieldNames().forEachRemaining(result::add);
return result;
}

// for [databind#346]
public void testEmptyNodeAsValue() throws Exception
{
Expand Down Expand Up @@ -202,6 +227,40 @@ public void testBasicsPutSet()
assertEquals("foobar", old.textValue());
}

public void testPreserveOrder()
{
final JsonNodeFactory f = new JsonNodeFactory();
ObjectNode root = f.objectNode();

root.put("key", "foobar");
root.put("b", 1);

assertKeys("key, b", root);
}

public void testSorted()
{
final JsonNodeFactory f = new JsonNodeFactory(JsonNodeFactory.SORTED_OBJECT_NODE_CHILDREN_FACTORY);
ObjectNode root = f.objectNode();

root.put("key", "foobar");
root.put("b", 1);

assertKeys("b, key", root);
}

public void testCopy()
{
final JsonNodeFactory f = new JsonNodeFactory(JsonNodeFactory.SORTED_OBJECT_NODE_CHILDREN_FACTORY);
ObjectNode root = f.objectNode();

root.put("key", "foobar");
root.put("b", 1);

ObjectNode copy = root.deepCopy();
assertKeys("b, key", copy);
}

public void testBigNumbers()
{
ObjectNode n = new ObjectNode(JsonNodeFactory.instance);
Expand Down Expand Up @@ -465,6 +524,25 @@ public void testEqualityWrtOrder() throws Exception
assertTrue(ob1.equals(ob2));
assertTrue(ob2.equals(ob1));
}

public void testEqualityWrtOrder2() throws Exception
{
ObjectNode ob1 = SORTED_MAPPER.createObjectNode();
ObjectNode ob2 = MAPPER.createObjectNode();

// same contents, different insertion order; should not matter

ob1.put("a", 1);
ob1.put("b", 2);
ob1.put("c", 3);

ob2.put("b", 2);
ob2.put("c", 3);
ob2.put("a", 1);

assertTrue(ob1.equals(ob2));
assertTrue(ob2.equals(ob1));
}

public void testSimplePath() throws Exception
{
Expand Down Expand Up @@ -524,6 +602,15 @@ public void testPropertiesTraversal() throws Exception
assertEquals("a/1,b/true,c/\"stuff\"", _toString(n));
}

public void testSortTree() throws Exception {
JsonNode orig = MAPPER.readTree(a2q(
"{ 'b':1, 'x':true, 'a':{ '2': null, '1': 'stuff' }}"));
JsonNodeFactory sortedFactory = new JsonNodeFactory(JsonNodeFactory.SORTED_OBJECT_NODE_CHILDREN_FACTORY);
JsonNode sortedCopy = sortedFactory.deepCopy(orig);
String actual = q2a(MAPPER.writeValueAsString(sortedCopy));
assertEquals("{'a':{'1':'stuff','2':null},'b':1,'x':true}", actual);
}

private String _toString(JsonNode n) {
return n.properties().stream()
.map(e -> e.getKey() + "/" + e.getValue())
Expand Down