/** * 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.Property; import javax.jcr.RepositoryException; import javax.jcr.nodetype.NodeType; import javax.jcr.version.Version; import javax.jcr.version.VersionManager; import javafx.beans.property.StringProperty; 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.JcrParentNode; import org.jcrom.annotations.JcrPath; import org.jcrom.annotations.JcrProperty; 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.*; import static org.jcrom.util.JavaFXUtils.getObject; import static org.jcrom.util.JavaFXUtils.getType; import static org.jcrom.util.JavaFXUtils.setObject; /** * 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 */ public class Mapper { static final String DEFAULT_FIELDNAME = "fieldName"; /** 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; protected PropertyMapper propertyMapper; protected ReferenceMapper referenceMapper; protected FileNodeMapper fileNodeMapper; protected 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 */ public 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 { Field field = findNameField(object); return (String) JavaFXUtils.getObject(field, object); } String getNodePath(Object object) throws IllegalAccessException { Field field = findPathField(object); return (String) JavaFXUtils.getObject(field, object); } Object getParentObject(Object childObject) throws IllegalAccessException { Field parentField = findParentField(childObject); return parentField != null ? getObject(parentField, 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) getObject(idField, 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 { Field field = findNameField(object); setObject(field, object, name); } void setNodePath(Object object, String path) throws IllegalAccessException { Field field = findPathField(object); setObject(field, 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) { setObject(uuidField, object, uuid); } } void setId(Object object, String id) throws IllegalAccessException { Field idField = findIdField(object); if (idField != null) { setObject(idField, 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, parentObj, 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, null, 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 { if (javafx.beans.property.Property.class.isAssignableFrom(entity.getClass())) { entity = ((javafx.beans.property.Property) entity).getValue(); } 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; } protected Object mapNodeToClass(Object obj, Node node, NodeFilter nodeFilter, Object parentObject, 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); } for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(obj.getClass(), false)) { field.setAccessible(true); if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrProperty.class) && nodeFilter.isDepthPropertyIncluded(depth)) { propertyMapper.mapPropertyToField(obj, field, node, depth, nodeFilter); } 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()); setObject(field, obj, node.getIdentifier()); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrIdentifier.class)) { setObject(field, 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()); setObject(field, 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()); setObject(field, 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)) { setObject(field, 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()); setObject(field, 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(); setObject(field, 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()); setObject(field, obj, JcrUtils.getValue(field.getType(), node.getSession().getValueFactory().createValue(baseVersion.getCreated()))); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrCheckedout.class)) { setObject(field, obj, node.isCheckedOut()); } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrCreated.class)) { if (node.hasProperty(Property.JCR_CREATED)) { setObject(field, obj, JcrUtils.getValue(field.getType(), node.getProperty(Property.JCR_CREATED).getValue())); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrParentNode.class)) { if (parentObject != null && getType(field, obj).isInstance(parentObject)) { setObject(field, obj, parentObject); } } else if (jcrom.getAnnotationReader().isAnnotationPresent(field, JcrChildNode.class) && nodeFilter.isDepthIncluded(depth)) { childNodeMapper.getChildrenFromNode(field, node, obj, depth, nodeFilter, this); } 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)) { setObject(field, obj, node.getPath()); } } return obj; } 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 (getObject(field, obj) != null) { return getObject(field, 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) getObject(field, 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; } } }