From 1d1da846802693d921807abb656fbfcad9cb5c90 Mon Sep 17 00:00:00 2001 From: NeumimTo Date: Fri, 22 Mar 2019 23:22:57 +0100 Subject: [PATCH] convert ObjectMapper to interface --- .../DefaultObjectMapperFactory.java | 14 +- .../objectmapping/GuiceObjectMapper.java | 2 +- .../GuiceObjectMapperFactory.java | 12 +- .../objectmapping/ObjectMapper.java | 217 +++------------ .../objectmapping/ObjectMapperImpl.java | 251 ++++++++++++++++++ ...st.java => GuiceObjectMapperImplTest.java} | 2 +- .../objectmapping/ObjectMapperTest.java | 4 +- 7 files changed, 298 insertions(+), 204 deletions(-) create mode 100644 configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapperImpl.java rename configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/{GuiceObjectMapperTest.java => GuiceObjectMapperImplTest.java} (97%) diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/DefaultObjectMapperFactory.java b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/DefaultObjectMapperFactory.java index 50c57ceec..dd773faf0 100644 --- a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/DefaultObjectMapperFactory.java +++ b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/DefaultObjectMapperFactory.java @@ -25,7 +25,7 @@ import java.util.concurrent.ExecutionException; /** - * Factory for a basic {@link ObjectMapper}. + * Factory for a basic {@link ObjectMapperImpl}. */ public class DefaultObjectMapperFactory implements ObjectMapperFactory { private static final ObjectMapperFactory INSTANCE = new DefaultObjectMapperFactory(); @@ -35,23 +35,23 @@ public static ObjectMapperFactory getInstance() { return INSTANCE; } - private final LoadingCache, ObjectMapper> mapperCache = CacheBuilder.newBuilder() + private final LoadingCache, ObjectMapperImpl> mapperCache = CacheBuilder.newBuilder() .weakKeys() .maximumSize(500) - .build(new CacheLoader, ObjectMapper>() { + .build(new CacheLoader, ObjectMapperImpl>() { @Override - public ObjectMapper load(Class key) throws Exception { - return new ObjectMapper<>(key); + public ObjectMapperImpl load(Class key) throws Exception { + return new ObjectMapperImpl<>(key); } }); @NonNull @Override @SuppressWarnings("unchecked") - public ObjectMapper getMapper(@NonNull Class type) throws ObjectMappingException { + public ObjectMapperImpl getMapper(@NonNull Class type) throws ObjectMappingException { Preconditions.checkNotNull(type, "type"); try { - return (ObjectMapper) mapperCache.get(type); + return (ObjectMapperImpl) mapperCache.get(type); } catch (ExecutionException e) { if (e.getCause() instanceof ObjectMappingException) { throw (ObjectMappingException) e.getCause(); diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapper.java b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapper.java index 129f34c37..81f2a6d14 100644 --- a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapper.java +++ b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapper.java @@ -28,7 +28,7 @@ * *

Instances of this object should be reached using a {@link GuiceObjectMapperFactory}.

*/ -class GuiceObjectMapper extends ObjectMapper { +class GuiceObjectMapper extends ObjectMapperImpl { private final Injector injector; private final Key typeKey; diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperFactory.java b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperFactory.java index 7edafeda1..5afd24e24 100644 --- a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperFactory.java +++ b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperFactory.java @@ -28,17 +28,17 @@ import java.util.concurrent.ExecutionException; /** - * A factory for {@link ObjectMapper}s that will inherit the injector from wherever it is provided. + * A factory for {@link ObjectMapperImpl}s that will inherit the injector from wherever it is provided. * *

This class is intended to be constructed through Guice dependency injection.

*/ @Singleton public final class GuiceObjectMapperFactory implements ObjectMapperFactory { - private final LoadingCache, ObjectMapper> cache = CacheBuilder.newBuilder() + private final LoadingCache, ObjectMapperImpl> cache = CacheBuilder.newBuilder() .weakKeys().maximumSize(512) - .build(new CacheLoader, ObjectMapper>() { + .build(new CacheLoader, ObjectMapperImpl>() { @Override - public ObjectMapper load(Class key) throws Exception { + public ObjectMapperImpl load(Class key) throws Exception { return new GuiceObjectMapper<>(injector, key); } }); @@ -53,10 +53,10 @@ protected GuiceObjectMapperFactory(Injector baseInjector) { @NonNull @Override @SuppressWarnings("unchecked") - public ObjectMapper getMapper(@NonNull Class type) throws ObjectMappingException { + public ObjectMapperImpl getMapper(@NonNull Class type) throws ObjectMappingException { Preconditions.checkNotNull(type, "type"); try { - return (ObjectMapper) cache.get(type); + return (ObjectMapperImpl) cache.get(type); } catch (ExecutionException e) { if (e.getCause() instanceof ObjectMappingException) { throw (ObjectMappingException) e.getCause(); diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapper.java b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapper.java index b197a7289..1d84fd414 100644 --- a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapper.java +++ b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapper.java @@ -17,31 +17,18 @@ package ninja.leaping.configurate.objectmapping; import com.google.common.base.Preconditions; -import com.google.common.reflect.TypeToken; import ninja.leaping.configurate.ConfigurationNode; -import ninja.leaping.configurate.commented.CommentedConfigurationNode; -import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer; import org.checkerframework.checker.nullness.qual.NonNull; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.Map; - /** - * This is the object mapper. It handles conversion between configuration nodes and + * This is the object mapper interface. Its implementation should handle conversion between configuration nodes and * fields annotated with {@link Setting} in objects. * - * Values in the node not used by the mapped object will be preserved. + * Values in the node not used by the mapped object should be preserved. * * @param The type to work with */ -public class ObjectMapper { - private final Class clazz; - private final Constructor constructor; - private final Map cachedFields = new HashMap<>(); - +public interface ObjectMapper { /** * Create a new object mapper that can work with objects of the given class using the @@ -53,7 +40,7 @@ public class ObjectMapper { * @throws ObjectMappingException If invalid annotated fields are presented */ @SuppressWarnings("unchecked") - public static ObjectMapper forClass(@NonNull Class clazz) throws ObjectMappingException { + static ObjectMapper forClass(@NonNull Class clazz) throws ObjectMappingException { return DefaultObjectMapperFactory.getInstance().getMapper(clazz); } @@ -66,81 +53,42 @@ public static ObjectMapper forClass(@NonNull Class clazz) throws Objec * @throws ObjectMappingException */ @SuppressWarnings("unchecked") - public static ObjectMapper.BoundInstance forObject(@NonNull T obj) throws ObjectMappingException { + static BoundInstance forObject(@NonNull T obj) throws ObjectMappingException { Preconditions.checkNotNull(obj); return forClass((Class) obj.getClass()).bind(obj); } /** - * Holder for field-specific information + * Returns whether this object mapper can create new object instances. This may be + * false if the provided class has no zero-argument constructors. + * + * @return Whether new object instances can be created */ - protected static class FieldData { - private final Field field; - private final TypeToken fieldType; - private final String comment; + boolean canCreateInstances(); - public FieldData(Field field, String comment) throws ObjectMappingException { - this.field = field; - this.comment = comment; - this.fieldType = TypeToken.of(field.getGenericType()); - } - - public void deserializeFrom(Object instance, ConfigurationNode node) throws ObjectMappingException { - TypeSerializer serial = node.getOptions().getSerializers().get(this.fieldType); - if (serial == null) { - throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " - + this.fieldType); - } - Object newVal = node.isVirtual() ? null : serial.deserialize(this.fieldType, node); - try { - if (newVal == null) { - Object existingVal = field.get(instance); - if (existingVal != null) { - serializeTo(instance, node); - } - } else { - field.set(instance, newVal); - } - } catch (IllegalAccessException e) { - throw new ObjectMappingException("Unable to deserialize field " + field.getName(), e); - } - } + /** + * Return a view on this mapper that is bound to a single object instance + * + * @param instance The instance to bind to + * @return A view referencing this mapper and the bound instance + */ + BoundInstance bind(T instance); - @SuppressWarnings("rawtypes") - public void serializeTo(Object instance, ConfigurationNode node) throws ObjectMappingException { - try { - Object fieldVal = this.field.get(instance); - if (fieldVal == null) { - node.setValue(null); - } else { - TypeSerializer serial = node.getOptions().getSerializers().get(this.fieldType); - if (serial == null) { - throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " + this.fieldType); - } - serial.serialize(this.fieldType, fieldVal, node); - } + /** + * Returns a view on this mapper that is bound to a newly created object instance + * + * @see #bind(Object) + * @return Bound mapper attached to a new object instance + * @throws ObjectMappingException If the object could not be constructed correctly + */ + BoundInstance bindToNew() throws ObjectMappingException; - if (node instanceof CommentedConfigurationNode && this.comment != null && !this.comment.isEmpty()) { - CommentedConfigurationNode commentNode = ((CommentedConfigurationNode) node); - if (!commentNode.getComment().isPresent()) { - commentNode.setComment(this.comment); - } - } - } catch (IllegalAccessException e) { - throw new ObjectMappingException("Unable to serialize field " + field.getName(), e); - } - } - } + Class getMappedType(); /** * Represents an object mapper bound to a certain instance of the object */ - public class BoundInstance { - private final T boundInstance; - - protected BoundInstance(T boundInstance) { - this.boundInstance = boundInstance; - } + interface BoundInstance { /** * Populate the annotated fields in a pre-created object @@ -149,13 +97,7 @@ protected BoundInstance(T boundInstance) { * @return The object provided, for easier chaining * @throws ObjectMappingException If an error occurs while populating data */ - public T populate(ConfigurationNode source) throws ObjectMappingException { - for (Map.Entry ent : cachedFields.entrySet()) { - ConfigurationNode node = source.getNode(ent.getKey()); - ent.getValue().deserializeFrom(boundInstance, node); - } - return boundInstance; - } + T populate(ConfigurationNode source) throws ObjectMappingException; /** * Serialize the data contained in annotated fields to the configuration node. @@ -163,112 +105,13 @@ public T populate(ConfigurationNode source) throws ObjectMappingException { * @param target The target node to serialize to * @throws ObjectMappingException if serialization was not possible due to some error. */ - public void serialize(ConfigurationNode target) throws ObjectMappingException { - for (Map.Entry ent : cachedFields.entrySet()) { - ConfigurationNode node = target.getNode(ent.getKey()); - ent.getValue().serializeTo(boundInstance, node); - } - } + void serialize(ConfigurationNode target) throws ObjectMappingException; /** * Return the instance this mapper is bound to. * * @return The active instance */ - public T getInstance() { - return boundInstance; - } - } - - /** - * Create a new object mapper of a given type - * - * @param clazz The type this object mapper will work with - * @throws ObjectMappingException if the provided class is in someway invalid - */ - protected ObjectMapper(Class clazz) throws ObjectMappingException { - this.clazz = clazz; - Constructor constructor = null; - try { - constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - } catch (NoSuchMethodException ignore) { - } - this.constructor = constructor; - Class collectClass = clazz; - do { - collectFields(cachedFields, collectClass); - } while (!(collectClass = collectClass.getSuperclass()).equals(Object.class)); - } - - protected void collectFields(Map cachedFields, Class clazz) throws ObjectMappingException { - for (Field field : clazz.getDeclaredFields()) { - if (field.isAnnotationPresent(Setting.class)) { - Setting setting = field.getAnnotation(Setting.class); - String path = setting.value(); - if (path.isEmpty()) { - path = field.getName(); - } - - FieldData data = new FieldData(field, setting.comment()); - field.setAccessible(true); - if (!cachedFields.containsKey(path)) { - cachedFields.put(path, data); - } - } - } - } - - /** - * Create a new instance of an object of the appropriate type. This method is not - * responsible for any population. - * - * @return The new object instance - * @throws ObjectMappingException If constructing a new instance was not possible - */ - protected T constructObject() throws ObjectMappingException { - if (constructor == null) { - throw new ObjectMappingException("No zero-arg constructor is available for class " + clazz + " but is required to construct new instances!"); - } - try { - return constructor.newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new ObjectMappingException("Unable to create instance of target class " + clazz, e); - } - } - - /** - * Returns whether this object mapper can create new object instances. This may be - * false if the provided class has no zero-argument constructors. - * - * @return Whether new object instances can be created - */ - public boolean canCreateInstances() { - return constructor != null; - } - - /** - * Return a view on this mapper that is bound to a single object instance - * - * @param instance The instance to bind to - * @return A view referencing this mapper and the bound instance - */ - public BoundInstance bind(T instance) { - return new BoundInstance(instance); - } - - /** - * Returns a view on this mapper that is bound to a newly created object instance - * - * @see #bind(Object) - * @return Bound mapper attached to a new object instance - * @throws ObjectMappingException If the object could not be constructed correctly - */ - public BoundInstance bindToNew() throws ObjectMappingException { - return new BoundInstance(constructObject()); - } - - public Class getMappedType() { - return this.clazz; + Object getInstance(); } } diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapperImpl.java b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapperImpl.java new file mode 100644 index 000000000..0e1e6cb5e --- /dev/null +++ b/configurate-core/src/main/java/ninja/leaping/configurate/objectmapping/ObjectMapperImpl.java @@ -0,0 +1,251 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ninja.leaping.configurate.objectmapping; + +import com.google.common.reflect.TypeToken; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.commented.CommentedConfigurationNode; +import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; + +/** + * This is the object mapper. It handles conversion between configuration nodes and + * fields annotated with {@link Setting} in objects. + * + * Values in the node not used by the mapped object will be preserved. + * + * @param The type to work with + */ +public class ObjectMapperImpl implements ObjectMapper { + private final Class clazz; + private final Constructor constructor; + private final Map cachedFields = new HashMap<>(); + + + /** + * Holder for field-specific information + */ + protected static class FieldData { + private final Field field; + private final TypeToken fieldType; + private final String comment; + + public FieldData(Field field, String comment) throws ObjectMappingException { + this.field = field; + this.comment = comment; + this.fieldType = TypeToken.of(field.getGenericType()); + } + + public void deserializeFrom(Object instance, ConfigurationNode node) throws ObjectMappingException { + TypeSerializer serial = node.getOptions().getSerializers().get(this.fieldType); + if (serial == null) { + throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " + + this.fieldType); + } + Object newVal = node.isVirtual() ? null : serial.deserialize(this.fieldType, node); + try { + if (newVal == null) { + Object existingVal = field.get(instance); + if (existingVal != null) { + serializeTo(instance, node); + } + } else { + field.set(instance, newVal); + } + } catch (IllegalAccessException e) { + throw new ObjectMappingException("Unable to deserialize field " + field.getName(), e); + } + } + + @SuppressWarnings("rawtypes") + public void serializeTo(Object instance, ConfigurationNode node) throws ObjectMappingException { + try { + Object fieldVal = this.field.get(instance); + if (fieldVal == null) { + node.setValue(null); + } else { + TypeSerializer serial = node.getOptions().getSerializers().get(this.fieldType); + if (serial == null) { + throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " + this.fieldType); + } + serial.serialize(this.fieldType, fieldVal, node); + } + + if (node instanceof CommentedConfigurationNode && this.comment != null && !this.comment.isEmpty()) { + CommentedConfigurationNode commentNode = ((CommentedConfigurationNode) node); + if (!commentNode.getComment().isPresent()) { + commentNode.setComment(this.comment); + } + } + } catch (IllegalAccessException e) { + throw new ObjectMappingException("Unable to serialize field " + field.getName(), e); + } + } + } + + /** + * Represents an object mapper bound to a certain instance of the object + */ + public class BoundInstanceImpl implements BoundInstance { + private final T boundInstance; + + protected BoundInstanceImpl(T boundInstance) { + this.boundInstance = boundInstance; + } + + /** + * Populate the annotated fields in a pre-created object + * + * @param source The source to get data from + * @return The object provided, for easier chaining + * @throws ObjectMappingException If an error occurs while populating data + */ + @Override + public T populate(ConfigurationNode source) throws ObjectMappingException { + for (Map.Entry ent : cachedFields.entrySet()) { + ConfigurationNode node = source.getNode(ent.getKey()); + ent.getValue().deserializeFrom(boundInstance, node); + } + return boundInstance; + } + + /** + * Serialize the data contained in annotated fields to the configuration node. + * + * @param target The target node to serialize to + * @throws ObjectMappingException if serialization was not possible due to some error. + */ + @Override + public void serialize(ConfigurationNode target) throws ObjectMappingException { + for (Map.Entry ent : cachedFields.entrySet()) { + ConfigurationNode node = target.getNode(ent.getKey()); + ent.getValue().serializeTo(boundInstance, node); + } + } + + /** + * Return the instance this mapper is bound to. + * + * @return The active instance + */ + @Override + public T getInstance() { + return boundInstance; + } + } + + /** + * Create a new object mapper of a given type + * + * @param clazz The type this object mapper will work with + * @throws ObjectMappingException if the provided class is in someway invalid + */ + protected ObjectMapperImpl(Class clazz) throws ObjectMappingException { + this.clazz = clazz; + Constructor constructor = null; + try { + constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + } catch (NoSuchMethodException ignore) { + } + this.constructor = constructor; + Class collectClass = clazz; + do { + collectFields(cachedFields, collectClass); + } while (!(collectClass = collectClass.getSuperclass()).equals(Object.class)); + } + + protected void collectFields(Map cachedFields, Class clazz) throws ObjectMappingException { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Setting.class)) { + Setting setting = field.getAnnotation(Setting.class); + String path = setting.value(); + if (path.isEmpty()) { + path = field.getName(); + } + + FieldData data = new FieldData(field, setting.comment()); + field.setAccessible(true); + if (!cachedFields.containsKey(path)) { + cachedFields.put(path, data); + } + } + } + } + + /** + * Create a new instance of an object of the appropriate type. This method is not + * responsible for any population. + * + * @return The new object instance + * @throws ObjectMappingException If constructing a new instance was not possible + */ + protected T constructObject() throws ObjectMappingException { + if (constructor == null) { + throw new ObjectMappingException("No zero-arg constructor is available for class " + clazz + " but is required to construct new instances!"); + } + try { + return constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new ObjectMappingException("Unable to create instance of target class " + clazz, e); + } + } + + /** + * Returns whether this object mapper can create new object instances. This may be + * false if the provided class has no zero-argument constructors. + * + * @return Whether new object instances can be created + */ + @Override + public boolean canCreateInstances() { + return constructor != null; + } + + /** + * Return a view on this mapper that is bound to a single object instance + * + * @param instance The instance to bind to + * @return A view referencing this mapper and the bound instance + */ + @Override + public BoundInstance bind(T instance) { + return new BoundInstanceImpl<>(instance); + } + + /** + * Returns a view on this mapper that is bound to a newly created object instance + * + * @see #bind(Object) + * @return Bound mapper attached to a new object instance + * @throws ObjectMappingException If the object could not be constructed correctly + */ + @Override + public BoundInstance bindToNew() throws ObjectMappingException { + return new BoundInstanceImpl<>(constructObject()); + } + + @Override + public Class getMappedType() { + return this.clazz; + } +} diff --git a/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperTest.java b/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperImplTest.java similarity index 97% rename from configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperTest.java rename to configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperImplTest.java index f47e77303..b38336e09 100644 --- a/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperTest.java +++ b/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/GuiceObjectMapperImplTest.java @@ -29,7 +29,7 @@ /** * Created by zml on 7/5/15. */ -public class GuiceObjectMapperTest { +public class GuiceObjectMapperImplTest { private static class TestModule extends AbstractModule { @Override diff --git a/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/ObjectMapperTest.java b/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/ObjectMapperTest.java index 3878eb87c..d9c44c89d 100644 --- a/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/ObjectMapperTest.java +++ b/configurate-core/src/test/java/ninja/leaping/configurate/objectmapping/ObjectMapperTest.java @@ -88,7 +88,7 @@ private static class CommentedObject { @Test public void testCommentsApplied() throws ObjectMappingException { CommentedConfigurationNode node = SimpleCommentedConfigurationNode.root(); - ObjectMapper.BoundInstance mapper = ObjectMapper.forClass(CommentedObject.class).bindToNew(); + ObjectMapper.BoundInstance mapper = ObjectMapper.forClass(CommentedObject.class).bindToNew(); CommentedObject obj = mapper.populate(node); obj.color = "fuchsia"; obj.politician = "All of them"; @@ -162,7 +162,7 @@ private static class InnerObject { @Test public void testNestedObjectWithComments() throws ObjectMappingException { CommentedConfigurationNode node = SimpleCommentedConfigurationNode.root(); - final ObjectMapper.BoundInstance mapper = ObjectMapper.forObject(new ParentObject()); + final ObjectMapper.BoundInstance mapper = ObjectMapper.forObject(new ParentObject()); mapper.populate(node); assertEquals("Comment on parent", node.getNode("inner").getComment().get()); assertTrue(node.getNode("inner").hasMapChildren());