/** * 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.annotation.Annotation; import java.lang.reflect.Field; import java.sql.Timestamp; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.nodetype.NodeType; import javax.jcr.version.Version; import javax.jcr.version.VersionManager; import net.sf.cglib.proxy.LazyLoader; import org.jcrom.annotations.JcrBaseVersionCreated; import org.jcrom.annotations.JcrBaseVersionName; import org.jcrom.annotations.JcrCheckedout; import org.jcrom.annotations.JcrChildNode; import org.jcrom.annotations.JcrCreated; import org.jcrom.annotations.JcrFileNode; import org.jcrom.annotations.JcrIdentifier; import org.jcrom.annotations.JcrName; import org.jcrom.annotations.JcrNode; import org.jcrom.annotations.JcrPageContentNode; import org.jcrom.annotations.JcrPageNode; import org.jcrom.annotations.JcrParentNode; import org.jcrom.annotations.JcrPath; import org.jcrom.annotations.JcrProperty; import org.jcrom.annotations.JcrPropertyCheckSuccessful; import org.jcrom.annotations.JcrPropertyMap; import org.jcrom.annotations.JcrProtectedProperty; import org.jcrom.annotations.JcrReference; import org.jcrom.annotations.JcrSerializedProperty; import org.jcrom.annotations.JcrUUID; import org.jcrom.annotations.JcrVersionCreated; import org.jcrom.annotations.JcrVersionName; import org.jcrom.callback.DefaultJcromCallback; import org.jcrom.callback.JcromCallback; import org.jcrom.util.JcrUtils; import org.jcrom.util.NodeFilter; import org.jcrom.util.PathUtils; import org.jcrom.util.ReflectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class handles the heavy lifting of mapping a JCR node to a JCR entity object, and vice versa. * * @author Olafur Gauti Gudmundsson * @author Nicolas Dos Santos */ class Mapper { private static final Logger LOG = LoggerFactory.getLogger(Mapper.class); /** Set of classes that have been validated for mapping by this mapper */ private final CopyOnWriteArraySet<Class<?>> mappedClasses = new CopyOnWriteArraySet<Class<?>>(); /** Specifies whether to clean up the node names */ private final boolean cleanNames; /** Specifies whether to retrieve mapped class name from node property */ private final boolean dynamicInstantiation; private final PropertyMapper propertyMapper; private final ReferenceMapper referenceMapper; private final FileNodeMapper fileNodeMapper; private final ChildNodeMapper childNodeMapper; private final Jcrom jcrom; private final ThreadLocal<Map<HistoryKey, Object>> history = new ThreadLocal<Map<HistoryKey, Object>>(); /** * Create a Mapper for a specific class. * * @param entityClass * the class that we will me mapping to/from */ Mapper(boolean cleanNames, boolean dynamicInstantiation, Jcrom jcrom) { this.cleanNames = cleanNames; this.dynamicInstantiation = dynamicInstantiation; this.jcrom = jcrom; this.propertyMapper = new PropertyMapper(this); this.referenceMapper = new ReferenceMapper(this); this.fileNodeMapper = new FileNodeMapper(this); this.childNodeMapper = new ChildNodeMapper(this); } void clearHistory() { history.remove(); } boolean isMapped(Class<?> c) { return mappedClasses.contains(c); } void addMappedClass(Class<?> c) { mappedClasses.add(c); } CopyOnWriteArraySet<Class<?>> getMappedClasses() { return mappedClasses; } boolean isCleanNames() { return cleanNames; } boolean isDynamicInstantiation() { return dynamicInstantiation; } Class<?> getClassForName(String className) { return getClassForName(className, null); } Class<?> getClassForName(String className, Class<?> defaultClass) { for (Class<?> c : mappedClasses) { if (className.equals(c.getCanonicalName())) { return c; } } try { return Class.forName(className, true, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException ex) { return defaultClass; } } String getCleanName(String name) { if (name == null) { throw new JcrMappingException("Node name is null"); } if (cleanNames) { return PathUtils.createValidName(name); } else { return name; } } Object findEntityByPath(List<?> entities, String path) throws IllegalAccessException { for (Object entity : entities) { if (path.equals(getNodePath(entity))) { return entity; } } return null; } private Field findAnnotatedField(Object obj, Class<? extends Annotation> annotationClass) { for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(obj.getClass(), false)) { if (jcrom.getAnnotationReader().isAnnotationPresent(field, annotationClass)) { field.setAccessible(true); return field; } } return null; } Field findPathField(Object obj) { return findAnnotatedField(obj, JcrPath.class); } Field findParentField(Object obj) { return findAnnotatedField(obj, JcrParentNode.class); } Field findNameField(Object obj) { return findAnnotatedField(obj, JcrName.class); } /** * @deprecated This method is now deprecated because {@link JcrUUID} annotation is deprecated.<br/> * {@link #findIdField(Object)} with {@link JcrIdentifier} annotation should be used instead. */ @Deprecated Field findUUIDField(Object obj) { return findAnnotatedField(obj, JcrUUID.class); } Field findIdField(Object obj) { return findAnnotatedField(obj, JcrIdentifier.class); } String getNodeName(Object object) throws IllegalAccessException { return (String) findNameField(object).get(object); } String getNodePath(Object object) throws IllegalAccessException { return (String) findPathField(object).get(object); } Object getParentObject(Object childObject) throws IllegalAccessException { Field parentField = findParentField(childObject); return parentField != null ? parentField.get(childObject) : null; } String getChildContainerNodePath(Object childObject, Object parentObject, Node parentNode) throws IllegalAccessException, RepositoryException { return childNodeMapper.getChildContainerNodePath(childObject, parentObject, parentNode); } /** * @deprecated This method is now deprecated because {@link #findUUIDField(Object)} annotation is deprecated.<br/> * {@link #getNodeId(Object)} should be used instead. */ @Deprecated String getNodeUUID(Object object) throws IllegalAccessException { return (String) findUUIDField(object).get(object); } String getNodeId(Object object) throws IllegalAccessException { Field idField = findIdField(object); return idField != null ? (String) idField.get(object) : getNodeUUID(object); } static boolean hasMixinType(Node node, String mixinType) throws RepositoryException { for (NodeType nodeType : node.getMixinNodeTypes()) { if (nodeType.getName().equals(mixinType)) { return true; } } return false; } void setBaseVersionInfo(Object object, String name, Calendar created) throws IllegalAccessException { Field baseName = findAnnotatedField(object, JcrBaseVersionName.class); if (baseName != null) { baseName.set(object, name); } Field baseCreated = findAnnotatedField(object, JcrBaseVersionCreated.class); if (baseCreated != null) { if (baseCreated.getType() == Date.class) { baseCreated.set(object, created.getTime()); } else if (baseCreated.getType() == Timestamp.class) { baseCreated.set(object, new Timestamp(created.getTimeInMillis())); } else if (baseCreated.getType() == Calendar.class) { baseCreated.set(object, created); } } } void setNodeName(Object object, String name) throws IllegalAccessException { findNameField(object).set(object, name); } void setNodePath(Object object, String path) throws IllegalAccessException { findPathField(object).set(object, path); } /** * @deprecated This method is now deprecated because {@link #findUUIDField(Object)} annotation is deprecated.<br/> * {@link #setId(Object, String)} should be used instead. */ @Deprecated void setUUID(Object object, String uuid) throws IllegalAccessException { Field uuidField = findUUIDField(object); if (uuidField != null) { uuidField.set(object, uuid); } } void setId(Object object, String id) throws IllegalAccessException { Field idField = findIdField(object); if (idField != null) { idField.set(object, id); } } /** * Check if this node has a child version history reference. If so, then return the referenced node, else return the node supplied. * * @param node * @return * @throws javax.jcr.RepositoryException */ Node checkIfVersionedChild(Node node) throws RepositoryException { if (node.hasProperty(Property.JCR_CHILD_VERSION_HISTORY)) { // Node versionNode = // node.getSession().getNodeByUUID(node.getProperty("jcr:childVersionHistory").getString()); Node versionNode = getNodeById(node, node.getProperty(Property.JCR_CHILD_VERSION_HISTORY).getString()); NodeIterator it = versionNode.getNodes(); while (it.hasNext()) { Node n = it.nextNode(); if ((!n.getName().equals("jcr:rootVersion") && !n.getName().equals(Node.JCR_ROOT_VERSION)) && n.isNodeType(NodeType.NT_VERSION) && n.hasNode(Node.JCR_FROZEN_NODE) && node.getPath().indexOf("/" + n.getName() + "/") != -1) { return n.getNode(Node.JCR_FROZEN_NODE); } } return node; } else { return node; } } Object findParentObjectFromNode(Node node) throws RepositoryException, IllegalAccessException, ClassNotFoundException, InstantiationException, IOException { Object parentObj = null; Node parentNode = node.getParent(); while (parentNode != null) { Class<?> parentClass = findClassFromNode(Object.class, parentNode); if (parentClass != null && !parentClass.equals(Object.class)) { // Gets parent object without children parentObj = fromNode(parentClass, parentNode, new NodeFilter(NodeFilter.INCLUDE_ALL, 0)); break; } try { parentNode = parentNode.getParent(); } catch (Exception ignore) { parentNode = null; } } return parentObj; } Class<?> findClassFromNode(Class<?> defaultClass, Node node) throws RepositoryException, IllegalAccessException, ClassNotFoundException, InstantiationException { if (dynamicInstantiation) { // first we try to locate the class name from node property String classNameProperty = "className"; JcrNode jcrNode = ReflectionUtils.getJcrNodeAnnotation(defaultClass); if (jcrNode != null && !jcrNode.classNameProperty().equals("none")) { classNameProperty = jcrNode.classNameProperty(); } if (node.hasProperty(classNameProperty)) { String className = node.getProperty(classNameProperty).getString(); Class<?> c = getClassForName(className, defaultClass); if (isMapped(c)) { return c; } else { throw new JcrMappingException("Trying to instantiate unmapped class: " + c.getName()); } } else { // use default class return defaultClass; } } else { // use default class return defaultClass; } } Object createInstanceForNode(Class<?> objClass, Node node) throws RepositoryException, IllegalAccessException, ClassNotFoundException, InstantiationException { return findClassFromNode(objClass, node).newInstance(); } /** * Transforms the node supplied to an instance of the entity class that this Mapper was created for. * * @param node * the JCR node from which to create the object * @param nodeFilter * the NodeFilter to be applied * @param action * callback object that specifies the Jcr action * @return an instance of the JCR entity class, mapped from the node * @throws java.lang.Exception */ Object fromNodeWithParent(Class<?> entityClass, Node node, NodeFilter nodeFilter) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { history.set(new HashMap<HistoryKey, Object>()); Object obj = createInstanceForNode(entityClass, node); Object parentObj = findParentObjectFromNode(node); if (nodeFilter == null) { nodeFilter = new NodeFilter(NodeFilter.INCLUDE_ALL, NodeFilter.DEPTH_INFINITE); } if (ReflectionUtils.extendsClass(obj.getClass(), JcrFile.class)) { // special handling of JcrFile objects fileNodeMapper.mapSingleFile((JcrFile) obj, node, parentObj, 0, nodeFilter, this); } mapNodeToClass(obj, node, nodeFilter, 0); history.remove(); return obj; } /** * Transforms the node supplied to an instance of the entity class that this Mapper was created for. * * @param node * the JCR node from which to create the object * @param nodeFilter * the NodeFilter to be applied * @return an instance of the JCR entity class, mapped from the node * @throws java.lang.Exception */ Object fromNode(Class<?> entityClass, Node node, NodeFilter nodeFilter) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { history.set(new HashMap<HistoryKey, Object>()); Object obj = createInstanceForNode(entityClass, node); if (nodeFilter == null) { nodeFilter = new NodeFilter(NodeFilter.INCLUDE_ALL, NodeFilter.DEPTH_INFINITE); } if (ReflectionUtils.extendsClass(obj.getClass(), JcrFile.class)) { // special handling of JcrFile objects fileNodeMapper.mapSingleFile((JcrFile) obj, node, null, 0, nodeFilter, this); } mapNodeToClass(obj, node, nodeFilter, 0); history.remove(); return obj; } /** * Transforms the entity supplied to a JCR node, and adds that node as a child to the parent node supplied. * * @param parentNode * the parent node to which the entity node will be added * @param entity * the entity to be mapped to the JCR node * @param mixinTypes * an array of mixin type that will be added to the new node * @param action * callback object that specifies the Jcrom actions: * <ul> * <li> * {@link JcromCallback#doAddNode(Node, String, JcrNode, Object)} ,</li> * <li> * {@link JcromCallback#doAddMixinTypes(Node, String[], JcrNode, Object)} ,</li> * <li>{@link JcromCallback#doComplete(Object, Node)},</li> * </ul> * @return the newly created JCR node * @throws java.lang.Exception */ Node addNode(Node parentNode, Object entity, String[] mixinTypes, JcromCallback action) throws IllegalAccessException, RepositoryException, IOException { return addNode(parentNode, entity, mixinTypes, true, action); } Node addNode(Node parentNode, Object entity, String[] mixinTypes, boolean createNode, JcromCallback action) throws IllegalAccessException, RepositoryException, IOException { entity = clearCglib(entity); if (action == null) { action = new DefaultJcromCallback(jcrom); } // create the child node Node node; JcrNode jcrNode = ReflectionUtils.getJcrNodeAnnotation(entity.getClass()); if (createNode) { // add node String nodeName = getCleanName(getNodeName(entity)); node = action.doAddNode(parentNode, nodeName, jcrNode, entity); // add mixin types action.doAddMixinTypes(node, mixinTypes, jcrNode, entity); // update the object id, name and path setId(entity, node.getIdentifier()); setNodeName(entity, node.getName()); setNodePath(entity, node.getPath()); if (node.hasProperty(Property.JCR_UUID)) { // setUUID(entity, node.getUUID()); setUUID(entity, node.getIdentifier()); } } else { node = parentNode; } // add class name to property if (jcrNode != null && !jcrNode.classNameProperty().equals("none")) { action.doAddClassNameToProperty(node, jcrNode, entity); } // special handling of JcrFile objects if (ReflectionUtils.extendsClass(entity.getClass(), JcrFile.class)) { fileNodeMapper.addFileNode(node, (JcrFile) entity, this); } for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(entity.getClass(), true)) { field.setAccessible(true); if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrProperty.class)) { propertyMapper.addProperty(field, entity, node, this); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrSerializedProperty.class)) { propertyMapper.addSerializedProperty(field, entity, node); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrChildNode.class)) { childNodeMapper.addChildren(field, entity, node, this); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrReference.class)) { referenceMapper.addReferences(field, entity, node); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrFileNode.class)) { fileNodeMapper.addFiles(field, entity, node, this); } } // complete the addition of the new node action.doComplete(entity, node); return node; } /** * Update an existing JCR node with the entity supplied. * * @param node * the JCR node to be updated * @param entity * the entity that will be mapped to the existing node * @param nodeFilter * the NodeFilter to apply when updating child nodes and references * @param action * callback object that specifies the Jcrom actions * @return the updated node * @throws java.lang.Exception */ Node updateNode(Node node, Object entity, NodeFilter nodeFilter, JcromCallback action) throws RepositoryException, IllegalAccessException, IOException { return updateNode(node, entity, entity.getClass(), nodeFilter, 0, action); } Node updateNode(Node node, Object entity, Class<?> entityClass, NodeFilter nodeFilter, int depth, JcromCallback action) throws RepositoryException, IllegalAccessException, IOException { entity = clearCglib(entity); if (nodeFilter == null) { nodeFilter = new NodeFilter(NodeFilter.INCLUDE_ALL, NodeFilter.DEPTH_INFINITE); } if (action == null) { action = new DefaultJcromCallback(jcrom); } // map the class name to a property JcrNode jcrNode = ReflectionUtils.getJcrNodeAnnotation(entityClass); if (jcrNode != null && !jcrNode.classNameProperty().equals("none")) { // check if the class of the object has changed if (node.hasProperty(jcrNode.classNameProperty())) { String oldClassName = node.getProperty(jcrNode.classNameProperty()).getString(); if (!oldClassName.equals(entity.getClass().getCanonicalName())) { // different class, so we should remove the properties of // the old class Class<?> oldClass = getClassForName(oldClassName); if (oldClass != null) { Class<?> newClass = entity.getClass(); Set<Field> oldFields = new HashSet<Field>(); oldFields.addAll(Arrays.asList(ReflectionUtils.getDeclaredAndInheritedFields(oldClass, true))); oldFields.removeAll(Arrays.asList(ReflectionUtils.getDeclaredAndInheritedFields(newClass, true))); // remove the old fields for (Field field : oldFields) { if (node.hasProperty(field.getName())) { node.getProperty(field.getName()).remove(); } } } } } action.doUpdateClassNameToProperty(node, jcrNode, entity); } // special handling of JcrFile objects if (ReflectionUtils.extendsClass(entity.getClass(), JcrFile.class) && depth == 0) { fileNodeMapper.addFileNode(node, (JcrFile) entity, this); } for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(entityClass, true)) { field.setAccessible(true); if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrProperty.class) && nodeFilter.isDepthPropertyIncluded(depth)) { propertyMapper.updateProperty(field, entity, node, depth, nodeFilter, this); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrSerializedProperty.class) && nodeFilter.isDepthPropertyIncluded(depth)) { propertyMapper.updateSerializedProperty(field, entity, node, depth, nodeFilter); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrChildNode.class) && nodeFilter.isDepthIncluded(depth)) { // child nodes childNodeMapper.updateChildren(field, entity, node, depth, nodeFilter, this); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrReference.class)) { // references referenceMapper.updateReferences(field, entity, node, nodeFilter); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrFileNode.class) && nodeFilter.isDepthIncluded(depth)) { // file nodes fileNodeMapper.updateFiles(field, entity, node, this, depth, nodeFilter); } } // if name is different, then we move the node if (!node.getName().equals(getCleanName(getNodeName(entity)))) { boolean isVersionable = JcrUtils.hasMixinType(node, "mix:versionable") || JcrUtils.hasMixinType(node, NodeType.MIX_VERSIONABLE); Node parentNode = node.getParent(); if (isVersionable) { if (JcrUtils.hasMixinType(parentNode, "mix:versionable") || JcrUtils.hasMixinType(parentNode, NodeType.MIX_VERSIONABLE)) { JcrUtils.checkout(parentNode); } } // move node String nodeName = getCleanName(getNodeName(entity)); action.doMoveNode(parentNode, node, nodeName, jcrNode, entity); if (isVersionable) { if ((JcrUtils.hasMixinType(parentNode, "mix:versionable") || JcrUtils.hasMixinType(parentNode, NodeType.MIX_VERSIONABLE)) && parentNode.isCheckedOut()) { // Save session changes before checking-in the parent node node.getSession().save(); JcrUtils.checkin(parentNode); } } // update the object name and path setNodeName(entity, node.getName()); setNodePath(entity, node.getPath()); } // complete the update of the node action.doComplete(entity, node); return node; } private boolean isVersionable(Node node) throws RepositoryException { for (NodeType mixinType : node.getMixinNodeTypes()) { if (mixinType.getName().equals("mix:versionable") || mixinType.getName().equals(NodeType.MIX_VERSIONABLE)) { return true; } } return false; } Object mapNodeToClass(Object obj, Node node, NodeFilter nodeFilter, int depth) throws ClassNotFoundException, InstantiationException, RepositoryException, IllegalAccessException, IOException { if (!ReflectionUtils.extendsClass(obj.getClass(), JcrFile.class)) { // this does not apply for JcrFile extensions setNodeName(obj, node.getName()); } // construct history key HistoryKey key = new HistoryKey(); key.path = node.getPath(); if (nodeFilter.getMaxDepth() == NodeFilter.DEPTH_INFINITE) { // then use infinite depth as key depth key.depth = NodeFilter.DEPTH_INFINITE; } else { // calculate key depth from max depth - current depth key.depth = nodeFilter.getMaxDepth() - depth; } // now check the history key if (history.get() == null) { history.set(new HashMap<HistoryKey, Object>()); } if (history.get().containsKey(key)) { return history.get().get(key); } else { history.get().put(key, obj); } boolean allRequiredFieldsAreAssigned = true; Field jcrPropertyCheckSuccessfulField = null; for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(obj.getClass(), false)) { field.setAccessible(true); if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrProperty.class) && nodeFilter.isDepthPropertyIncluded(depth)) { allRequiredFieldsAreAssigned &= propertyMapper.mapPropertyToField(obj, field, node, depth, nodeFilter); } else if(jcrom.getAnnotationReader().isAnnotationPresent(field, JcrPropertyMap.class)) { if(field.getType().equals(Map.class)) { mapPropertiesToMap(obj, field, node); } else { LOG.error("The field [{}] within the class [{}] is not type of [java.util.Map]", field.getName(), obj.getClass().getCanonicalName()); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrSerializedProperty.class) && nodeFilter.isDepthPropertyIncluded(depth)) { propertyMapper.mapSerializedPropertyToField(obj, field, node, depth, nodeFilter); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrProtectedProperty.class)) { propertyMapper.mapProtectedPropertyToField(obj, field, node); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrUUID.class)) { if (node.hasProperty(Property.JCR_UUID)) { // field.set(obj, node.getUUID()); field.set(obj, node.getIdentifier()); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrIdentifier.class)) { field.set(obj, node.getIdentifier()); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrBaseVersionName.class)) { if (isVersionable(node)) { // Version baseVersion = node.getBaseVersion(); Version baseVersion = getVersionManager(node).getBaseVersion(node.getPath()); field.set(obj, baseVersion.getName()); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrBaseVersionCreated.class)) { if (isVersionable(node)) { // Version baseVersion = node.getBaseVersion(); Version baseVersion = getVersionManager(node).getBaseVersion(node.getPath()); field.set(obj, JcrUtils.getValue(field.getType(), node.getSession().getValueFactory().createValue(baseVersion.getCreated()))); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrVersionName.class)) { if (node.getParent() != null && node.getParent().isNodeType(NodeType.NT_VERSION)) { field.set(obj, node.getParent().getName()); } else if (isVersionable(node)) { // if we're not browsing version history, then this must be // the base version // Version baseVersion = node.getBaseVersion(); Version baseVersion = getVersionManager(node).getBaseVersion(node.getPath()); field.set(obj, baseVersion.getName()); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrVersionCreated.class)) { if (node.getParent() != null && node.getParent().isNodeType(NodeType.NT_VERSION)) { Version version = (Version) node.getParent(); field.set(obj, JcrUtils.getValue(field.getType(), node.getSession().getValueFactory().createValue(version.getCreated()))); } else if (isVersionable(node)) { // if we're not browsing version history, then this must be // the base version // Version baseVersion = node.getBaseVersion(); Version baseVersion = getVersionManager(node).getBaseVersion(node.getPath()); field.set(obj, JcrUtils.getValue(field.getType(), node.getSession().getValueFactory().createValue(baseVersion.getCreated()))); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrCheckedout.class)) { field.set(obj, node.isCheckedOut()); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrCreated.class)) { if (node.hasProperty(Property.JCR_CREATED)) { field.set(obj, JcrUtils.getValue(field.getType(), node.getProperty(Property.JCR_CREATED).getValue())); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrParentNode.class)) { Object parentObject = resolveParentObject(field, node); if (parentObject != null && field.getType().isInstance(parentObject)) { field.set(obj, parentObject); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrChildNode.class) && nodeFilter.isDepthIncluded(depth)) { if (Node.class.equals(field.getType())) { resolveChildNode(field, obj, node); } else { childNodeMapper.getChildrenFromNode(field, node, obj, depth, nodeFilter, this); } allRequiredFieldsAreAssigned &= resolveFieldRequired(field, obj); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrReference.class)) { referenceMapper.getReferencesFromNode(field, node, obj, depth, nodeFilter, this); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrFileNode.class) && nodeFilter.isDepthIncluded(depth)) { fileNodeMapper.getFilesFromNode(field, node, obj, depth, nodeFilter, this); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrPath.class)) { field.set(obj, node.getPath()); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrPropertyCheckSuccessful.class)) { if (jcrPropertyCheckSuccessfulField != null) { LOG.warn( "The class [{}] defines multiple fields with the annotation [JcrPropertyCheckSuccessful]. Change your implementation so that the annotation is only used once per class.", field.getDeclaringClass().getCanonicalName()); } jcrPropertyCheckSuccessfulField = field; } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrPageNode.class)) { resolveParentNodeOfType(node, JcrPageNode.CQ_PAGE_PRIMARY_TYPE, field, obj); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrPageContentNode.class)) { resolveParentNodeOfType(node, JcrPageContentNode.CQ_PAGE_CONTENT_PRIMARY_TYPE, field, obj); } } if (jcrPropertyCheckSuccessfulField != null) { jcrPropertyCheckSuccessfulField.set(obj, allRequiredFieldsAreAssigned); } return obj; } private void mapPropertiesToMap(Object obj, Field field, Node node) throws RepositoryException { JcrPropertyMap jcrPropertyMap = getJcrom().getAnnotationReader().getAnnotation(field, JcrPropertyMap.class); String childNodeName = field.getName(); if (JcrChildNode.DEFAULT_NAME.equals(jcrPropertyMap.name())) { childNodeName = jcrPropertyMap.name(); } if(node.hasNode(childNodeName)) { Node childNode = node.getNode(childNodeName); Map<String, Object> properties = new HashMap<String, Object>(); PropertyIterator propertyIterator = childNode.getProperties(); while(propertyIterator.hasNext()) { Property property = propertyIterator.nextProperty(); if(property.getValue().getType() == PropertyType.STRING) { properties.put(property.getName(), property.getValue().getString()); } } try { field.set(obj, properties); } catch (Exception ex) { LOG.error("Error while setting HashMap to property ["+ field.getName() +"] within class ["+ obj.getClass().getCanonicalName() +"]", ex); } } } private Object resolveParentObject(Field field, Node node) throws RepositoryException { JcrParentNode jcrParentNode = getJcrom().getAnnotationReader().getAnnotation(field, JcrParentNode.class); int relativeParentLevel = jcrParentNode.relativeParentLevel(); int absoluteParentLevel = jcrParentNode.absoluteParentLevel(); boolean relativeParentLevelDefined = JcrParentNode.DEFAULT_VALUE_PARENT_LEVEL != relativeParentLevel; boolean absoluteParentLevelDefined = JcrParentNode.DEFAULT_VALUE_PARENT_LEVEL != absoluteParentLevel; Node resolvedParentNode = null; if (!JcrParentNode.DEFAULT_PARENT_PATH.equals(jcrParentNode.relativeParentPath())) { if (node.hasNode(jcrParentNode.relativeParentPath())) { resolvedParentNode = node.getNode(jcrParentNode.relativeParentPath()); } else { LOG.debug("No node can be resolved with the relative path [{}] from the path [{}]", jcrParentNode.relativeParentPath(), node.getPath()); } } else if (relativeParentLevelDefined && absoluteParentLevelDefined) { throw new JcrParameterException("For field [" + field.getName() + "] of class [" + field.getDeclaringClass().getCanonicalName() + "] the parameters 'relativeParentLevel' as well as 'absoluteParentLevel' are defined but they are mutually exclusive to each other."); } else if (relativeParentLevelDefined && absoluteParentLevel < node.getDepth()) { resolvedParentNode = node; for (int i = 0; i < relativeParentLevel; i++) { resolvedParentNode = resolvedParentNode.getParent(); } } else if (absoluteParentLevelDefined && absoluteParentLevel < node.getDepth()) { resolvedParentNode = node; for (; resolvedParentNode.getDepth() > absoluteParentLevel; resolvedParentNode = resolvedParentNode.getParent()) ; } else { resolvedParentNode = node.getParent(); } Object resolvedParentObject = resolvedParentNode; if (resolvedParentObject != null && !Node.class.equals(field.getType())) { resolvedParentObject = jcrom.fromNode(field.getType(), resolvedParentNode); } return resolvedParentObject; } private boolean resolveFieldRequired(Field field, Object obj) throws IllegalAccessException { JcrChildNode jcrChildNode = getJcrom().getAnnotationReader().getAnnotation(field, JcrChildNode.class); boolean fieldRequired = !jcrChildNode.required() || jcrChildNode.required() && field.get(obj) != null; return fieldRequired; } private void resolveChildNode(Field field, Object obj, Node node) { JcrChildNode jcrChildNode = getJcrom().getAnnotationReader().getAnnotation(field, JcrChildNode.class); String childNodeName = field.getName(); if (!JcrChildNode.DEFAULT_NAME.equals(jcrChildNode.name())) { childNodeName = jcrChildNode.name(); } try { if (node.getSession().itemExists(node.getPath() + "/" + childNodeName)) { Node childNode = node.getNode(childNodeName); field.set(obj, childNode); } } catch (PathNotFoundException e) { LOG.info("The child node for field [" + field.getName() + "] of class [" + field.getDeclaringClass().getCanonicalName() + "] could not be resolved at ["+ childNodeName +"]"); } catch (IllegalAccessException e) { LOG.error("Error while setting child node for field [" + field.getName() + "] of class [" + field.getDeclaringClass().getCanonicalName() + "]", e); } catch (RepositoryException e) { LOG.error("Error while resolving child node for field [" + field.getName() + "] of class [" + field.getDeclaringClass().getCanonicalName() + "]", e); } } private void resolveParentNodeOfType(Node node, String nodeType, Field field, Object obj) { if (field.getType() != Node.class) { LOG.error("The field [{}] of class [{}] is not type of [javax.jcr.Node]", field.getName(), field.getDeclaringClass().getCanonicalName()); return; } try { for (Node selectedNode = node; selectedNode.getDepth() >= 0; selectedNode = selectedNode.getParent()) { if (nodeType.equals(selectedNode.getPrimaryNodeType().getName())) { field.set(obj, selectedNode); return; } } } catch (Exception ex) { LOG.error("Error while resolving parent node of type [" + nodeType + "]", ex); } } static VersionManager getVersionManager(Node node) throws RepositoryException { VersionManager versionMgr = node.getSession().getWorkspace().getVersionManager(); return versionMgr; } static Node getNodeById(Node node, String id) throws RepositoryException { // return node.getSession().getNodeByUUID(uuid); return node.getSession().getNodeByIdentifier(id); } /** * This is a temporary solution to enable lazy loading of single child nodes and single references. The problem is that Jcrom uses direct field * modification, but CGLIB fails to cascade field changes between the enhanced class and the lazy object. * * @param obj * @return * @throws java.lang.IllegalAccessException */ Object clearCglib(Object obj) throws IllegalAccessException { for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(obj.getClass(), true)) { field.setAccessible(true); if (field.getName().equals("CGLIB$LAZY_LOADER_0")) { if (field.get(obj) != null) { return field.get(obj); } else { // lazy loading has not been triggered yet, so // we do it manually return triggerLazyLoading(obj); } } } return obj; } Object triggerLazyLoading(Object obj) throws IllegalAccessException { for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(obj.getClass(), false)) { field.setAccessible(true); if (field.getName().equals("CGLIB$CALLBACK_0")) { try { return ((LazyLoader) field.get(obj)).loadObject(); } catch (Exception e) { throw new JcrMappingException("Could not trigger lazy loading", e); } } } return obj; } PropertyMapper getPropertyMapper() { return propertyMapper; } ReferenceMapper getReferenceMapper() { return referenceMapper; } FileNodeMapper getFileNodeMapper() { return fileNodeMapper; } ChildNodeMapper getChildNodeMapper() { return childNodeMapper; } Jcrom getJcrom() { return jcrom; } /** * Class for the history key. Contains the node path and the depth. Thanks to Leander for supplying this fix. */ private static class HistoryKey { private String path; private int depth; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + depth; result = prime * result + ((path == null) ? 0 : path.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } HistoryKey other = (HistoryKey) obj; if (depth != other.depth) { return false; } if (path == null) { if (other.path != null) { return false; } } else if (!path.equals(other.path)) { return false; } return true; } } }