You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2019/05/29 19:02:48 UTC

[sling-whiteboard] branch master updated: Adding Brendan Robert's Sling JCR Persist

This is an automated email from the ASF dual-hosted git repository.

dklco pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 473fb2e  Adding Brendan Robert's Sling JCR Persist
473fb2e is described below

commit 473fb2ec9b7ad02126c679817512e383234674e5
Author: Dan Klco <dk...@apache.org>
AuthorDate: Wed May 29 14:02:38 2019 -0500

    Adding Brendan Robert's Sling JCR Persist
---
 SlingJCRPersist/pom.xml                            | 170 +++++++++++
 SlingJCRPersist/src/main/assembly/felix.xml        |  27 ++
 .../injectors/MapOfChildResourcesInjector.java     | 113 +++++++
 .../apache/sling/models/persist/JcrPersist.java    |  20 ++
 .../models/persist/annotations/ChildType.java      |  31 ++
 .../persist/annotations/DirectDescendants.java     |  38 +++
 .../sling/models/persist/annotations/Ignore.java   |  37 +++
 .../models/persist/annotations/ResourceType.java   |  31 ++
 .../sling/models/persist/impl/JcrPersistImpl.java  |  52 ++++
 .../sling/models/persist/impl/JcrWriter.java       | 280 ++++++++++++++++++
 .../sling/models/persist/impl/ResourceTypeKey.java |  59 ++++
 .../models/persist/impl/util/AssertUtils.java      |  50 ++++
 .../models/persist/impl/util/ReflectionUtils.java  | 303 +++++++++++++++++++
 .../injectors/BeanWithDirectMappedChildren.java    |  48 +++
 .../models/injectors/BeanWithMappedChildren.java   |  46 +++
 .../injectors/MapOfChildResourcesInjectorTest.java |  96 ++++++
 .../apache/sling/models/persist/JcrWriterTest.java | 327 +++++++++++++++++++++
 .../persist/bean/BeanWithAnnotatedPathField.java   |  26 ++
 .../persist/bean/BeanWithAnnotatedPathGetter.java  |  30 ++
 .../models/persist/bean/BeanWithMappedNames.java   | 148 ++++++++++
 .../models/persist/bean/BeanWithPathField.java     |  23 ++
 .../models/persist/bean/BeanWithPathGetter.java    |  21 ++
 .../sling/models/persist/bean/ComplexBean.java     |  84 ++++++
 .../sling/models/persist/bean/MappedChildren.java  |  44 +++
 .../models/persist/impl/ResourceTypeKeyTest.java   |  52 ++++
 25 files changed, 2156 insertions(+)

diff --git a/SlingJCRPersist/pom.xml b/SlingJCRPersist/pom.xml
new file mode 100755
index 0000000..72c0be1
--- /dev/null
+++ b/SlingJCRPersist/pom.xml
@@ -0,0 +1,170 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.sling</groupId>
+    <artifactId>SlingJCRPersist</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Sling JCRPersist OSGi Bundle</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>5.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.jcr.api</artifactId>
+            <version>2.4.0</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.16.0</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <version>1.4.0</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.9</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.models.api</artifactId>
+            <version>1.3.0</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.log</artifactId>
+            <version>2.1.2</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <version>1</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>3.2.2</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>com.drewnoakes</groupId>
+            <artifactId>metadata-extractor</artifactId>
+            <version>2.6.2</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock</artifactId>
+            <version>2.3.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.jcr-mock</artifactId>
+            <version>1.4.4</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>3.11.1</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.9.6</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>2.9.0</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.9.6</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>5.0.0</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <version>5.0.0</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <type>jar</type>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <version>4.2.0</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-Activator>org.apache.sling.jcrpersist.Activator</Bundle-Activator>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/SlingJCRPersist/src/main/assembly/felix.xml b/SlingJCRPersist/src/main/assembly/felix.xml
new file mode 100755
index 0000000..05f14c5
--- /dev/null
+++ b/SlingJCRPersist/src/main/assembly/felix.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly>
+ <id>all</id>
+  <formats>
+    <format>zip</format>
+  </formats>
+  <dependencySets>
+    <dependencySet>
+        <useProjectArtifact>false</useProjectArtifact>
+        <outputDirectory>modules</outputDirectory>
+    </dependencySet>
+  </dependencySets>
+  <files>
+    <file>
+      <source>${project.build.directory}/${project.build.finalName}.jar</source>
+      <outputDirectory>modules</outputDirectory>
+    </file>
+    <file>
+      <source>${project.build.directory}/felix.jar</source>
+      <outputDirectory>bin</outputDirectory>
+    </file>
+    <file>
+      <source>${project.build.directory}/config.properties</source>
+      <outputDirectory>conf</outputDirectory>
+    </file>
+  </files>
+</assembly>
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/injectors/MapOfChildResourcesInjector.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/injectors/MapOfChildResourcesInjector.java
new file mode 100755
index 0000000..3a517dc
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/injectors/MapOfChildResourcesInjector.java
@@ -0,0 +1,113 @@
+package org.apache.sling.models.injectors;
+
+import com.drew.lang.annotations.NotNull;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.persist.annotations.DirectDescendants;
+import org.apache.sling.models.spi.AcceptsNullName;
+import org.apache.sling.models.spi.DisposalCallbackRegistry;
+import org.apache.sling.models.spi.Injector;
+import org.osgi.service.component.annotations.Component;
+
+@Component(service = Injector.class)
+public class MapOfChildResourcesInjector implements Injector, AcceptsNullName {
+
+    @Override
+    public @NotNull
+    String getName() {
+        return "map-of-child-resources";
+    }
+
+    @Override
+    public Object getValue(@NotNull Object adaptable, String name, @NotNull Type declaredType, @NotNull AnnotatedElement element,
+            @NotNull DisposalCallbackRegistry callbackRegistry) {
+        if (adaptable instanceof Resource) {
+            boolean directDescendants = element.getAnnotation(DirectDescendants.class) != null;
+            Resource source = ((Resource) adaptable);
+            if (!directDescendants) {
+                source = source.getChild(name);
+            }
+            return createMap(source != null ? source.getChildren() : Collections.EMPTY_LIST, declaredType);
+        } else if (adaptable instanceof SlingHttpServletRequest) {
+            return getValue(((SlingHttpServletRequest) adaptable).getResource(), name, declaredType, element, callbackRegistry);
+        }
+        return null;
+    }
+
+    // TODO: Clean up and refactor this method
+    private Object createMap(Iterable<Resource> children, Type declaredType) {
+        if (declaredType instanceof ParameterizedType) {
+            ParameterizedType type = (ParameterizedType) declaredType;
+            if (type.getRawType().equals(Map.class)) {
+                Type keyType = type.getActualTypeArguments()[0];
+                Type valueType = type.getActualTypeArguments()[1];
+                if (keyType.equals(String.class) && valueType instanceof Class) {
+                    Class<?> valueClass = (Class) valueType;
+                    return StreamSupport.stream(children.spliterator(), false).map(r ->
+                        buildSimpleMapEntry(valueClass, r)
+                    ).filter(o -> o != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
+                }
+            } else if (type.getRawType().equals(EnumMap.class)) {
+                Class keyType = (Class) type.getActualTypeArguments()[0];
+                Map<String, Object> enumKeys = EnumUtils.getEnumMap(keyType);
+                Type valueType = type.getActualTypeArguments()[1];
+                Boolean isSingleValue = valueType instanceof Class;
+                Class<?> valueClass = isSingleValue
+                        ? (Class) valueType
+                        : (Class) ((ParameterizedType) valueType).getActualTypeArguments()[0];
+                EnumMap out = new EnumMap(keyType);
+                StreamSupport.stream(children.spliterator(), false).map(r
+                        -> buildEnumMapEntry(enumKeys, r, valueClass, isSingleValue)
+                )
+                        .filter(o -> o != null)
+                        .forEach(e -> out.put(e.getKey(), e.getValue()));
+                return out;
+            }
+        }
+        return null;
+    }
+
+    private Map.Entry buildSimpleMapEntry(Class<?> valueClass, Resource r) {
+        if (valueClass.equals(Resource.class)) {
+            return new AbstractMap.SimpleEntry<>(r.getName(), r);
+        } else {
+            Object adapted = r.adaptTo(valueClass);
+            if (adapted == null) {
+                return null;
+            } else {
+                return new AbstractMap.SimpleEntry<>(r.getName(), adapted);
+            }
+        }
+    }
+
+    private Map.Entry buildEnumMapEntry(Map<String, Object> enumKeys, Resource r, Class<?> valueClass, Boolean isSingleValue) {
+        Object key = enumKeys.get(r.getName());
+        if (valueClass.equals(Resource.class)) {
+            return new AbstractMap.SimpleEntry<>(key, r);
+        } else if (isSingleValue) {
+            Object adapted = r.adaptTo(valueClass);
+            if (adapted == null) {
+                return null;
+            } else {
+                return new AbstractMap.SimpleEntry<>(key, adapted);
+            }
+        } else {
+            List values = StreamSupport.stream(r.getChildren().spliterator(), false)
+                    .map(c -> c.adaptTo(valueClass))
+                    .collect(Collectors.toList());
+            return new AbstractMap.SimpleEntry<>(key, values);
+        }
+    }
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/JcrPersist.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/JcrPersist.java
new file mode 100755
index 0000000..2858694
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/JcrPersist.java
@@ -0,0 +1,20 @@
+package org.apache.sling.models.persist;
+
+import javax.jcr.RepositoryException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ResourceResolver;
+
+/**
+ * Definition of JCR Persist service
+ */
+public interface JcrPersist {
+
+    void persist(Object object, ResourceResolver resourceResolver) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException;
+
+    void persist(Object object, ResourceResolver resourceResolver, boolean deepPersist) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException;
+
+    void persist(String nodePath, Object object, ResourceResolver resourceResolver) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException;
+
+    void persist(String nodePath, Object object, ResourceResolver resourceResolver, boolean deepPersist) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException;
+    
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/ChildType.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/ChildType.java
new file mode 100755
index 0000000..da454d2
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/ChildType.java
@@ -0,0 +1,31 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+package org.apache.sling.models.persist.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
+public @interface ChildType {
+    String value() default "";
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/DirectDescendants.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/DirectDescendants.java
new file mode 100755
index 0000000..498f50f
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/DirectDescendants.java
@@ -0,0 +1,38 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+
+package org.apache.sling.models.persist.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation to indicate that collection elements
+ * are direct descendants of this node and do not have an extra
+ * wrapper child node around.
+ * 
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.METHOD })
+public @interface DirectDescendants {
+
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/Ignore.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/Ignore.java
new file mode 100755
index 0000000..e998dc1
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/Ignore.java
@@ -0,0 +1,37 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+
+package org.apache.sling.models.persist.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation to signify that the attribute be excluded
+ * from all serialization/deserialization workflows.
+ * 
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.METHOD })
+public @interface Ignore {
+
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/ResourceType.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/ResourceType.java
new file mode 100755
index 0000000..69b19b9
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/annotations/ResourceType.java
@@ -0,0 +1,31 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+package org.apache.sling.models.persist.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
+public @interface ResourceType {
+    String value() default "";
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/JcrPersistImpl.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/JcrPersistImpl.java
new file mode 100755
index 0000000..bddd912
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/JcrPersistImpl.java
@@ -0,0 +1,52 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+package org.apache.sling.models.persist.impl;
+
+import javax.jcr.RepositoryException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.models.persist.JcrPersist;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Main class that does the magic of writing POJO directly using transparent persistence.
+ */
+@Component(service = JcrPersist.class)
+public class JcrPersistImpl implements JcrPersist {
+    @Override
+    public void persist(Object object, ResourceResolver resourceResolver) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        JcrWriter.persist(object, resourceResolver, true);
+    }
+    
+    @Override
+    public void persist(Object object, ResourceResolver resourceResolver, boolean deepPersist) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        JcrWriter.persist(object, resourceResolver, deepPersist);
+    }
+    
+    @Override
+    public void persist(String nodePath, Object object, ResourceResolver resourceResolver) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        JcrWriter.persist(nodePath, object, resourceResolver, true);
+    }
+
+    @Override
+    public void persist(String nodePath, Object object, ResourceResolver resourceResolver, boolean deepPersist) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        JcrWriter.persist(nodePath, object, resourceResolver, deepPersist);
+    }
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/JcrWriter.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/JcrWriter.java
new file mode 100755
index 0000000..340e9fb
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/JcrWriter.java
@@ -0,0 +1,280 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+package org.apache.sling.models.persist.impl;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.stream.StreamSupport;
+import javax.jcr.RepositoryException;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.models.annotations.Path;
+import org.apache.sling.models.persist.annotations.DirectDescendants;
+import org.apache.sling.models.persist.impl.util.AssertUtils;
+import org.apache.sling.models.persist.impl.util.ReflectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.sling.models.persist.impl.util.ReflectionUtils.getAnnotatedValue;
+
+/**
+ * Code to persist a given object instance to a JCR node.
+ *
+ */
+public class JcrWriter {
+
+    private JcrWriter() {
+        // Utility class, cannot be instantiated
+    }
+
+    /**
+     * My private logger
+     */
+    private static final Logger LOGGER = LoggerFactory.getLogger(JcrWriter.class);
+
+    static final Map<String, Object> RESOURCE_TYPE_NT_UNSTRUCTURED = new HashMap<>();
+
+    static final String JCR_PRIMARYTYPE = "jcr:primaryType";
+    static final String NT_UNSTRUCTURED = "nt:unstructured";
+    static final String JCR_CONTENT = "jcr:content";
+
+    static {
+        RESOURCE_TYPE_NT_UNSTRUCTURED.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED);
+    }
+
+    public static void persist(final Object instance, ResourceResolver resourceResolver, boolean deepPersist) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        String path = getJcrPath(instance);
+        persist(path, instance, resourceResolver, deepPersist);
+    }
+
+    public static void persist(final String nodePath, final Object instance, ResourceResolver resourceResolver, boolean deepPersist) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        if (nodePath == null || nodePath.trim().isEmpty()) {
+            throw new IllegalArgumentException("Node path cannot be null/empty");
+        }
+
+        if (instance == null) {
+            throw new IllegalArgumentException("Object to save cannot be null");
+        }
+
+        if (resourceResolver == null) {
+            throw new IllegalArgumentException("ResourceResolver cannot be null");
+        }
+
+        Resource resource;
+        final ResourceTypeKey resourceType = ResourceTypeKey.fromObject(instance);
+//        System.out.println(String.format("Creating node at: %s of type: %s", nodePath, resourceType.primaryType));
+
+        // let's create the resource first
+        LOGGER.debug("Creating node at: {} of type: {}", nodePath, resourceType.primaryType);
+        resource = ResourceUtil.getOrCreateResource(resourceResolver, nodePath, resourceType.primaryType, NT_UNSTRUCTURED, true);
+        if (AssertUtils.isNotEmpty(resourceType.childType)) {
+            LOGGER.debug("Needs a child node, creating node at: {} of type: {}", nodePath, resourceType.childType);
+            resource = ResourceUtil.getOrCreateResource(resourceResolver, nodePath + "/" + JCR_CONTENT, resourceType.childType, NT_UNSTRUCTURED, true);
+        }
+
+        if (ReflectionUtils.isArrayOrCollection(instance)) {
+            persistComplexValue(instance, true, nodePath, "dunno", resource);
+        } else {
+            // find properties to be saved
+            List<Field> fields = ReflectionUtils.getAllFields(instance.getClass());
+            if (fields == null || fields.isEmpty()) {
+                // TODO: remove all properties
+            } else {
+                Resource r = resource;
+                fields.stream()
+                        .filter(ReflectionUtils::isNotTransient)
+                        .filter(ReflectionUtils::isSupportedType)
+                        .filter(f -> ReflectionUtils.hasNoTransientGetter(f.getName(), instance.getClass()))
+                        .forEach(field -> persistField(r, instance, field, deepPersist));
+            }
+        }
+
+        // save back
+        resourceResolver.commit();
+    }
+
+    private static void persistField(Resource resource, Object instance, Field field, boolean deepPersist) {
+        try {
+            // read the existing resource map
+            ModifiableValueMap values = resource.adaptTo(ModifiableValueMap.class);
+            String nodePath = resource.getPath();
+
+            // find the type of field
+            final Class<?> fieldType = field.getType();
+            final String fieldName = ReflectionUtils.getFieldName(field);
+
+            // set accessible
+            field.setAccessible(true);
+
+            // handle the value as primitive first
+            if (ReflectionUtils.isPrimitiveFieldType(fieldType)) {
+                Object value = field.get(instance);
+
+                // remove the attribute that is null, or remove in case it changes type
+                values.remove(fieldName);
+                if (value != null) {
+                    values.put(fieldName, value);
+                }
+            } else if (deepPersist) {
+                boolean directDescendents = field.getAnnotation(DirectDescendants.class) != null;
+                persistComplexValue(field.get(instance), directDescendents, nodePath, fieldName, resource);
+            }
+        } catch (IllegalAccessException | RepositoryException | PersistenceException ex) {
+            LOGGER.error("Error when persisting content to " + resource.getPath(), ex);
+        }
+    }
+
+    private static void persistComplexValue(Object obj, Boolean implicitCollection, String nodePath, final String fieldName, Resource resource) throws RepositoryException, IllegalAccessException, IllegalArgumentException, PersistenceException {
+        if (obj == null) {
+            return;
+        }
+        if (Collection.class.isAssignableFrom(obj.getClass())) {
+            Collection collection = (Collection) obj;
+            if (!collection.isEmpty()) {
+                String childrenRoot = buildChildrenRoot(nodePath, fieldName, resource.getResourceResolver(), implicitCollection);
+                persistCollection(childrenRoot, collection, resource.getResourceResolver());
+            }
+        } else if (Map.class.isAssignableFrom(obj.getClass())) {
+            Map map = (Map) obj;
+            if (!map.isEmpty()) {
+                String childrenRoot = buildChildrenRoot(nodePath, fieldName, resource.getResourceResolver(), implicitCollection);
+                persistMap(childrenRoot, map, resource.getResourceResolver());
+            }
+        } else {
+            // this is a single compound object
+            // create a child node and persist all its values
+            persist(nodePath + "/" + fieldName, obj, resource.getResourceResolver(), true);
+        }
+    }
+
+    private static void persistCollection(final String collectionRoot, final Collection collection, ResourceResolver resourceResolver) throws PersistenceException, RepositoryException, IllegalArgumentException, IllegalAccessException {
+        // now for each child in the collection - create a new node
+        Set<String> childNodes = new HashSet<>();
+        if (collection != null) {
+            for (Object childObject : collection) {
+                String childName = null;
+                String childPath = getJcrPath(childObject);
+
+                if (childPath != null) {
+                    childName = extractNodeNameFromPath(childPath);
+                } else {
+                    childName = UUID.randomUUID().toString();
+                }
+
+                childPath = collectionRoot + "/" + childName;
+                childNodes.add(childPath);
+                persist(childPath, childObject, resourceResolver, true);
+            }
+        }
+        deleteOrphanNodes(resourceResolver, collectionRoot, childNodes);
+    }
+
+    private static <K, V> void persistMap(final String collectionRoot, final Map<K,V> collection, ResourceResolver resourceResolver) throws PersistenceException, RepositoryException, IllegalArgumentException, IllegalAccessException {
+        // now for each child in the collection - create a new node
+        Set<String> childNodes = new HashSet<>();
+        if (collection != null) {
+            for (Map.Entry<K,V> childObject : collection.entrySet()) {
+                String childName = String.valueOf(childObject.getKey());
+                String childPath = collectionRoot + "/" + childName;
+                childNodes.add(childPath);
+                persist(childPath, childObject.getValue(), resourceResolver, true);
+            }
+        }
+        deleteOrphanNodes(resourceResolver, collectionRoot, childNodes);
+    }
+
+    private static String buildChildrenRoot(String nodePath, String fieldName, ResourceResolver rr, boolean implicitCollection) throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        if (implicitCollection) {
+            return nodePath;
+        } else {
+            // create a child collection of required type
+            // and persist all instances
+
+            // create the first child node first
+            persist(nodePath + "/" + fieldName, new Object(), rr, false);
+
+            // update collection root
+            return nodePath + "/" + fieldName;
+        }
+    }
+
+    private static void deleteOrphanNodes(ResourceResolver resourceResolver, final String collectionRoot, Set<String> childNodes) {
+        // Delete any children that are not in the list
+        Resource collectionParent = resourceResolver.getResource(collectionRoot);
+        if (collectionParent != null) {
+            StreamSupport
+                    .stream(resourceResolver.getChildren(collectionParent).spliterator(), false)
+                    .filter(r -> !childNodes.contains(r.getPath()))
+                    .forEach(r -> {
+                        try {
+                            resourceResolver.delete(r);
+                        } catch (PersistenceException ex) {
+                            LOGGER.error("Unable to remove stale resource at " + r.getPath(), ex);
+                        }
+                    });
+        }
+    }
+
+    private static String extractNodeNameFromPath(String pathValue) throws IllegalArgumentException, IllegalAccessException {
+        int lastIndex = pathValue.lastIndexOf('/');
+        if (lastIndex >= 0) {
+            pathValue = pathValue.substring(lastIndex + 1);
+        }
+
+        return pathValue;
+    }
+
+    private static String getJcrPath(Object obj) {
+        String path = (String) getAnnotatedValue(obj, Path.class);
+        if (path != null) {
+            return path;
+        }
+
+        try {
+            Method pathGetter = MethodUtils.getMatchingMethod(obj.getClass(), "getPath");
+            if (pathGetter != null) {
+                return (String) MethodUtils.invokeMethod(obj, "getPath");
+            }
+
+            Field pathField = FieldUtils.getDeclaredField(obj.getClass(), "path");
+            if (pathField != null) {
+                return (String) FieldUtils.readField(pathField, obj, true);
+            }
+        } catch (IllegalArgumentException | NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
+            java.util.logging.Logger.getLogger(JcrWriter.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        LOGGER.warn("Object of type {} does NOT contain a Path attribute or a path property - multiple instances may conflict", obj.getClass());
+        return null;
+    }
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/ResourceTypeKey.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/ResourceTypeKey.java
new file mode 100755
index 0000000..a1f08fa
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/ResourceTypeKey.java
@@ -0,0 +1,59 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.persist.annotations.ChildType;
+import org.apache.sling.models.persist.annotations.ResourceType;
+
+import static org.apache.sling.models.persist.impl.util.ReflectionUtils.getAnnotatedValue;
+
+/**
+ * Represents primary/child pair type
+ */
+public class ResourceTypeKey {
+
+    static final Map<Class<?>, ResourceTypeKey> NODE_TYPE_FOR_CLASS = new HashMap<>();
+    static public final ResourceTypeKey NT_UNSTRUCTURED = new ResourceTypeKey("nt:unstructured", null);
+
+    final public String primaryType;
+    final public String childType;
+
+    public ResourceTypeKey(String primaryType, String childType) {
+        this.primaryType = primaryType;
+        this.childType = childType;
+    }
+
+    public static ResourceTypeKey fromObject(Object obj) {
+        if (obj == null) {
+            return ResourceTypeKey.NT_UNSTRUCTURED;
+        }
+
+        if (NODE_TYPE_FOR_CLASS.containsKey(obj.getClass())) {
+            return NODE_TYPE_FOR_CLASS.get(obj.getClass());
+        }
+
+        Model modelAnnotation = obj.getClass().getAnnotation(Model.class);
+        String primaryType = (String) getAnnotatedValue(obj, ResourceType.class);
+        // Use the model annotation for resource type as needed
+        if (primaryType == null
+                && modelAnnotation != null
+                && modelAnnotation.resourceType() != null
+                && modelAnnotation.resourceType().length == 1) {
+            primaryType = modelAnnotation.resourceType()[0];
+        }
+        String childType = (String) getAnnotatedValue(obj, ChildType.class);
+        if (primaryType != null) {
+            ResourceTypeKey key = new ResourceTypeKey(primaryType, childType);
+            NODE_TYPE_FOR_CLASS.put(obj.getClass(), key);
+            return key;
+        } else {
+            return ResourceTypeKey.NT_UNSTRUCTURED;
+        }
+    }
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/util/AssertUtils.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/util/AssertUtils.java
new file mode 100755
index 0000000..f994b23
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/util/AssertUtils.java
@@ -0,0 +1,50 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+package org.apache.sling.models.persist.impl.util;
+
+/**
+ * Simple assertion functions.
+ *
+ * Part of the code of this class has been borrowed from the open-source project
+ * <code>jerry-core</code> from https://github.com/sangupta/jerry-core.
+ *
+ */
+public class AssertUtils {
+    private AssertUtils() {
+        // Utility class cannot be instantiated
+    }
+
+    public static boolean isEmpty(String str) {
+        return str == null || str.isEmpty();
+    }
+
+    public static boolean isEmpty(Object[] array) {
+        return array == null || array.length == 0;
+    }
+
+    public static boolean isNotEmpty(String str) {
+        return !isEmpty(str);
+    }
+    
+    public static boolean isNotEmpty(Object[] array) {
+        return !isEmpty(array);
+    }
+
+}
diff --git a/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/util/ReflectionUtils.java b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/util/ReflectionUtils.java
new file mode 100755
index 0000000..4bb46cc
--- /dev/null
+++ b/SlingJCRPersist/src/main/java/org/apache/sling/models/persist/impl/util/ReflectionUtils.java
@@ -0,0 +1,303 @@
+/*
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2016 Adobe
+ * %%
+ * 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.
+ * #L%
+ */
+package org.apache.sling.models.persist.impl.util;
+
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.beans.Transient;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.inject.Named;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Via;
+import org.apache.sling.models.persist.JcrPersist;
+import org.apache.sling.models.persist.annotations.Ignore;
+
+/**
+ * Utility methods around object reflection.
+ *
+ */
+public class ReflectionUtils {
+
+    private ReflectionUtils() {
+        // Utility class cannot be instantiated
+    }
+
+    /**
+     * Classes that correspond to basic parameter types, which are handled
+     * directly in code
+     */
+    static final Set<Class<?>> BASIC_PARAMS = new HashSet<>();
+
+    static final Set<Class<?>> UNSUPPORTED_CLASSES = new HashSet<>();
+    static final Set<String> UNSUPPORTED_PACKAGES = new HashSet<>();
+
+    /**
+     * Add default values
+     */
+    static {
+        // primitives
+        BASIC_PARAMS.add(byte.class);
+        BASIC_PARAMS.add(char.class);
+        BASIC_PARAMS.add(short.class);
+        BASIC_PARAMS.add(int.class);
+        BASIC_PARAMS.add(long.class);
+        BASIC_PARAMS.add(float.class);
+        BASIC_PARAMS.add(double.class);
+        BASIC_PARAMS.add(boolean.class);
+
+        // primitive boxed
+        BASIC_PARAMS.add(Byte.class);
+        BASIC_PARAMS.add(Character.class);
+        BASIC_PARAMS.add(Short.class);
+        BASIC_PARAMS.add(Integer.class);
+        BASIC_PARAMS.add(Long.class);
+        BASIC_PARAMS.add(Float.class);
+        BASIC_PARAMS.add(Double.class);
+        BASIC_PARAMS.add(Boolean.class);
+
+        // other basic types
+        BASIC_PARAMS.add(String.class);
+        BASIC_PARAMS.add(Calendar.class);
+        BASIC_PARAMS.add(Date.class);
+        BASIC_PARAMS.add(URI.class);
+        BASIC_PARAMS.add(BigDecimal.class);
+
+        // basic type arrays
+        BASIC_PARAMS.add(String[].class);
+        BASIC_PARAMS.add(Calendar[].class);
+        BASIC_PARAMS.add(Date[].class);
+        BASIC_PARAMS.add(URI[].class);
+        BASIC_PARAMS.add(BigDecimal[].class);
+
+        // primitives array
+        BASIC_PARAMS.add(byte[].class);
+        BASIC_PARAMS.add(char[].class);
+        BASIC_PARAMS.add(short[].class);
+        BASIC_PARAMS.add(int[].class);
+        BASIC_PARAMS.add(long[].class);
+        BASIC_PARAMS.add(float[].class);
+        BASIC_PARAMS.add(double[].class);
+        BASIC_PARAMS.add(boolean[].class);
+
+        // primitive boxed arrays
+        BASIC_PARAMS.add(Byte[].class);
+        BASIC_PARAMS.add(Character[].class);
+        BASIC_PARAMS.add(Short[].class);
+        BASIC_PARAMS.add(Integer[].class);
+        BASIC_PARAMS.add(Long[].class);
+        BASIC_PARAMS.add(Float[].class);
+        BASIC_PARAMS.add(Double[].class);
+        BASIC_PARAMS.add(Boolean[].class);
+
+        // Any field with this type will be ignored
+        UNSUPPORTED_CLASSES.add(Resource.class);
+        UNSUPPORTED_CLASSES.add(JcrPersist.class);
+        UNSUPPORTED_PACKAGES.add("javax.jcr");
+        UNSUPPORTED_PACKAGES.add("com.day.cq");
+        UNSUPPORTED_PACKAGES.add("org.apache.sling.api");
+        UNSUPPORTED_PACKAGES.add("com.adobe.acs.commons.mcp");
+    }
+
+    public static Collection<Class<?>> getSupportedPropertyTypes() {
+        return BASIC_PARAMS;
+    }
+
+    public static boolean isArrayOrCollection(Object instance) {
+        return instance.getClass().isArray() || instance instanceof Collection;
+    }
+
+    /**
+     * Check if a given field is transient. A field is considered transient if
+     * and only if the field is marked with `transient` keyword and no
+     * annotation of type {@link Named} exists over the field; or if the field
+     * is marked with {@link Ignore} annotation.
+     *
+     * @param field the non-<code>null</code> field to check.
+     *
+     * @return <code>false</code> if field is to be considered transient,
+     * <code>true</code> otherwise
+     */
+    public static boolean isNotTransient(Field field) {
+        if (field != null && Modifier.isTransient(field.getModifiers())) {
+            // if property is covered using @Named annotation it shall not be excluded
+            Named aemProperty = field.getAnnotation(Named.class);
+            return aemProperty != null;
+        } else {
+            // is the property annotated with @Ignore?
+            return field == null || field.getAnnotation(Ignore.class) == null;
+        }
+    }
+
+    public static boolean hasNoTransientGetter(String fieldName, Class clazz) {
+        PropertyDescriptor desc;
+        try {
+            desc = new PropertyDescriptor(fieldName, clazz);
+            if (desc.getReadMethod() != null && desc.getReadMethod().getAnnotation(Transient.class) != null) {
+                return false;
+            }
+        } catch (IntrospectionException ex) {
+            // Do nothing
+        }
+        return true;
+    }
+
+    public static boolean isSupportedType(Field field) {
+        Class clazz = field.getType();
+        if (Map.class.isAssignableFrom(clazz)) {
+            ParameterizedType p = (ParameterizedType) field.getGenericType();
+            Type paramType = p.getActualTypeArguments()[1];
+            try {
+                // In case the value type is a collection of something, check to be safe
+                if (!Class.class.isAssignableFrom(paramType.getClass())) {
+                    paramType = ((ParameterizedType) paramType).getActualTypeArguments()[0];
+                }
+                // Assume for now that we've narrowed down to the final object type to confirm
+                clazz = (Class) paramType;
+            } catch (ClassCastException ex) {
+                return false;
+            }
+        }
+        if (UNSUPPORTED_CLASSES.contains(clazz)) {
+            return false;
+        }
+        Package pkg = clazz.isArray() ? clazz.getComponentType().getPackage() : clazz.getPackage();
+        if (pkg == null) {
+            return true;
+        } else {
+            String packageName = pkg.getName();
+            return UNSUPPORTED_PACKAGES
+                    .stream()
+                    .noneMatch(packageName::startsWith);
+        }
+    }
+
+    /**
+     * Return all fields including all-private and all-inherited fields for the
+     * given class.
+     *
+     * @param clazz the class for which fields are needed
+     *
+     * @return the {@link List} of {@link Field} objects in no certain order
+     *
+     * @throws IllegalArgumentException if given class is <code>null</code>
+     */
+    public static List<Field> getAllFields(Class<?> clazz) {
+        List<Field> fields = new ArrayList<>();
+        populateAllFields(clazz, fields);
+        return fields;
+    }
+
+    public static void populateAllFields(Class<?> clazz, List<Field> fields) {
+        if (clazz == null) {
+            return;
+        }
+
+        Field[] array = clazz.getDeclaredFields();
+        if (AssertUtils.isNotEmpty(array)) {
+            fields.addAll(Arrays.asList(array));
+        }
+
+        if (clazz.getSuperclass() == null) {
+            return;
+        }
+
+        populateAllFields(clazz.getSuperclass(), fields);
+    }
+
+    // Utility function common to reading/writing start here
+    /**
+     * Returns the name of the field to look for in JCR.
+     *
+     * @param field
+     * @return
+     */
+    public static String getFieldName(Field field) {
+        Named namedAnnotation = field.getAnnotation(Named.class);
+        Via viaAnnotation = field.getAnnotation(Via.class);
+        if (namedAnnotation != null) {
+            return namedAnnotation.value();
+        } else if (viaAnnotation != null && viaAnnotation.value() != null) {
+            return viaAnnotation.value();
+        } else {
+            return field.getName();
+        }
+    }
+
+    public static boolean isPrimitiveFieldType(Class<?> fieldType) {
+        return getSupportedPropertyTypes().contains(fieldType);
+    }
+
+    /**
+     * Get the value defined on an annotation, if it is a class annotation, or a
+     * method or field-level member which has that annotation
+     *
+     * @param obj Object which has the given annotation as a class, method, or
+     * field annotation
+     * @param annotatedType desired annotation type class
+     * @return Value if found otherwise null
+     */
+    public static Object getAnnotatedValue(Object obj, Class annotatedType) {
+        if (obj == null) {
+            return null;
+        }
+        Annotation a = obj.getClass().getAnnotation(annotatedType);
+        try {
+            if (a != null) {
+                String value = (String) MethodUtils.invokeMethod(a, "value");
+                if (value != null && !value.isEmpty()) {
+                    return value;
+                }
+            }
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
+            // Do nothing, it didn't have a value defined on that annotation
+        }
+        List<Field> fields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), annotatedType);
+        try {
+            if (fields == null || fields.isEmpty()) {
+                List<Method> methods = MethodUtils.getMethodsListWithAnnotation(obj.getClass(), annotatedType);
+                return CollectionUtils.isNotEmpty(methods) ? methods.get(0).invoke(obj) : null;
+            } else {
+                return fields.get(0).get(obj);
+            }
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+            return null;
+        }
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/BeanWithDirectMappedChildren.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/BeanWithDirectMappedChildren.java
new file mode 100755
index 0000000..b6272b4
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/BeanWithDirectMappedChildren.java
@@ -0,0 +1,48 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.injectors;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.DefaultInjectionStrategy;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.persist.annotations.DirectDescendants;
+
+/**
+ * Expresses a sling model which has child nodes as a map
+ */
+@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+public class BeanWithDirectMappedChildren {
+
+    public transient String path;
+
+    @DirectDescendants
+    @Inject
+    public Map<String, Person> people = new HashMap<>();
+
+    public void addPerson(String firstName, String lastName) {
+        Person p = new Person();
+        String name = lastName + '-' + firstName;
+        p.firstName = firstName;
+        p.lastName = lastName;
+        p.path = "./" + name;
+        people.put(name, p);
+    }
+
+    @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+    public static class Person {
+
+        transient String path;
+
+        @Inject
+        String firstName;
+
+        @Inject
+        String lastName;
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/BeanWithMappedChildren.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/BeanWithMappedChildren.java
new file mode 100755
index 0000000..96370fb
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/BeanWithMappedChildren.java
@@ -0,0 +1,46 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.injectors;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.DefaultInjectionStrategy;
+import org.apache.sling.models.annotations.Model;
+
+/**
+ * Expresses a sling model which has child nodes as a map
+ */
+@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+public class BeanWithMappedChildren {
+
+    public transient String path;
+
+    @Inject
+    public Map<String, Person> people = new HashMap<>();
+
+    public void addPerson(String firstName, String lastName) {
+        Person p = new Person();
+        String name = lastName + '-' + firstName;
+        p.firstName = firstName;
+        p.lastName = lastName;
+        p.path = "./" + name;
+        people.put(name, p);
+    }
+
+    @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+    public static class Person {
+
+        transient String path;
+
+        @Inject
+        String firstName;
+
+        @Inject
+        String lastName;
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/MapOfChildResourcesInjectorTest.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/MapOfChildResourcesInjectorTest.java
new file mode 100755
index 0000000..ccf7e39
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/injectors/MapOfChildResourcesInjectorTest.java
@@ -0,0 +1,96 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.injectors;
+
+import javax.jcr.RepositoryException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.models.persist.JcrPersist;
+import org.apache.sling.models.persist.impl.JcrPersistImpl;
+import org.apache.sling.models.spi.Injector;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import static org.junit.Assert.*;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ *
+ * @author Brendan Robert
+ */
+public class MapOfChildResourcesInjectorTest {
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    ResourceResolver rr;
+    JcrPersist jcrPersist;
+
+    @Before
+    public void setUp() {
+        rr = context.resourceResolver();
+        context.addModelsForPackage(this.getClass().getPackage().getName());
+        context.registerService(Injector.class, new MapOfChildResourcesInjector());
+        jcrPersist = new JcrPersistImpl();
+    }
+
+    @Test
+    public void roundtripTest() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        BeanWithMappedChildren source = new BeanWithMappedChildren();
+        source.addPerson("joe", "schmoe");
+        source.addPerson("john", "doe");
+        source.addPerson("bob", "smith");
+        jcrPersist.persist("/test/bean", source, rr);
+
+        Resource targetResource = rr.getResource("/test/bean");
+        assertNotNull("Bean should have been persisted");
+
+        Resource personRes = rr.getResource("/test/bean/people/smith-bob");
+        assertNotNull("Person should have persisted in repository", personRes);
+
+        BeanWithMappedChildren target = targetResource.adaptTo(BeanWithMappedChildren.class);
+        assertNotNull("Bean should deserialize", target);
+
+        assertEquals("Should have 3 children", 3, target.people.size());
+    }
+
+    @Test
+    public void roundtripEmpytTest() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        BeanWithMappedChildren source = new BeanWithMappedChildren();
+        jcrPersist.persist("/test/empty-bean", source, rr);
+
+        Resource targetResource = rr.getResource("/test/empty-bean");
+        assertNotNull("Bean should have been persisted");
+
+        Resource personRes = rr.getResource("/test/empty-bean/people");
+        assertNull("Person should not have persisted in repository", personRes);
+
+        BeanWithMappedChildren target = targetResource.adaptTo(BeanWithMappedChildren.class);
+        assertNotNull("Bean should deserialize", target);
+
+        assertEquals("Should have 0 children", 0, target.people.size());
+    }
+
+    @Test
+    public void roundtripTestDirectChildren() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        BeanWithDirectMappedChildren source = new BeanWithDirectMappedChildren();
+        source.addPerson("joe", "schmoe");
+        source.addPerson("john", "doe");
+        source.addPerson("bob", "smith");
+        jcrPersist.persist("/test/bean", source, rr);
+
+        Resource targetResource = rr.getResource("/test/bean");
+        assertNotNull("Bean should have been persisted");
+
+        Resource personRes = rr.getResource("/test/bean/smith-bob");
+        assertNotNull("Person should have persisted in repository", personRes);
+
+        BeanWithDirectMappedChildren target = targetResource.adaptTo(BeanWithDirectMappedChildren.class);
+        assertNotNull("Bean should deserialize", target);
+
+        assertEquals("Should have 3 children", 3, target.people.size());
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/JcrWriterTest.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/JcrWriterTest.java
new file mode 100755
index 0000000..3215c44
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/JcrWriterTest.java
@@ -0,0 +1,327 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.stream.StreamSupport;
+import javax.jcr.RepositoryException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.models.persist.bean.BeanWithAnnotatedPathField;
+import org.apache.sling.models.persist.bean.BeanWithAnnotatedPathGetter;
+import org.apache.sling.models.persist.bean.BeanWithMappedNames;
+import org.apache.sling.models.persist.bean.BeanWithPathField;
+import org.apache.sling.models.persist.bean.BeanWithPathGetter;
+import org.apache.sling.models.persist.bean.ComplexBean;
+import org.apache.sling.models.persist.bean.MappedChildren;
+import org.apache.sling.models.persist.impl.JcrPersistImpl;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.*;
+
+/**
+ * Test basic JCR persistence behaviors
+ */
+public class JcrWriterTest {
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    ResourceResolver rr;
+    JcrPersist jcrPersist = new JcrPersistImpl();
+
+    @Before
+    public void setUp() {
+        rr = context.resourceResolver();
+        context.addModelsForClasses(
+                BeanWithAnnotatedPathField.class,
+                BeanWithAnnotatedPathGetter.class,
+                BeanWithPathField.class,
+                BeanWithPathGetter.class,
+                BeanWithMappedNames.class,
+                BeanWithMappedNames.ChildBean.class,
+                MappedChildren.class,
+                MappedChildren.Child.class,
+                ComplexBean.class,
+                ComplexBean.Level2Bean.class,
+                ComplexBean.Level3Bean.class);
+    }
+
+    /**
+     * Confirm that content is written to correct path indicated by either path
+     * field, path getter, or path annotation.Also asserts that path annotation
+     * takes precedence over any field or getter method.
+     *
+     * @throws javax.jcr.RepositoryException
+     * @throws org.apache.sling.api.resource.PersistenceException
+     * @throws java.lang.IllegalAccessException
+     */
+    @Test
+    public void testPersistBeanPath() throws RepositoryException, PersistenceException, IllegalAccessException {
+        BeanWithPathGetter bean1 = new BeanWithPathGetter();
+        jcrPersist.persist(bean1, rr, false);
+        Resource res = rr.getResource(bean1.getPath());
+        assertNotNull("Resource not created at expected path", res);
+        assertEquals("Expected property not found", bean1.prop1, res.getValueMap().get("prop1", "missing"));
+
+        BeanWithPathField bean2 = new BeanWithPathField();
+        jcrPersist.persist(bean2, rr, false);
+        res = rr.getResource(bean2.path + "/jcr:content");
+        assertNotNull("Resource not created at expected path", res);
+        assertEquals("Expected property not found", bean2.prop1, res.getValueMap().get("prop1", "missing"));
+
+        BeanWithAnnotatedPathField bean3 = new BeanWithAnnotatedPathField();
+        jcrPersist.persist(bean3, rr);
+        res = rr.getResource(bean3.correctPath);
+        assertNotNull("Resource not created at expected path", res);
+        assertEquals("Expected property not found", bean3.prop1, res.getValueMap().get("prop1", "missing"));
+
+        BeanWithAnnotatedPathGetter bean4 = new BeanWithAnnotatedPathGetter();
+        jcrPersist.persist(bean4, rr, true);
+        res = rr.getResource(bean4.getCorrectPath());
+        assertNotNull("Resource not created at expected path", res);
+        assertEquals("Expected property not found", bean4.prop1, res.getValueMap().get("prop1", "missing"));
+    }
+
+    /**
+     * Confirm that content is persisted at provided path even if it has a path
+     * annotation or path getter, etc.
+     *
+     * @throws javax.jcr.RepositoryException
+     * @throws org.apache.sling.api.resource.PersistenceException
+     * @throws java.lang.IllegalAccessException
+     */
+    @Test
+    public void testPersistProvidedPath() throws RepositoryException, PersistenceException, IllegalAccessException {
+        String testPath = "/manual/path";
+        BeanWithAnnotatedPathField bean = new BeanWithAnnotatedPathField();
+        jcrPersist.persist(testPath, bean, rr, false);
+        Resource res = rr.getResource(bean.correctPath);
+        assertNull("Should not have stored content here", res);
+        res = rr.getResource(testPath);
+        assertNotNull("Resource not created at expected path", res);
+        assertEquals("Expected property not found", bean.prop1, res.getValueMap().get("prop1", "missing"));
+    }
+
+    @Test
+    public void testComplexObjectGraph() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        // First create a bean with a complex structure and various object types buried in it
+        ComplexBean sourceBean = new ComplexBean();
+        sourceBean.name = "Complex-bean-test";
+        sourceBean.arrayOfStrings = new String[]{"Value 1", "Value 2", "Value 3"};
+        sourceBean.level2.name = "Complex-bean-level2";
+        ComplexBean.Level3Bean l31 = new ComplexBean.Level3Bean();
+        l31.value1 = "L3-1";
+        l31.value2 = 123;
+        l31.valueList = new String[]{"L31a", "L31b", "L31c", "L31d"};
+        ComplexBean.Level3Bean l32 = new ComplexBean.Level3Bean();
+        l32.value1 = "L3-2";
+        l32.value2 = 456;
+        l32.valueList = new String[]{"L32a", "L32b", "L32c", "L32d"};
+        l32.path = "/test/complex-beans/Complex-bean-test/level2/level3/child-2";
+        sourceBean.level2.level3.add(l31);
+        sourceBean.level2.level3.add(l32);
+
+        // Persist the bean
+        jcrPersist.persist(sourceBean.getPath(), sourceBean, rr);
+
+        // Now retrieve that object from the repository
+        rr.refresh();
+        Resource createdResource = rr.getResource(sourceBean.getPath());
+        ComplexBean targetBean = createdResource.adaptTo(ComplexBean.class);
+
+        assertNotNull(targetBean);
+        assertNotEquals(sourceBean, targetBean);
+        assertTrue("Expecing children of object to have been deserialized", targetBean.level2.level3 != null && targetBean.level2.level3.size() > 0);
+        targetBean.level2.level3.get(0).path = l31.path;
+        assertThat(targetBean).isEqualToComparingFieldByFieldRecursively(sourceBean);
+    }
+
+    @Test
+    public void testChildObjectRemoval() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        // First create a bean with a complex structure and various object types buried in it
+        ComplexBean sourceBean = new ComplexBean();
+        sourceBean.name = "Complex-bean-test";
+        sourceBean.arrayOfStrings = new String[]{"Value 1", "Value 2", "Value 3"};
+        sourceBean.level2.name = "Complex-bean-level2";
+        ComplexBean.Level3Bean l31 = new ComplexBean.Level3Bean();
+        l31.value1 = "L3-1";
+        l31.value2 = 123;
+        l31.valueList = new String[]{"L31a", "L31b", "L31c", "L31d"};
+        ComplexBean.Level3Bean l32 = new ComplexBean.Level3Bean();
+        l32.value1 = "L3-2";
+        l32.value2 = 456;
+        l32.valueList = new String[]{"L32a", "L32b", "L32c", "L32d"};
+        l32.path = "/test/complex-beans/Complex-bean-test/level2/level3/child-2";
+        sourceBean.level2.level3.add(l31);
+        sourceBean.level2.level3.add(l32);
+
+        // Persist the bean
+        jcrPersist.persist(sourceBean, rr);
+
+        // Child record should exist
+        Resource existingResource = rr.getResource(l32.path);
+        assertNotNull(existingResource);
+
+        sourceBean.level2.level3.remove(l32);
+        jcrPersist.persist(sourceBean, rr);
+
+        // Child record should no longer exist
+        Resource deletedResource = rr.getResource(l32.path);
+        assertNull(deletedResource);
+    }
+
+    @Test
+    public void testMappedNames() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException, JsonProcessingException {
+        // Create test beans
+        BeanWithMappedNames.ChildBean child1 = new BeanWithMappedNames.ChildBean();
+        BeanWithMappedNames.ChildBean child2 = new BeanWithMappedNames.ChildBean();
+        BeanWithMappedNames.ChildBean child3 = new BeanWithMappedNames.ChildBean();
+        child1.setName("child-1");
+        child2.setName("child-2");
+        child3.setName("child-3");
+
+        BeanWithMappedNames bean = new BeanWithMappedNames();
+        bean.setWrong1("Name");
+        bean.setWrong2(new String[]{"foo", "bar", "baz"});
+        bean.setWrong3(child1);
+        bean.setWrong4(Arrays.asList(child1, child2, child3));
+        bean.setWrong5(new HashMap<String, BeanWithMappedNames.ChildBean>() {
+            {
+                put("child1", child1);
+                put("child2", child2);
+                put("child3", child3);
+            }
+        });
+        bean.setWrong6(Boolean.TRUE);
+
+        // Persist values
+        jcrPersist.persist("/test/mapped", bean, rr);
+
+        // Check that everything stored correctly
+        Resource res = rr.getResource("/test/mapped");
+        ValueMap properties = res.getValueMap();
+        // Part 1: Simple property
+        assertEquals("Name", properties.get("prop-1", String.class));
+        assertNull(properties.get("wrong1"));
+        // Part 2: Array property
+        String[] prop2 = properties.get("prop-2", String[].class);
+        assertArrayEquals(prop2, new String[]{"foo", "bar", "baz"});
+        assertNull(properties.get("wrong2"));
+        // Part 3: Object property
+        assertNull(rr.getResource("/test/mapped/wrong3"));
+        Resource childRes1 = rr.getResource("/test/mapped/child-1");
+        assertNotNull(childRes1);
+        assertEquals("child-1", childRes1.getValueMap().get("name"));
+        // Part 4: Object list property
+        assertNull(rr.getResource("/test/mapped/wrong4"));
+        Resource childRes2 = rr.getResource("/test/mapped/child-2");
+        assertNotNull(childRes2);
+        assertEquals(StreamSupport
+                .stream(childRes2.getChildren().spliterator(), false)
+                .count(), 3L);
+        // Part 5: Map-of-objects property
+        assertNull(rr.getResource("/test/mapped/wrong5"));
+        Resource childRes3 = rr.getResource("/test/mapped/child-3");
+        assertNotNull(childRes3);
+        assertEquals(StreamSupport
+                .stream(childRes3.getChildren().spliterator(), false)
+                .count(), 3L);
+        // Part 6: Boolean property
+        assertNull(properties.get("wrong6"));
+        assertNull(properties.get("isWrong6"));
+        assertTrue(properties.get("prop-3", Boolean.class));
+
+        // Now confirm Jackson respects its mappings too
+        ObjectMapper mapper = new ObjectMapper();
+        String json = mapper.writeValueAsString(bean);
+        assertFalse("Should not have wrong property names: " + json, json.contains("wrong"));
+        assertTrue("Should have prop-1" + json, json.contains("prop-1"));
+        assertTrue("Should have prop-2" + json, json.contains("prop-2"));
+        assertTrue("Should have prop-3" + json, json.contains("prop-3"));
+        assertTrue("Should have child-1" + json, json.contains("child-1"));
+        assertTrue("Should have child-2" + json, json.contains("child-2"));
+        assertTrue("Should have child-3" + json, json.contains("child-3"));
+    }
+
+    @Test
+    /**
+     * Test named map children with map<String, Object>
+     *
+     */
+    public void testMapChildrenWithStringKeys() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        // Create some values in the Map<String, Object> data structure
+        MappedChildren bean = new MappedChildren();
+        MappedChildren.Child child1 = new MappedChildren.Child();
+        bean.stringKeys.put("one", child1);
+        MappedChildren.Child child2 = new MappedChildren.Child();
+        bean.stringKeys.put("two", child2);
+        MappedChildren.Child child3 = new MappedChildren.Child();
+        bean.stringKeys.put("three", child3);
+        child1.name = "one";
+        child1.testValue = "Test Value 1";
+        child2.name = "two";
+        child2.testValue = "Test Value 2";
+        child3.name = "three";
+        child3.testValue = "Test Value 3";
+
+        // Attempt to save the data structure
+        jcrPersist.persist("/test/path", bean, rr);
+
+        // Confirm the children were saved in the expected places using the map key as the node name
+        Resource r1 = rr.getResource("/test/path/stringKeys/one");
+        assertNotNull(r1);
+        Resource r2 = rr.getResource("/test/path/stringKeys/two");
+        assertNotNull(r2);
+        Resource r3 = rr.getResource("/test/path/stringKeys/three");
+        assertNotNull(r3);
+    }
+
+    @Test
+    /**
+     * Test named map children with map<String, Object>
+     *
+     */
+    public void testMapChildrenWithEnumerationKeys() throws RepositoryException, PersistenceException, IllegalArgumentException, IllegalAccessException {
+        // Do same thing except using enumKeys map on the bean object
+        // e.g. --> bean.enumKeys.put(MappedChildren.KEYS.ONE, child1);
+
+        // Create some values in the Map<String, Object> data structure
+        MappedChildren bean = new MappedChildren();
+        MappedChildren.Child child1 = new MappedChildren.Child();
+        bean.enumKeys.put(MappedChildren.KEYS.ONE, child1);
+        MappedChildren.Child child2 = new MappedChildren.Child();
+        bean.enumKeys.put(MappedChildren.KEYS.TWO, child2);
+        MappedChildren.Child child3 = new MappedChildren.Child();
+        bean.enumKeys.put(MappedChildren.KEYS.THREE, child3);
+        child1.name = "one";
+        child1.testValue = "Test Value 1";
+        child2.name = "two";
+        child2.testValue = "Test Value 2";
+        child3.name = "three";
+        child3.testValue = "Test Value 3";
+
+        // Attempt to save the data structure
+        jcrPersist.persist("/test/path", bean, rr);
+
+        // Confirm the children were saved in the expected places using the map key as the node name
+        Resource r1 = rr.getResource("/test/path/enumKeys/ONE");
+        assertNotNull(r1);
+        Resource r2 = rr.getResource("/test/path/enumKeys/TWO");
+        assertNotNull(r2);
+        Resource r3 = rr.getResource("/test/path/enumKeys/THREE");
+        assertNotNull(r3);
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithAnnotatedPathField.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithAnnotatedPathField.java
new file mode 100755
index 0000000..1958f84
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithAnnotatedPathField.java
@@ -0,0 +1,26 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.bean;
+
+import javax.inject.Inject;
+import org.apache.sling.models.annotations.Path;
+
+/**
+ * Example of bean with getPath method that stores the path in a field using an annotation marker
+ */
+public class BeanWithAnnotatedPathField {
+    @Inject
+    public String prop1 = "testValue";
+    
+    public String path = "/test/WRONG-path";
+
+    @Path
+    public String correctPath = "/test/annotated-field-path";
+    
+    public String getPath() {
+        return path;
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithAnnotatedPathGetter.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithAnnotatedPathGetter.java
new file mode 100755
index 0000000..5010077
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithAnnotatedPathGetter.java
@@ -0,0 +1,30 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.bean;
+
+import javax.inject.Inject;
+import org.apache.sling.models.annotations.Path;
+
+/**
+ * Example of bean with getPath method that stores the path in a field using an annotation marker
+ */
+public class BeanWithAnnotatedPathGetter {
+    @Inject
+    public String prop1 = "testValue";
+    
+    public String path = "/test/WRONG-path";
+
+    public String correctPath = "/test/annotated-getter-path";
+    
+    public String getPath() {
+        return path;
+    }
+
+    @Path
+    public String getCorrectPath() {
+        return path;
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithMappedNames.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithMappedNames.java
new file mode 100755
index 0000000..38db353
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithMappedNames.java
@@ -0,0 +1,148 @@
+package org.apache.sling.models.persist.bean;
+
+//import com.adobe.cq.export.json.ExporterConstants;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * every property has a different mapped name, designed to test the @Named annotation is respected.
+ * fields are all named "wrong" because if we see that in the stored JCR values then the persist logic was wrong.
+ */
+//@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
+public class BeanWithMappedNames {
+    @Inject
+    @Named("prop-1")
+    @JsonProperty("prop-1")
+    private String wrong1;
+
+    @Inject
+    @Named("prop-2")
+    @JsonProperty("prop-2")
+    private String[] wrong2;
+
+    @Inject
+    @Named("child-1")
+    @JsonProperty("child-1")
+    private ChildBean wrong3;
+
+    @Inject
+    @Named("child-2")
+    @JsonProperty("child-2")
+    private List<ChildBean> wrong4;
+
+    @Inject
+    @Named("child-3")
+    @JsonProperty("child-3")
+    private Map<String,ChildBean> wrong5;
+
+    @Inject
+    @Named("prop-3")
+    @JsonProperty("prop-3")
+    private Boolean wrong6;
+
+    public static class ChildBean {
+        @Inject
+        private String name;
+
+        /**
+         * @return the name
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * @param name the name to set
+         */
+        public void setName(String name) {
+            this.name = name;
+        }
+    }
+
+    /**
+     * @return the wrong1
+     */
+    public String getWrong1() {
+        return wrong1;
+    }
+
+    /**
+     * @param wrong1 the wrong1 to set
+     */
+    public void setWrong1(String wrong1) {
+        this.wrong1 = wrong1;
+    }
+
+    /**
+     * @return the wrong2
+     */
+    public String[] getWrong2() {
+        return wrong2;
+    }
+
+    /**
+     * @param wrong2 the wrong2 to set
+     */
+    public void setWrong2(String[] wrong2) {
+        this.wrong2 = wrong2;
+    }
+
+    /**
+     * @return the wrong3
+     */
+    public ChildBean getWrong3() {
+        return wrong3;
+    }
+
+    /**
+     * @param wrong3 the wrong3 to set
+     */
+    public void setWrong3(ChildBean wrong3) {
+        this.wrong3 = wrong3;
+    }
+
+    /**
+     * @return the wrong4
+     */
+    public List<ChildBean> getWrong4() {
+        return wrong4;
+    }
+
+    /**
+     * @param wrong4 the wrong4 to set
+     */
+    public void setWrong4(List<ChildBean> wrong4) {
+        this.wrong4 = wrong4;
+    }
+
+    /**
+     * @return the wrong5
+     */
+    public Map<String,ChildBean> getWrong5() {
+        return wrong5;
+    }
+
+    /**
+     * @param wrong5 the wrong5 to set
+     */
+    public void setWrong5(Map<String,ChildBean> wrong5) {
+        this.wrong5 = wrong5;
+    }
+
+    /**
+     * @return the wrong6
+     */
+    public Boolean isWrong6() {
+        return wrong6;
+    }
+
+    /**
+     * @param wrong6 the wrong6 to set
+     */
+    public void setWrong6(Boolean wrong6) {
+        this.wrong6 = wrong6;
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithPathField.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithPathField.java
new file mode 100755
index 0000000..0b5e853
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithPathField.java
@@ -0,0 +1,23 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.bean;
+
+import javax.inject.Inject;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.persist.annotations.ChildType;
+
+/**
+ * Example of bean with getPath method that stores the path in a field
+ */
+@Model(adaptables = Resource.class, resourceType = "test/testBean")
+@ChildType("test/testBean/field-path")
+public class BeanWithPathField {
+    @Inject
+    public String prop1 = "testValue";
+    
+    public String path = "/test/field-path";
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithPathGetter.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithPathGetter.java
new file mode 100755
index 0000000..ee3274f
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/BeanWithPathGetter.java
@@ -0,0 +1,21 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.bean;
+
+import javax.inject.Inject;
+
+/**
+ * Example of bean with getPath method that renders path of the bean [possible] dynamically.
+ */
+public class BeanWithPathGetter {
+    @Inject
+    public String prop1 = "testValue";
+    
+    // This provides the path of the bean, which could also be some kind of dynamic business logic.
+    public String getPath() {
+        return "/test/dynamic-path";
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/ComplexBean.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/ComplexBean.java
new file mode 100755
index 0000000..74d299f
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/ComplexBean.java
@@ -0,0 +1,84 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.bean;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Named;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.DefaultInjectionStrategy;
+import org.apache.sling.models.annotations.Model;
+
+/**
+ * Example of a model bean with an object graph of depth 4
+ */
+@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+public class ComplexBean {
+    public ComplexBean() {
+        
+    }
+
+    public ComplexBean(Resource resource) {
+        if (resource != null) {
+            name = resource.getName();
+        }        
+    }
+    
+    public String name = "change-me";
+
+    public String getPath() {
+        return "/test/complex-beans/" + name;
+    }
+    
+    // --- Serializable properties
+    @Inject
+    @Named("array-of-strings")
+    public String[] arrayOfStrings = {"one", "two", "three", "four"};
+
+    @Inject
+    public Date now = new Date();
+
+    @Inject
+    public long nowLong = now.getTime();
+
+    @Inject
+    public Level2Bean level2 = new Level2Bean();
+
+    @Model(adaptables = Resource.class)
+    public static class Level2Bean {
+        @Inject
+        public String name = "level2";
+
+        @Inject
+        public List<Level3Bean> level3 = new ArrayList<>();
+    }
+
+    @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+    public static class Level3Bean {
+    public Level3Bean() {
+        
+    }
+
+    public Level3Bean(Resource resource) {
+        if (resource != null) {
+            path = resource.getPath();
+        }        
+    }
+
+    public String path;
+
+        @Inject
+        public String value1 = "val1";
+
+        @Inject
+        public int value2 = -1;
+
+        @Inject
+        public String[] valueList = {};
+    }
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/MappedChildren.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/MappedChildren.java
new file mode 100755
index 0000000..d93180e
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/bean/MappedChildren.java
@@ -0,0 +1,44 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.bean;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.DefaultInjectionStrategy;
+import org.apache.sling.models.annotations.Model;
+
+/**
+ * Bean with children arranged in maps (enumeration map and also string keys)
+ */
+@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+public class MappedChildren {
+    public static enum KEYS{ONE,TWO,THREE};
+
+    public String name;
+
+    public Map<String, Child> stringKeys = new HashMap<>();
+
+    public EnumMap<KEYS, Child> enumKeys = new EnumMap<>(KEYS.class);
+
+    @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+    public static class Child {
+        public String name;
+        public String testValue;
+    }
+
+    public MappedChildren() {
+    }
+
+    public MappedChildren(Resource resource) {
+        if (resource != null) {
+            name = resource.getName();
+        }
+    }
+
+
+}
diff --git a/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/impl/ResourceTypeKeyTest.java b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/impl/ResourceTypeKeyTest.java
new file mode 100755
index 0000000..b25713e
--- /dev/null
+++ b/SlingJCRPersist/src/test/java/org/apache/sling/models/persist/impl/ResourceTypeKeyTest.java
@@ -0,0 +1,52 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.apache.sling.models.persist.impl;
+
+import org.apache.sling.models.persist.bean.BeanWithPathField;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test various behaviors of ResourceTypeKey class
+ */
+public class ResourceTypeKeyTest {
+
+    public ResourceTypeKeyTest() {
+    }
+
+    @Before
+    public void setUp() {
+    }
+
+    /**
+     * Test of fromObject method, of class ResourceTypeKey relying on class annotations
+     */
+    @Test
+    public void testFromObject() {
+        BeanWithPathField bean = new BeanWithPathField();
+        ResourceTypeKey result = ResourceTypeKey.fromObject(bean);
+        assertEquals("test/testBean", result.primaryType);
+        assertEquals("test/testBean/field-path", result.childType);
+    }
+
+    @Test
+    public void testFromObjectCache() {
+        BeanWithPathField bean1 = new BeanWithPathField();
+        BeanWithPathField bean2 = new BeanWithPathField();
+        ResourceTypeKey result1 = ResourceTypeKey.fromObject(bean1);
+        ResourceTypeKey result2 = ResourceTypeKey.fromObject(bean2);
+        assertEquals("Should use the same object value", result1, result2);
+    }
+
+    @Test
+    public void testNullObject() {
+        ResourceTypeKey result = ResourceTypeKey.fromObject(null);
+        assertNotNull("Should not return null", result);
+        assertEquals("Should be NT UNSTRUCTURED", "nt:unstructured", result.primaryType);
+    }
+}