X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=org.argeo.server.jcr%2Fsrc%2Fmain%2Fjava%2Forg%2Fargeo%2Fjcr%2Fspring%2FBeanNodeMapper.java;fp=org.argeo.server.jcr%2Fsrc%2Fmain%2Fjava%2Forg%2Fargeo%2Fjcr%2Fspring%2FBeanNodeMapper.java;h=9f70f5c58d2637381854a8ed213e6cff95df63c3;hb=1df1bf64759d35d3d72b9d96b26b71118fdbe031;hp=0000000000000000000000000000000000000000;hpb=3a3d316af102ba410d1d9e6de349d0c8f7ac044f;p=lgpl%2Fargeo-commons.git diff --git a/org.argeo.server.jcr/src/main/java/org/argeo/jcr/spring/BeanNodeMapper.java b/org.argeo.server.jcr/src/main/java/org/argeo/jcr/spring/BeanNodeMapper.java new file mode 100644 index 000000000..9f70f5c58 --- /dev/null +++ b/org.argeo.server.jcr/src/main/java/org/argeo/jcr/spring/BeanNodeMapper.java @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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; + } +}