/* * Copyright (C) 2007-2012 Mathieu Baudier * * 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.argeo.jcr.spring; import java.beans.PropertyDescriptor; import java.io.InputStream; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import javax.jcr.Binary; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.ValueFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.ArgeoException; import org.argeo.jcr.JcrUtils; import org.argeo.jcr.NodeMapper; import org.argeo.jcr.NodeMapperProvider; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; public class BeanNodeMapper implements NodeMapper { private final static Log log = LogFactory.getLog(BeanNodeMapper.class); private final static String NODE_VALUE = "value"; // private String keyNode = "bean:key"; private String uuidProperty = "uuid"; private String classProperty = "class"; private Boolean versioning = false; private Boolean strictUuidReference = false; // TODO define a primaryNodeType Strategy private String primaryNodeType = null; private ClassLoader classLoader = getClass().getClassLoader(); private NodeMapperProvider nodeMapperProvider; /** * exposed method to retrieve a bean from a node */ public Object load(Node node) { try { if (nodeMapperProvider != null) { NodeMapper nodeMapper = nodeMapperProvider.findNodeMapper(node); if (nodeMapper != this) { return nodeMapper.load(node); } } return nodeToBean(node); } catch (RepositoryException e) { throw new ArgeoException("Cannot load object from node " + node, e); } } /** Update an existing node with an object */ public void update(Node node, Object obj) { try { if (nodeMapperProvider != null) { NodeMapper nodeMapper = nodeMapperProvider.findNodeMapper(node); if (nodeMapper != this) { nodeMapper.update(node, obj); } else beanToNode(createBeanWrapper(obj), node); } else beanToNode(createBeanWrapper(obj), node); } catch (RepositoryException e) { throw new ArgeoException("Cannot update node " + node + " with " + obj, e); } } /** * if no storage path is given; we use canonical path * * @see this.storagePath() */ public Node save(Session session, Object obj) { return save(session, storagePath(obj), obj); } /** * Create a new node to store an object. If the parentNode doesn't exist, it * is created * * the primaryNodeType may be initialized before */ public Node save(Session session, String path, Object obj) { try { final Node node; String parentPath = JcrUtils.parentPath(path); // find or create parent node Node parentNode; if (session.itemExists(path)) parentNode = (Node) session.getItem(parentPath); else { parentNode = JcrUtils.mkdirs(session, parentPath, null, null, versioning); } // create node if (primaryNodeType != null) node = parentNode.addNode(JcrUtils.lastPathElement(path), primaryNodeType); else node = parentNode.addNode(JcrUtils.lastPathElement(path)); // Check specific cases if (nodeMapperProvider != null) { NodeMapper nodeMapper = nodeMapperProvider.findNodeMapper(node); if (nodeMapper != this) { nodeMapper.update(node, obj); return node; } } update(node, obj); return node; } catch (ArgeoException e) { throw e; } catch (Exception e) { throw new ArgeoException("Cannot save or update " + obj + " under " + path, e); } } /** * Parse the FQN of a class to string with '/' delimiters Prefix the * returned string with "/objects/" */ public String storagePath(Object obj) { String clss = obj.getClass().getName(); StringBuffer buf = new StringBuffer("/objects/"); StringTokenizer st = new StringTokenizer(clss, "."); while (st.hasMoreTokens()) { buf.append(st.nextToken()).append('/'); } buf.append(obj.toString()); return buf.toString(); } @SuppressWarnings("unchecked") /** * Transforms a node into an object of the class defined by classProperty Property */ protected Object nodeToBean(Node node) throws RepositoryException { if (log.isTraceEnabled()) log.trace("Load " + node); try { String clssName = node.getProperty(classProperty).getValue() .getString(); BeanWrapper beanWrapper = createBeanWrapper(loadClass(clssName)); // process properties PropertyIterator propIt = node.getProperties(); props: while (propIt.hasNext()) { Property prop = propIt.nextProperty(); if (!beanWrapper.isWritableProperty(prop.getName())) continue props; PropertyDescriptor pd = beanWrapper.getPropertyDescriptor(prop .getName()); Class propClass = pd.getPropertyType(); if (log.isTraceEnabled()) log.trace("Load " + prop + ", propClass=" + propClass + ", property descriptor=" + pd); // primitive list if (propClass != null && List.class.isAssignableFrom(propClass)) { List lst = new ArrayList(); Class valuesClass = classFromProperty(prop); if (valuesClass != null) for (Value value : prop.getValues()) { lst.add(asObject(value, valuesClass)); } continue props; } // Case of other type of property accepted by jcr // Long, Double, String, Binary, Date, Boolean, Name Object value = asObject(prop.getValue(), pd.getPropertyType()); if (value != null) beanWrapper.setPropertyValue(prop.getName(), value); } // process children nodes NodeIterator nodeIt = node.getNodes(); nodes: while (nodeIt.hasNext()) { Node childNode = nodeIt.nextNode(); String name = childNode.getName(); if (!beanWrapper.isWritableProperty(name)) continue nodes; PropertyDescriptor pd = beanWrapper.getPropertyDescriptor(name); Class propClass = pd.getPropertyType(); // objects list if (propClass != null && List.class.isAssignableFrom(propClass)) { String lstClass = childNode.getProperty(classProperty) .getString(); List lst; try { lst = (List) loadClass(lstClass).newInstance(); } catch (Exception e) { lst = new ArrayList(); } if (childNode.hasNodes()) { // Look for children nodes NodeIterator valuesIt = childNode.getNodes(); while (valuesIt.hasNext()) { Node lstValueNode = valuesIt.nextNode(); Object lstValue = nodeToBean(lstValueNode); lst.add(lstValue); } } else { // look for a property with the same name which will // provide // primitives Property childProp = childNode.getProperty(childNode .getName()); Class valuesClass = classFromProperty(childProp); if (valuesClass != null) if (childProp.getDefinition().isMultiple()) for (Value value : childProp.getValues()) { lst.add(asObject(value, valuesClass)); } else lst.add(asObject(childProp.getValue(), valuesClass)); } beanWrapper.setPropertyValue(name, lst); continue nodes; } // objects map if (propClass != null && Map.class.isAssignableFrom(propClass)) { String mapClass = childNode.getProperty(classProperty) .getString(); Map map; try { map = (Map) loadClass(mapClass) .newInstance(); } catch (Exception e) { map = new HashMap(); } // properties PropertyIterator keysPropIt = childNode.getProperties(); keyProps: while (keysPropIt.hasNext()) { Property keyProp = keysPropIt.nextProperty(); // FIXME: use property editor String key = keyProp.getName(); if (classProperty.equals(key)) continue keyProps; Class keyPropClass = classFromProperty(keyProp); if (keyPropClass != null) { Object mapValue = asObject(keyProp.getValue(), keyPropClass); map.put(key, mapValue); } } // node NodeIterator keysIt = childNode.getNodes(); while (keysIt.hasNext()) { Node mapValueNode = keysIt.nextNode(); // FIXME: use property editor Object key = mapValueNode.getName(); Object mapValue = nodeToBean(mapValueNode); map.put(key, mapValue); } beanWrapper.setPropertyValue(name, map); continue nodes; } // default Object value = nodeToBean(childNode); beanWrapper.setPropertyValue(name, value); } return beanWrapper.getWrappedInstance(); } catch (Exception e) { throw new ArgeoException("Cannot map node " + node, e); } } /** * Transforms an object to the specified jcr Node in order to persist it. * * @param beanWrapper * @param node * @throws RepositoryException */ protected void beanToNode(BeanWrapper beanWrapper, Node node) throws RepositoryException { properties: for (PropertyDescriptor pd : beanWrapper .getPropertyDescriptors()) { String name = pd.getName(); if (!beanWrapper.isReadableProperty(name)) continue properties;// skip Object value = beanWrapper.getPropertyValue(name); if (value == null) { // remove values when updating if (node.hasProperty(name)) node.setProperty(name, (Value) null); if (node.hasNode(name)) node.getNode(name).remove(); continue properties; } // if (uuidProperty != null && uuidProperty.equals(name)) { // // node.addMixin(ArgeoJcrConstants.MIX_REFERENCEABLE); // node.setProperty(ArgeoJcrConstants.JCR_UUID, value.toString()); // continue properties; // } if ("class".equals(name)) { if (classProperty != null) { node.setProperty(classProperty, ((Class) value).getName()); // TODO: store a class hierarchy? } continue properties; } // Some bean reference other classes. We must deal with this case if (value instanceof Class) { node.setProperty(name, ((Class) value).getName()); continue properties; } Value val = asValue(node.getSession(), value); if (val != null) { node.setProperty(name, val); continue properties; } if (value instanceof List) { List lst = (List) value; addList(node, name, lst); continue properties; } if (value instanceof Map) { Map map = (Map) value; addMap(node, name, map); continue properties; } BeanWrapper child = createBeanWrapper(value); // TODO: delegate to another mapper // TODO: deal with references // Node childNode = findChildReference(session, child); // if (childNode != null) { // node.setProperty(name, childNode); // continue properties; // } // default case (recursive) if (node.hasNode(name)) {// update // TODO: optimize node.getNode(name).remove(); } Node childNode = node.addNode(name); beanToNode(child, childNode); } } /** * Process specific case of list * * @param node * @param name * @param lst * @throws RepositoryException */ protected void addList(Node node, String name, List lst) throws RepositoryException { if (node.hasNode(name)) {// update // TODO: optimize node.getNode(name).remove(); } Node listNode = node.addNode(name); listNode.setProperty(classProperty, lst.getClass().getName()); Value[] values = new Value[lst.size()]; boolean atLeastOneSet = false; for (int i = 0; i < lst.size(); i++) { Object lstValue = lst.get(i); values[i] = asValue(node.getSession(), lstValue); if (values[i] != null) { atLeastOneSet = true; } else { Node childNode = findChildReference(node.getSession(), createBeanWrapper(lstValue)); if (childNode != null) { values[i] = node.getSession().getValueFactory() .createValue(childNode); atLeastOneSet = true; } } } // will be either properties or nodes, not both if (!atLeastOneSet && lst.size() != 0) { for (Object lstValue : lst) { Node childNode = listNode.addNode(NODE_VALUE); beanToNode(createBeanWrapper(lstValue), childNode); } } else { listNode.setProperty(name, values); } } /** * Process specific case of maps. * * @param node * @param name * @param map * @throws RepositoryException */ protected void addMap(Node node, String name, Map map) throws RepositoryException { if (node.hasNode(name)) {// update // TODO: optimize node.getNode(name).remove(); } Node mapNode = node.addNode(name); mapNode.setProperty(classProperty, map.getClass().getName()); for (Object key : map.keySet()) { Object mapValue = map.get(key); // PropertyEditor pe = beanWrapper.findCustomEditor(key.getClass(), // null); String keyStr; // if (pe == null) { if (key instanceof CharSequence) keyStr = key.toString(); else throw new ArgeoException( "Cannot find property editor for class " + key.getClass()); // } else { // pe.setValue(key); // keyStr = pe.getAsText(); // } // TODO: check string format Value mapVal = asValue(node.getSession(), mapValue); if (mapVal != null) mapNode.setProperty(keyStr, mapVal); else { Node entryNode = mapNode.addNode(keyStr); beanToNode(createBeanWrapper(mapValue), entryNode); } } } protected BeanWrapper createBeanWrapper(Object obj) { return new BeanWrapperImpl(obj); } protected BeanWrapper createBeanWrapper(Class clss) { return new BeanWrapperImpl(clss); } /** Returns null if value cannot be found */ protected Value asValue(Session session, Object value) throws RepositoryException { ValueFactory valueFactory = session.getValueFactory(); if (value instanceof Integer) return valueFactory.createValue((Integer) value); else if (value instanceof Long) return valueFactory.createValue((Long) value); else if (value instanceof Float) return valueFactory.createValue((Float) value); else if (value instanceof Double) return valueFactory.createValue((Double) value); else if (value instanceof Boolean) return valueFactory.createValue((Boolean) value); else if (value instanceof Calendar) return valueFactory.createValue((Calendar) value); else if (value instanceof Date) { Calendar cal = new GregorianCalendar(); cal.setTime((Date) value); return valueFactory.createValue(cal); } else if (value instanceof CharSequence) return valueFactory.createValue(value.toString()); else if (value instanceof InputStream) { Binary binary = session.getValueFactory().createBinary( (InputStream) value); return valueFactory.createValue(binary); } else return null; } protected Class classFromProperty(Property property) throws RepositoryException { switch (property.getType()) { case PropertyType.LONG: return Long.class; case PropertyType.DOUBLE: return Double.class; case PropertyType.STRING: return String.class; case PropertyType.BOOLEAN: return Boolean.class; case PropertyType.DATE: return Calendar.class; case PropertyType.NAME: return null; default: throw new ArgeoException("Cannot find class for property " + property + ", type=" + PropertyType.nameFromValue(property.getType())); } } protected Object asObject(Value value, Class propClass) throws RepositoryException { if (propClass.equals(Integer.class)) return (int) value.getLong(); else if (propClass.equals(Long.class)) return value.getLong(); else if (propClass.equals(Float.class)) return (float) value.getDouble(); else if (propClass.equals(Double.class)) return value.getDouble(); else if (propClass.equals(Boolean.class)) return value.getBoolean(); else if (CharSequence.class.isAssignableFrom(propClass)) return value.getString(); else if (InputStream.class.isAssignableFrom(propClass)) return value.getBinary().getStream(); else if (Calendar.class.isAssignableFrom(propClass)) return value.getDate(); else if (Date.class.isAssignableFrom(propClass)) return value.getDate().getTime(); else return null; } protected Node findChildReference(Session session, BeanWrapper child) throws RepositoryException { if (child.isReadableProperty(uuidProperty)) { String childUuid = child.getPropertyValue(uuidProperty).toString(); try { return session.getNodeByIdentifier(childUuid); } catch (ItemNotFoundException e) { if (strictUuidReference) throw new ArgeoException("No node found with uuid " + childUuid, e); } } return null; } protected Class loadClass(String name) { // log.debug("Class loader: " + classLoader); try { return classLoader.loadClass(name); } catch (ClassNotFoundException e) { throw new ArgeoException("Cannot load class " + name, e); } } protected String propertyName(String name) { return name; } public void setVersioning(Boolean versioning) { this.versioning = versioning; } public void setUuidProperty(String uuidProperty) { this.uuidProperty = uuidProperty; } public void setClassProperty(String classProperty) { this.classProperty = classProperty; } public void setStrictUuidReference(Boolean strictUuidReference) { this.strictUuidReference = strictUuidReference; } public void setPrimaryNodeType(String primaryNodeType) { this.primaryNodeType = primaryNodeType; } public void setClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } public void setNodeMapperProvider(NodeMapperProvider nodeMapperProvider) { this.nodeMapperProvider = nodeMapperProvider; } public String getPrimaryNodeType() { return this.primaryNodeType; } public String getClassProperty() { return this.classProperty; } }