diff --git a/src/main/java/tools/jackson/databind/DeserializationContext.java b/src/main/java/tools/jackson/databind/DeserializationContext.java index 5526e9d8d0..5b4112c3b4 100644 --- a/src/main/java/tools/jackson/databind/DeserializationContext.java +++ b/src/main/java/tools/jackson/databind/DeserializationContext.java @@ -2268,6 +2268,39 @@ public DatabindException missingInjectableValueException(String msg, valueId, forProperty, beanInstance); } + // XXX JREF handling start + public void enableJRefProcessing() { + setAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR, new ArrayList()); + } + + public boolean isJRefProcessingEnabled() { + return getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR) != null; + } + + public List getJRefResolvers() { + @SuppressWarnings("unchecked") + List jrefs = (List) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); + return jrefs == null?List.of():jrefs; + } + + public boolean addJRefResolver(JRefResolver resolver) { + @SuppressWarnings("unchecked") + List jrefs = (List) getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR); + if (jrefs != null) { + // If jrefs list is not set then ignore + return jrefs.add(resolver); + } + return false; + } + + public JRefPath findJRefPathForValue(Object value) { + if (getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR) != null && value instanceof JRefPath) { + return (JRefPath) value; + } else { + return null; + } + } + // XXX JREF handling end /* /********************************************************************** /* Other internal methods diff --git a/src/main/java/tools/jackson/databind/JRefModule.java b/src/main/java/tools/jackson/databind/JRefModule.java new file mode 100644 index 0000000000..e4824e38fc --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefModule.java @@ -0,0 +1,226 @@ +package tools.jackson.databind; + +import java.util.List; +import java.util.function.Function; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.BeanDescription.Supplier; +import tools.jackson.databind.deser.BeanDeserializerBuilder; +import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.deser.std.DelegatingDeserializer; +import tools.jackson.databind.introspect.BeanPropertyDefinition; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.TreeTraversingParser; +import tools.jackson.databind.type.ArrayType; +import tools.jackson.databind.type.CollectionLikeType; +import tools.jackson.databind.type.CollectionType; +import tools.jackson.databind.type.MapLikeType; +import tools.jackson.databind.type.MapType; +import tools.jackson.databind.type.ReferenceType; + +public class JRefModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public JRefModule() { + super("JRef"); + } + + protected class JRefValueDeserializerModifier extends ValueDeserializerModifier { + + private static final long serialVersionUID = 1L; + + protected ValueDeserializer makeJRefValueDeserializer(DeserializationConfig config, JavaType jt, + Supplier beanDescRef, ValueDeserializer d) { + return new JRefValueDeserializer(d); + } + + @Override + public ValueDeserializer modifyArrayDeserializer(DeserializationConfig config, ArrayType valueType, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, valueType, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyEnumDeserializer(DeserializationConfig config, JavaType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyCollectionLikeDeserializer(DeserializationConfig config, + CollectionLikeType type, Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyDeserializer(DeserializationConfig config, Supplier beanDescRef, + ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, null, beanDescRef, deserializer); + } + + @Override + public KeyDeserializer modifyKeyDeserializer(DeserializationConfig config, JavaType type, + KeyDeserializer deserializer) { + return super.modifyKeyDeserializer(config, type, deserializer); + } + + @Override + public ValueDeserializer modifyMapDeserializer(DeserializationConfig config, MapType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public ValueDeserializer modifyMapLikeDeserializer(DeserializationConfig config, MapLikeType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return makeJRefValueDeserializer(config, type, beanDescRef, deserializer); + } + + @Override + public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, Supplier beanDescRef, + BeanDeserializerBuilder builder) { + return super.updateBuilder(config, beanDescRef, builder); + } + + @Override + public List updateProperties(DeserializationConfig config, Supplier beanDescRef, + List propDefs) { + return super.updateProperties(config, beanDescRef, propDefs); + } + + @Override + public ValueDeserializer modifyReferenceDeserializer(DeserializationConfig config, ReferenceType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return super.modifyReferenceDeserializer(config, type, beanDescRef, deserializer); + } + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.addDeserializerModifier(new JRefValueDeserializerModifier()); + } + + public static class JRefValueDeserializer extends DelegatingDeserializer { + + void trace(String method, JRefValueDeserializer vds) { + System.out.println(new StringBuffer(method).append(".").append(vds.toString().toString())); + } + + @Override + public String toString() { + return "JRefValueDeserializer[delegate=" + _delegatee + ", class=" + _valueClass + "]"; + } + + void trace(String method) { + trace(method, this); + } + + public JRefValueDeserializer(ValueDeserializer src) { + super(src); + } + + protected class JRefFindResult { + JRefPath jrefPath; + JsonParser parser; + } + + protected TreeTraversingParser createTreeTraversingParser(JsonNode node) { + TreeTraversingParser result = new TreeTraversingParser(node); + result.nextToken(); + return result; + } + + /** + * Find JRef in parser input stream. + * + * @param p JsonParser to use + * @param ctxt the current context + * @param typeDeserializer optional typeDeserializer + * @return JRefFindResult with JRefPath either set to non null (path found), or + * set to null (meaning that the further deserialization should be + * undertaken with the parse given in JRefFindResult + */ + protected JRefFindResult findJRef(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) { + JRefFindResult result = new JRefFindResult(); + JsonToken tok = p.currentToken(); + if (tok == JsonToken.START_OBJECT) { + JsonNode n = ctxt.readTree(p); + if (n instanceof ObjectNode) { + ObjectNode on = (ObjectNode) n; + JsonNode valNode = on.get(JRefPath.JREF_REF); + if (valNode != null) { + String valStr = valNode.asString(); + if (valStr != null) { + result.jrefPath = new JRefPath(valStr, ctxt, getDelegatee(), typeDeserializer); + result.parser = p; + return result; + } + // JREF_REF found, but no/null path. This is a syntax error + throw DatabindException.from(p, "JRefPath detected on stream but path is null", null); + } + } + result.parser = createTreeTraversingParser(n); + } else { + result.parser = p; + } + return result; + } + + /** + * Deserialize with jref handling. Calls + * {@link #findJRef(JsonParser, DeserializationContext, TypeDeserializer)} to + * look on the parser stream for objects with 1 key == '$ref'. If found then the + * JRefPath is returned. If not found then the func arg is applied with the + * appropriate parser returned in JRefFindResult.parser field. + * + * @param p + * @param ctxt + * @param typeDeserializer + * @param func + * @return + */ + protected Object deserializerWithJRef(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer, Function func) { + Object result = null; + JRefFindResult findResult = findJRef(p, ctxt, typeDeserializer); + if (findResult.jrefPath != null) { + result = findResult.jrefPath; + } else { + result = func.apply(findResult.parser); + } + return result; + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + throws JacksonException { + return deserializerWithJRef(p, ctxt, typeDeserializer, + ps -> super.deserializeWithType(ps, ctxt, typeDeserializer)); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + return deserializerWithJRef(p, ctxt, null, ps -> super.deserialize(ps, ctxt)); + } + + @Override + protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { + return new JRefValueDeserializer(newDelegatee); + } + + } + +} diff --git a/src/main/java/tools/jackson/databind/JRefPath.java b/src/main/java/tools/jackson/databind/JRefPath.java new file mode 100644 index 0000000000..04c8a1f231 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefPath.java @@ -0,0 +1,52 @@ +package tools.jackson.databind; + +import java.util.Objects; + +import tools.jackson.databind.jsontype.TypeDeserializer; + +public class JRefPath { + + public static final String JREF_REF = "$ref"; + private final DeserializationContext ctxt; + private final String path; + private final ValueDeserializer deserializer; + private final TypeDeserializer typeDeserializer; + + public JRefPath(String path, DeserializationContext ctxt, ValueDeserializer deserializer, + TypeDeserializer typeDeserializer) { + Objects.requireNonNull(path, "path cannot be null"); + this.path = path; + Objects.requireNonNull(ctxt, "ctxt cannot be null"); + this.ctxt = ctxt; + Objects.requireNonNull(deserializer, "deserializer cannot be null"); + this.deserializer = deserializer; + this.typeDeserializer = typeDeserializer; + } + + public JRefPath(String path, DeserializationContext ctxt, ValueDeserializer deserializer) { + this(path, ctxt, deserializer, null); + } + + public DeserializationContext getContext() { + return this.ctxt; + } + + public String getPath() { + return this.path; + } + + public ValueDeserializer getValueDeserializer() { + return deserializer; + } + + public TypeDeserializer getTypeDeserializer() { + return typeDeserializer; + } + + @Override + public String toString() { + return "JRefPath[ctxt=" + ctxt + ", path=" + path + ", deserializer=" + deserializer + ", typeDeserializer=" + + typeDeserializer + "]"; + } + +} diff --git a/src/main/java/tools/jackson/databind/JRefResolveException.java b/src/main/java/tools/jackson/databind/JRefResolveException.java new file mode 100644 index 0000000000..17657291e9 --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefResolveException.java @@ -0,0 +1,39 @@ +package tools.jackson.databind; + +public class JRefResolveException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final JRefResolver resolver; + private final Object root; + + public JRefResolveException(JRefResolver resolver, Object root, String message, Throwable cause) { + super(message, cause); + this.resolver = resolver; + this.root = root; + } + + public JRefResolveException(JRefResolver resolver, Object root, String message) { + super(message); + this.resolver = resolver; + this.root = root; + } + + public JRefResolveException(JRefResolver resolver, String message) { + this(resolver, null, message); + } + + public JRefResolver getResolver() { + return this.resolver; + } + + public Object getRoot() { + return this.root; + } + + @Override + public String toString() { + return "JRefResolveException [resolver=" + resolver + ", root=" + root + ", message=" + super.getMessage() + + "]"; + } + +} diff --git a/src/main/java/tools/jackson/databind/JRefResolver.java b/src/main/java/tools/jackson/databind/JRefResolver.java new file mode 100644 index 0000000000..93ed62861b --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefResolver.java @@ -0,0 +1,172 @@ +package tools.jackson.databind; + +import java.lang.reflect.Field; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class JRefResolver { + + public static final String JREF_RESOLVER_LIST_CONTEXT_ATTR = JRefResolver.class.getName() + ".jrefs"; + + private final JRefPath jrefPath; + private final SetterFunction setter; + + public JRefResolver(JRefPath jrefPath, SetterFunction setter) { + Objects.requireNonNull(jrefPath, "jrefPath must not be null"); + this.jrefPath = jrefPath; + Objects.requireNonNull(setter, "setter function must must not be null"); + this.setter = setter; + } + + public Object resolve(Object root) throws JRefResolveException { + if (root == null) { + throw new JRefResolveException(this, "Root object cannot be null"); + } + // Now that we have the root, we can lookup the object at path + Object value = resolvePathToValue(root); + try { + return this.setter.set(value); + } catch (Throwable e) { + throw new JRefResolveException(this, root, "Exception setting value=" + value, e); + } + } + + private static String decode_uri(String uri) { + try { + return URLDecoder.decode(uri, StandardCharsets.UTF_8.toString()); + } catch (Exception e) { + return uri; + } + } + + protected String unescape(String segment) { + return segment.replace("~1", "/").replace("~0", "~"); + } + + protected String escape(String segment) { + return segment.replace("~", "~0").replace("/", "~1"); + } + + protected Iterable pointerSegments(String pointer) { + if (pointer.length() > 0 && !pointer.startsWith("/")) { + throw new IllegalArgumentException("Invalid JSON Pointer"); + } + + List segments = new ArrayList<>(); + int segmentStart = 1; + int segmentEnd; + + while (segmentStart <= pointer.length()) { + int position = pointer.indexOf("/", segmentStart); + segmentEnd = (position == -1) ? pointer.length() : position; + String segment = pointer.substring(segmentStart, segmentEnd); + segmentStart = segmentEnd + 1; + + segments.add(unescape(segment)); + + // If the pointer ended with a '/', we need to add an empty segment for the + // trailing slash + if (position != -1 && segmentStart > pointer.length()) { + segments.add(""); + } + } + + return segments; + } + + protected Object computeSegment(Object value, String segment) { + if (value instanceof List) { + return "-".equals(segment) ? ((List) value).size() : Integer.parseInt(segment); + } else { + return segment; + } + } + + protected Object get(String pointer, Object subject) { + if (subject == null) { + final List segments = new ArrayList<>(); + pointerSegments(pointer).forEach(segments::add); + return (Function) (Object s) -> _get(segments, s); + } else { + return _get(pointerSegments(pointer), subject); + } + } + + protected Object applySegment(Object value, Object segment, String cursor) { + if (value == null) { + throw new RuntimeException(String.format("Value at '%s' is %s and does not have property '%s'", cursor, + (cursor.isEmpty() ? "null" : "undefined"), segment)); + } else { + Object computedSegment = computeSegment(value, String.valueOf(segment)); + if (value instanceof Map) { + Map map = (Map) value; + if (map.containsKey(computedSegment)) { + return map.get(computedSegment); + } + } else if (value instanceof List) { + List list = (List) value; + if (computedSegment instanceof Integer) { + int index = (Integer) computedSegment; + if (index >= 0 && index < list.size()) { + return list.get(index); + } + } + } + return getAccessibleFieldValue(value, String.valueOf(computedSegment)); + } + } + + protected Field getAccessibleField(Class clazz, String fieldName) { + try { + Field f = clazz.getDeclaredField(fieldName); + f.setAccessible(true); + return f; + } catch (NoSuchFieldException | SecurityException e) { + throw new RuntimeException(String.format("Could not find field on class=%s with name=%s", clazz, fieldName), + e); + } + } + + protected Object getAccessibleFieldValue(Object value, String fieldName) { + try { + return getAccessibleField(value.getClass(), fieldName).get(value); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException( + String.format("Could not get value for field=%s on object=%s with field", fieldName, value)); + } + } + + protected Object _get(Iterable segments, Object subject) { + String cursor = ""; + for (String segment : segments) { + subject = applySegment(subject, segment, cursor); + cursor = append(segment, cursor); + } + return subject; + } + + protected String append(Object segment, String pointer) { + return pointer + "/" + escape(String.valueOf(segment)); + } + + protected Object resolvePathToValue(Object root) { + String refStr = this.jrefPath.getPath(); + String[] parts = refStr.split("#", 2); + if (parts.length > 1) { + Object refValue = get(decode_uri(parts[1]), root); + if (refValue == null) { + throw new JRefResolveException(this, root, + "Invalid local reference: path=" + this.jrefPath.getPath() + " not found on root=" + root); + } + return refValue; + } + throw new JRefResolveException(this, root, + "Invalid local reference: path=" + this.jrefPath.getPath() + " does not have preceding '#'"); + } + +} diff --git a/src/main/java/tools/jackson/databind/ObjectMapper.java b/src/main/java/tools/jackson/databind/ObjectMapper.java index f1651c0444..54c369f766 100644 --- a/src/main/java/tools/jackson/databind/ObjectMapper.java +++ b/src/main/java/tools/jackson/databind/ObjectMapper.java @@ -2666,6 +2666,8 @@ protected Object _readMapAndClose(DeserializationContextExt ctxt, result = ctxt.readRootValue(p, valueType, _findRootDeserializer(ctxt, valueType), null); ctxt.checkUnresolvedObjectId(); + // XXX JREF resolve jrefs + ctxt.getJRefResolvers().forEach(r -> r.resolve(result)); } if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, valueType); @@ -2725,8 +2727,14 @@ protected DeserializationContextExt _deserializationContext(JsonParser p) { // NOTE: only public to allow for testing public DeserializationContextExt _deserializationContext() { - return _deserializationContexts.createContext(deserializationConfig(), + DeserializationContextExt result = _deserializationContexts.createContext(deserializationConfig(), /* FormatSchema */ null, _injectableValues); + for(JacksonModule m: registeredModules()) { + if (m instanceof JRefModule) { + result.enableJRefProcessing(); + } + } + return result; } // 15-Feb-2026, tatu: Unused by databind itself diff --git a/src/main/java/tools/jackson/databind/SetterFunction.java b/src/main/java/tools/jackson/databind/SetterFunction.java new file mode 100644 index 0000000000..926e353290 --- /dev/null +++ b/src/main/java/tools/jackson/databind/SetterFunction.java @@ -0,0 +1,7 @@ +package tools.jackson.databind; + +@FunctionalInterface +public interface SetterFunction { + + public Object set(Object v) throws Throwable; +} diff --git a/src/main/java/tools/jackson/databind/ValueDeserializer.java b/src/main/java/tools/jackson/databind/ValueDeserializer.java index 47c725610f..554cbc2d28 100644 --- a/src/main/java/tools/jackson/databind/ValueDeserializer.java +++ b/src/main/java/tools/jackson/databind/ValueDeserializer.java @@ -479,11 +479,7 @@ public Boolean supportsUpdate(DeserializationConfig config) { } /** - * Method to collect all property names including nested unwrapped properties. - *

- * NOTE: if no names are returned, properties are considered to be - * unknown and caller will NOT assume names are statically known: this - * can affect processing of things like unwrapped properties. + * Method to collect all property names including nested unwrapped properties * * @param names (not null) Set to add property names to; for both regular * and "any" properties. @@ -502,6 +498,15 @@ public boolean hasAnySetter() { return false; } + // XXX JREF support method for subclasses + public JRefPath findJRefPathFromValue(DeserializationContext ctxt, Object value) { + if (ctxt.getAttribute(JRefResolver.JREF_RESOLVER_LIST_CONTEXT_ATTR) != null && value instanceof JRefPath) { + return (JRefPath) value; + } else { + return null; + } + } + /* /********************************************************************** /* Helper classes diff --git a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java index 4d8f4b78c9..eaad0c58e2 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java @@ -128,10 +128,23 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } - try { - _setter.get().invokeExact(instance, value); - } catch (Throwable e) { - _throwAsJacksonE(p, e, value); + + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise returns null + JRefPath jrefPath = _valueDeserializer.findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + // This is only called if JRefPath is returned from find method above + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + return setAndReturn(ctxt, instance, v); + })); + } else { + try { + _setter.get().invokeExact(instance, value); + } catch (Throwable e) { + _throwAsJacksonE(p, e, value); + } } } diff --git a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java index 56af932724..9491404189 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java @@ -398,8 +398,20 @@ protected Collection _deserializeFromArray(JsonParser p, Deserialization continue; } } - - result.add(value); + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise return null + JRefPath jrefPath = findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + result.add(v); + return result; + })); + } else { + // do whatever was done before JRefPath + result.add(value); + } /* 17-Dec-2017, tatu: should not occur at this level... } catch (UnresolvedForwardReference reference) { diff --git a/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java index 8c01b0a445..83e4b16950 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/MapDeserializer.java @@ -1,15 +1,38 @@ package tools.jackson.databind.deser.jdk; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIncludeProperties; -import tools.jackson.core.*; -import tools.jackson.databind.*; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.JsonTokenId; +import tools.jackson.core.StreamReadCapability; +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JRefPath; +import tools.jackson.databind.JRefResolver; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ValueDeserializer; import tools.jackson.databind.annotation.JacksonStdImpl; -import tools.jackson.databind.deser.*; +import tools.jackson.databind.deser.ContextualKeyDeserializer; +import tools.jackson.databind.deser.NullValueProvider; import tools.jackson.databind.deser.ReadableObjectId.Referring; +import tools.jackson.databind.deser.SettableBeanProperty; +import tools.jackson.databind.deser.UnresolvedForwardReference; +import tools.jackson.databind.deser.ValueInstantiator; import tools.jackson.databind.deser.bean.PropertyBasedCreator; import tools.jackson.databind.deser.bean.PropertyValueBuffer; import tools.jackson.databind.deser.std.ContainerDeserializerBase; @@ -668,9 +691,25 @@ protected final Map _readAndBindStringKeyMap(JsonParser p, Deseri if (useObjectId) { referringAccumulator.put(key, value); } else { - Object oldValue = result.put(key, value); - if (oldValue != null) { - _squashDups(ctxt, result, key, oldValue, value); + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise return null + JRefPath jrefPath = findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + final String k = key; + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + Object oldValue = result.put(k, v); + if (oldValue != null) { + _squashDups(ctxt, result, k, oldValue, v); + } + return result; + })); + } else { + Object oldValue = result.put(key, value); + if (oldValue != null) { + _squashDups(ctxt, result, key, oldValue, value); + } } } } catch (UnresolvedForwardReference reference) { @@ -684,7 +723,7 @@ protected final Map _readAndBindStringKeyMap(JsonParser p, Deseri return result; } - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") public Map _deserializeUsingCreator(JsonParser p, DeserializationContext ctxt) throws JacksonException { diff --git a/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java index 405dab1b60..b13a9560d9 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/StringCollectionDeserializer.java @@ -286,7 +286,8 @@ private Collection deserializeUsingCustom(JsonParser p, DeserializationC * notably XML. Note, however, that while we can get String, we can't * assume that's what we use due to custom deserializer */ - String value; + // XXX JRef + Object value; if (p.nextStringValue() == null) { JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { @@ -312,8 +313,19 @@ private Collection deserializeUsingCustom(JsonParser p, DeserializationC continue; } } - - result.add(value); + // XXX JREF handling + // Given ctxt/config and value, return JRefPath if value + // is instanceof JRefPath returned, otherwise return null + JRefPath jrefPath = findJRefPathFromValue(ctxt, value); + // Check for null/non-null + if (jrefPath != null) { + ctxt.addJRefResolver(new JRefResolver(jrefPath, (v) -> { + result.add((String) v); + return result; + })); + } else { + result.add((String) value); + } } } catch (Exception e) { throw DatabindException.wrapWithPath(ctxt, e, diff --git a/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java new file mode 100644 index 0000000000..467d0d274d --- /dev/null +++ b/src/test/java/tools/jackson/databind/deser/bean/JRefBeanDeserializerTest.java @@ -0,0 +1,200 @@ +package tools.jackson.databind.deser.bean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static tools.jackson.databind.testutil.DatabindTestUtil.jsonMapperBuilder; + +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.databind.JRefModule; +import tools.jackson.databind.ObjectMapper; + +public class JRefBeanDeserializerTest { + + static class IntType { + @JsonProperty + int i; + } + + static class IntItems { + @JsonProperty + int j; + @JsonProperty + List items; + } + + @Test + public void testIntItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"j\": 20, \"items\":[ { \"i\": 10}, { \"i\": { \"$ref\": \"#/items/0/i\" }}, { \"i\": { \"$ref\": \"#/j\" }}]}"; + IntItems result = mapper.readValue(input, IntItems.class); + assertEquals(result.items.get(0).i, result.items.get(1).i); + assertEquals(result.j, result.items.get(2).i); + } + + static class StringItems { + @JsonProperty + List items; + @JsonProperty + String second; + } + + @Test + public void testStringItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[\"hello\", { \"$ref\": \"#/items/0\" }], \"second\": { \"$ref\": \"#/items/0\" }}"; + StringItems result = mapper.readValue(input, StringItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + assertEquals(result.items.get(0), result.second); + } + + static class IntegerItems { + @JsonProperty + List items; + } + + @Test + public void testIntegerItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5, { \"$ref\": \"#/items/0\" }]}"; + IntegerItems result = mapper.readValue(input, IntegerItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class DoubleItems { + @JsonProperty + List items; + } + + @Test + public void testDoubleItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + DoubleItems result = mapper.readValue(input, DoubleItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class FloatItems { + @JsonProperty + List items; + } + + @Test + public void testFloatItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + FloatItems result = mapper.readValue(input, FloatItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class BooleanItems { + @JsonProperty + List items; + } + + @Test + public void testBooleanItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[true, { \"$ref\": \"#/items/0\" }]}"; + BooleanItems result = mapper.readValue(input, BooleanItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class Human { + @JsonProperty + String name; + @JsonProperty + Human parent; + @JsonProperty + Map props; + @JsonProperty + Human o; + @JsonProperty + String otherName; + @JsonProperty + Map moreProps; + + public Human() { + } + + @Override + public String toString() { + return "Human[name=" + name + ", parent=" + parent + ", props=" + props + ", o=" + this.o + "]"; + } + + } + + static class Message { + @JsonProperty + List items; + + public Message(List items) { + this.items = items; + } + + @Override + public String toString() { + return "Message[items=" + items + "]"; + } + } + + protected ObjectMapper buildObjectMapperWithJRefSupport() { + return jsonMapperBuilder().addModule(new JRefModule()).build(); + } + + @Test + public void testStringItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 }, \"otherName\": { \"$ref\": \"#/items/0/name\" } }]}"; + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0).name, msg.items.get(0).otherName); + } + + @Test + public void testCollectionStringKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"$ref\": \"#/items/0\" }]}"; + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0), msg.items.get(1)); + } + + @Test + public void testCollectionObjectKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"name\": \"wendy\", \"parent\": null, \"moreProps\": { \"$ref\": \"#/items/0/props\" }}]}"; + + Message msg = mapper.readValue(message, Message.class); + Assert.assertEquals(msg.items.get(0).props, msg.items.get(1).moreProps); + } + + @Test + public void testJRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + String message = "{\r\n" + " \"items\" : [ {\r\n" + " \"name\" : \"wendy\",\r\n" + " \"parent\" : {\r\n" + + " \"name\" : \"sam\",\r\n" + " \"parent\" : null,\r\n" + " \"props\" : {\r\n" + + " \"s1\" : 1\r\n" + " }\r\n" + " },\r\n" + " \"props\" : {\r\n" + + " \"q\" : \"r\",\r\n" + " \"p\" : { \"$ref\" : \"#/items/0/parent\" }" + " }\r\n" + + " }, {\r\n" + " \"name\" : \"rick\",\r\n" + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/items/0/parent\"\r\n" + " },\r\n" + + " \"o\" : { \"$ref\" : \"#/items/0/parent\" }\r\n" + " } ]\r\n" + "}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0).parent, msg.items.get(0).props.get("p")); + assertEquals(msg.items.get(0).parent, msg.items.get(1).parent); + } + +}