/** * This file is part of the JCROM project. * Copyright (C) 2008-2014 - All rights reserved. * Authors: Olafur Gauti Gudmundsson, Nicolas Dos Santos * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jcrom; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.RepositoryException; import org.jcrom.annotations.JcrChildNode; import org.jcrom.annotations.JcrProperty; import org.jcrom.util.NodeFilter; import org.jcrom.util.ReflectionUtils; /** * This class handles mappings of type @JcrChildNode * * @author Olafur Gauti Gudmundsson * @author Nicolas Dos Santos */ class ChildNodeMapper { private static final String POLICY_NODE_NAME = "rep:policy"; private final Mapper mapper; public ChildNodeMapper(Mapper mapper) { this.mapper = mapper; } private String getNodeName(Field field) { JcrChildNode jcrChildNode = mapper.getJcrom().getAnnotationReader().getAnnotation(field, JcrChildNode.class); String name = field.getName(); if (!jcrChildNode.name().equals(JcrProperty.DEFAULT_FIELDNAME)) { name = jcrChildNode.name(); } return name; } private void removeChildren(Node containerNode) throws RepositoryException { NodeIterator nodeIterator = containerNode.getNodes(); while (nodeIterator.hasNext()) { Node currentNode = nodeIterator.nextNode(); // ignore the policy node if (!currentNode.getName().equals(POLICY_NODE_NAME)) { currentNode.remove(); } } } private Node createChildNodeContainer(Node node, String containerName, JcrChildNode jcrChildNode, Mapper mapper) throws RepositoryException { if (!node.hasNode(mapper.getCleanName(containerName))) { Node containerNode = node.addNode(mapper.getCleanName(containerName), jcrChildNode.containerNodeType()); // add annotated mixin types if (jcrChildNode != null && jcrChildNode.containerMixinTypes() != null) { for (String mixinType : jcrChildNode.containerMixinTypes()) { if (containerNode.canAddMixin(mixinType)) { containerNode.addMixin(mixinType); } } } return containerNode; } else { return node.getNode(mapper.getCleanName(containerName)); } } private void addSingleChildToNode(Field field, JcrChildNode jcrChildNode, Object obj, String nodeName, Node node, Mapper mapper, int depth, NodeFilter nodeFilter) throws IllegalAccessException, RepositoryException, IOException { if (jcrChildNode.createContainerNode()) { Node childContainer = createChildNodeContainer(node, nodeName, jcrChildNode, mapper); if (!childContainer.hasNodes()) { if (field.get(obj) != null) { // add the node if it does not exist mapper.addNode(childContainer, field.get(obj), null, null); } } else { if (field.get(obj) != null) { Object childObj = field.get(obj); mapper.updateNode(childContainer.getNodes().nextNode(), childObj, childObj.getClass(), nodeFilter, depth + 1, null); } else { // field is now null, so we remove the child node removeChildren(childContainer); } } } else { // don't create a container node for this child, // use the field name for the node instead if (!node.hasNode(nodeName)) { if (field.get(obj) != null) { Object childObj = field.get(obj); mapper.setNodeName(childObj, nodeName); mapper.addNode(node, childObj, null, null); } } else { if (field.get(obj) != null) { Object childObj = field.get(obj); mapper.setNodeName(childObj, nodeName); mapper.updateNode(node.getNode(nodeName), childObj, childObj.getClass(), nodeFilter, depth + 1, null); } else { NodeIterator nodeIterator = node.getNodes(nodeName); while (nodeIterator.hasNext()) { nodeIterator.nextNode().remove(); } } } } } private void addMultipleChildrenToNode(Field field, JcrChildNode jcrChildNode, Object obj, String nodeName, Node node, Mapper mapper, int depth, NodeFilter nodeFilter) throws IllegalAccessException, RepositoryException, IOException { Node childContainer = createChildNodeContainer(node, nodeName, jcrChildNode, mapper); List<?> children = (List<?>) field.get(obj); if (children != null && !children.isEmpty()) { if (childContainer.hasNodes()) { // children exist, we must update NodeIterator childNodes = childContainer.getNodes(); while (childNodes.hasNext()) { Node child = childNodes.nextNode(); Object childEntity = mapper.findEntityByPath(children, child.getPath()); if (childEntity == null) { // this child was not found, so we remove it child.remove(); } else { mapper.updateNode(child, childEntity, childEntity.getClass(), nodeFilter, depth + 1, null); } } // we must add new children, if any for (int i = 0; i < children.size(); i++) { Object child = children.get(i); String childPath = mapper.getNodePath(child); if (childPath == null || childPath.equals("") || !childContainer.hasNode(mapper.getCleanName(mapper.getNodeName(child)))) { mapper.addNode(childContainer, child, null, null); } } } else { // no children exist, we add for (int i = 0; i < children.size(); i++) { mapper.addNode(childContainer, children.get(i), null, null); } } } else { // field list is now null (or empty), so we remove the child nodes removeChildren(childContainer); } } /** * Maps a Map<String,Object> or Map<String,List<Object>> to a JCR Node. */ private void addMapOfChildrenToNode(Field field, JcrChildNode jcrChildNode, Object obj, String nodeName, Node node, Mapper mapper, int depth, NodeFilter nodeFilter) throws IllegalAccessException, RepositoryException, IOException { Node childContainer = createChildNodeContainer(node, nodeName, jcrChildNode, mapper); Map<?, ?> childMap = (Map<?, ?>) field.get(obj); if (childMap != null && !childMap.isEmpty()) { Class<?> paramClass = ReflectionUtils.getParameterizedClass(field, 1); if (childContainer.hasNodes()) { // nodes already exist, we need to update Map<String, String> mapWithCleanKeys = new HashMap<String, String>(); for (Map.Entry<?, ?> entry : childMap.entrySet()) { String key = (String) entry.getKey(); String cleanKey = mapper.getCleanName(key); if (childContainer.hasNode(cleanKey)) { if (ReflectionUtils.implementsInterface(paramClass, List.class)) { // lists are hard to update, so we just recreate it childContainer.getNode(cleanKey).remove(); Node listContainer = childContainer.addNode(cleanKey); List<?> childList = (List<?>) entry.getValue(); for (int i = 0; i < childList.size(); i++) { mapper.addNode(listContainer, childList.get(i), null, null); } } else { // update the child mapper.updateNode(childContainer.getNode(cleanKey), entry.getValue(), paramClass, nodeFilter, depth + 1, null); } } else { // this child does not exist, so we add it addMapChild(paramClass, childContainer, childMap, key, cleanKey, mapper); } mapWithCleanKeys.put(cleanKey, "1"); } // remove nodes that no longer exist NodeIterator childNodes = childContainer.getNodes(); while (childNodes.hasNext()) { Node child = childNodes.nextNode(); if (!mapWithCleanKeys.containsKey(child.getName())) { child.remove(); } } } else { // no children exist, we simply add all for (Object k : childMap.keySet()) { String key = (String) k; String cleanKey = mapper.getCleanName(key); addMapChild(paramClass, childContainer, childMap, key, cleanKey, mapper); } } } else { // map is now null (or empty), so we remove the child nodes removeChildren(childContainer); } } private void addMapChild(Class<?> paramClass, Node childContainer, Map<?, ?> childMap, String key, String cleanKey, Mapper mapper) throws IllegalAccessException, RepositoryException, IOException { if (ReflectionUtils.implementsInterface(paramClass, List.class)) { List<?> childList = (List<?>) childMap.get(key); // create a container for the List Node listContainer = childContainer.addNode(cleanKey); for (int i = 0; i < childList.size(); i++) { mapper.addNode(listContainer, childList.get(i), null, null); } } else { mapper.setNodeName(childMap.get(key), cleanKey); mapper.addNode(childContainer, childMap.get(key), null, null); } } private void setChildren(Field field, Object obj, Node node, int depth, NodeFilter nodeFilter, Mapper mapper) throws IllegalAccessException, RepositoryException, IOException { JcrChildNode jcrChildNode = mapper.getJcrom().getAnnotationReader().getAnnotation(field, JcrChildNode.class); if (!jcrChildNode.readOnly()) { String nodeName = getNodeName(field); // make sure that this child is supposed to be updated if (nodeFilter == null || nodeFilter.isIncluded(field.getName(), depth)) { if (ReflectionUtils.implementsInterface(field.getType(), List.class)) { // multiple children in a List addMultipleChildrenToNode(field, jcrChildNode, obj, nodeName, node, mapper, depth, nodeFilter); } else if (ReflectionUtils.implementsInterface(field.getType(), Map.class)) { // multiple children in a Map addMapOfChildrenToNode(field, jcrChildNode, obj, nodeName, node, mapper, depth, nodeFilter); } else { // single child addSingleChildToNode(field, jcrChildNode, obj, nodeName, node, mapper, depth, nodeFilter); } } } } void addChildren(Field field, Object entity, Node node, Mapper mapper) throws IllegalAccessException, RepositoryException, IOException { setChildren(field, entity, node, NodeFilter.DEPTH_INFINITE, null, mapper); } void updateChildren(Field field, Object obj, Node node, int depth, NodeFilter nodeFilter, Mapper mapper) throws IllegalAccessException, RepositoryException, IOException { setChildren(field, obj, node, depth, nodeFilter, mapper); } @SuppressWarnings("unchecked") List<?> getChildrenList(Class<?> childObjClass, Node childrenContainer, Object parentObj, Mapper mapper, int depth, NodeFilter nodeFilter, JcrChildNode jcrChildNode) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { List<Object> children = jcrChildNode.listContainerClass().newInstance(); NodeIterator iterator = childrenContainer.getNodes(); while (iterator.hasNext()) { Node childNode = iterator.nextNode(); // ignore the policy node when loading child nodes if (!childNode.getName().equals(POLICY_NODE_NAME)) { children.add(getSingleChild(childObjClass, childNode, parentObj, mapper, depth, nodeFilter)); } } return children; } @SuppressWarnings("unchecked") private Map<?, ?> getChildrenMap(Class<?> mapParamClass, Node childrenContainer, Object parentObj, Mapper mapper, int depth, NodeFilter nodeFilter, JcrChildNode jcrChildNode) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { Map<Object, Object> children = jcrChildNode.mapContainerClass().newInstance(); NodeIterator iterator = childrenContainer.getNodes(); while (iterator.hasNext()) { Node childNode = iterator.nextNode(); if (ReflectionUtils.implementsInterface(mapParamClass, List.class)) { // each value in the map is a list of child nodes if (jcrChildNode.lazy()) { // lazy loading children.put(childNode.getName(), ProxyFactory.createChildNodeListProxy(Object.class, parentObj, childNode.getPath(), childNode.getSession(), mapper, depth, nodeFilter, jcrChildNode)); } else { // eager loading children.put(childNode.getName(), getChildrenList(Object.class, childNode, parentObj, mapper, depth, nodeFilter, jcrChildNode)); } } else { // each value in the map is a child node if (jcrChildNode.lazy()) { // lazy loading children.put( childNode.getName(), ProxyFactory.createChildNodeProxy(mapper.findClassFromNode(Object.class, childNode), parentObj, childNode.getPath(), childNode.getSession(), mapper, depth, nodeFilter, false)); } else { // eager loading children.put(childNode.getName(), getSingleChild(Object.class, childNode, parentObj, mapper, depth, nodeFilter)); } } } return children; } Object getSingleChild(Class<?> childObjClass, Node childNode, Object obj, Mapper mapper, int depth, NodeFilter nodeFilter) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { childNode = mapper.checkIfVersionedChild(childNode); Object childObj = mapper.createInstanceForNode(childObjClass, childNode); childObj = mapper.mapNodeToClass(childObj, childNode, nodeFilter, depth + 1); return childObj; } void getChildrenFromNode(Field field, Node node, Object obj, int depth, NodeFilter nodeFilter, Mapper mapper) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { String nodeName = getNodeName(field); JcrChildNode jcrChildNode = mapper.getJcrom().getAnnotationReader().getAnnotation(field, JcrChildNode.class); boolean childHasNodes = node.hasNode(nodeName) && (node.getNode(nodeName).hasNodes() || node.getNode(nodeName).hasProperty(Property.JCR_CHILD_VERSION_HISTORY)); if (node.hasNode(nodeName) && (childHasNodes || (!jcrChildNode.createContainerNode() && !ReflectionUtils.implementsInterface(field.getType(), List.class) && !ReflectionUtils .implementsInterface(field.getType(), Map.class))) && nodeFilter.isIncluded(nodeName, depth)) { // child nodes are almost always stored inside a container node Node childrenContainer = node.getNode(nodeName); childrenContainer = mapper.checkIfVersionedChild(childrenContainer); if (ReflectionUtils.implementsInterface(field.getType(), List.class)) { // we can expect more than one child object here Class<?> childObjClass = ReflectionUtils.getParameterizedClass(field); List<?> children; if (jcrChildNode.lazy()) { // lazy loading children = ProxyFactory.createChildNodeListProxy(childObjClass, obj, childrenContainer.getPath(), node.getSession(), mapper, depth, nodeFilter, jcrChildNode); } else { // eager loading children = getChildrenList(childObjClass, childrenContainer, obj, mapper, depth, nodeFilter, jcrChildNode); } field.set(obj, children); } else if (ReflectionUtils.implementsInterface(field.getType(), Map.class)) { // dynamic map of child nodes // lazy loading is applied to each value in the Map Class<?> mapParamClass = ReflectionUtils.getParameterizedClass(field, 1); field.set(obj, getChildrenMap(mapParamClass, childrenContainer, obj, mapper, depth, nodeFilter, jcrChildNode)); } else { // instantiate the field class Class<?> childObjClass = field.getType(); if (childrenContainer.hasNodes() || !jcrChildNode.createContainerNode()) { if (jcrChildNode.lazy()) { // lazy loading field.set(obj, ProxyFactory.createChildNodeProxy(childObjClass, obj, childrenContainer.getPath(), node.getSession(), mapper, depth, nodeFilter, jcrChildNode.createContainerNode())); } else { // eager loading if (jcrChildNode.createContainerNode()) { field.set(obj, getSingleChild(childObjClass, childrenContainer.getNodes().nextNode(), obj, mapper, depth, nodeFilter)); } else { field.set(obj, getSingleChild(childObjClass, childrenContainer, obj, mapper, depth, nodeFilter)); } } } else { // Issue 87: Child nodes not set to empty list/null Object fieldValue = field.get(obj); if (fieldValue != null) { if (fieldValue instanceof Collection<?>) { ((Collection<?>) fieldValue).clear(); } else if (fieldValue instanceof Map<?, ?>) { ((Map<?, ?>) fieldValue).clear(); } else { field.set(obj, null); } } } } } } String getChildContainerNodePath(Object childObject, Object parentObject, Node parentNode) throws IllegalAccessException, RepositoryException { List<String> childContainerPaths = getChildContainerNodePaths(childObject, parentObject, parentNode); if (childContainerPaths.size() > 1) { throw new IllegalAccessException("Multiple child objects found with type '" + childObject.getClass() + "' from parent object '" + parentObject.getClass() + "'"); } return childContainerPaths.isEmpty() ? null : childContainerPaths.get(0); } List<String> getChildContainerNodePaths(Object childObject, Object parentObject, Node parentNode) throws IllegalAccessException, RepositoryException { Class<?> type = childObject.getClass(); String childNodeName = mapper.getNodeName(childObject); List<String> containers = new ArrayList<String>(); List<String> children = new ArrayList<String>(); for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(parentObject.getClass(), false)) { field.setAccessible(true); if (mapper.getJcrom().getAnnotationReader().isAnnotationPresent(field, JcrChildNode.class)) { Class<?> childObjClass; if (ReflectionUtils.implementsInterface(field.getType(), List.class)) { childObjClass = ReflectionUtils.getParameterizedClass(field); } else if (ReflectionUtils.implementsInterface(field.getType(), Map.class)) { childObjClass = ReflectionUtils.getParameterizedClass(field, 1); } else { childObjClass = field.getType(); } if (childObjClass.isAssignableFrom(type)) { JcrChildNode jcrChildNode = mapper.getJcrom().getAnnotationReader().getAnnotation(field, JcrChildNode.class); String nodeName = getNodeName(field); if (jcrChildNode.createContainerNode()) { if (!parentNode.hasNode(nodeName)) { throw new IllegalAccessException("The child container node not found with name '" + nodeName + "' from parent node '" + parentNode.getPath() + "'"); } Node n = parentNode.getNode(nodeName); containers.add(n.getPath()); } else { children.add(nodeName); } } } } // Check if there is child objects or the childNodeName is equal to one of the object names in the objects which is not creating a container node if (containers.isEmpty() && !children.contains(childNodeName)) { throw new IllegalAccessException("Child object not found with name '" + childNodeName + "' and type '" + type + "' from parent object '" + parentObject.getClass() + "'"); } return containers; } }