X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=server%2Fruntime%2Forg.argeo.server.jackrabbit%2Fsrc%2Fmain%2Fjava%2Forg%2Fargeo%2Fjcr%2FBeanNodeMapper.java;h=a526bcf5ec0d06ed838392baa08aabfbccd38bb1;hb=946518ac41ee428e6cfd27b8d8b7e92b6d38470c;hp=931b11f5ca612e0b7688352137adecb1c136ade2;hpb=ff2edf71a6223a88a10d2f6c51db2797bfb0c4b9;p=lgpl%2Fargeo-commons.git diff --git a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jcr/BeanNodeMapper.java b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jcr/BeanNodeMapper.java index 931b11f5c..a526bcf5e 100644 --- a/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jcr/BeanNodeMapper.java +++ b/server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jcr/BeanNodeMapper.java @@ -1,78 +1,129 @@ package org.argeo.jcr; import java.beans.PropertyDescriptor; -import java.beans.PropertyEditor; 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.Item; 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.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; -public class BeanNodeMapper { - private final static Log log = LogFactory.getLog(BeanNodeMapper.class); +public class BeanNodeMapper implements NodeMapper { + // private final static Log log = LogFactory.getLog(BeanNodeMapper.class); - private Boolean versioning = false; + 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; - 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('/'); + 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); } - buf.append(obj.toString()); - return buf.toString(); } - public Node saveOrUpdate(Session session, Object obj) { - return saveOrUpdate(session, storagePath(obj), obj); + /** 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); } - public Node saveOrUpdate(Session session, String path, Object 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 { - BeanWrapper beanWrapper = new BeanWrapperImpl(obj); final Node node; - if (session.itemExists(path)) { - Item item = session.getItem(path); - if (!item.isNode()) - throw new ArgeoException("An item exist under " + path - + " but it is not a node: " + item); - node = (Node) item; - } else { - String parentPath = JcrUtils.parentPath(path); - Node parentNode; - if (session.itemExists(path)) - parentNode = (Node) session.getItem(parentPath); - else - parentNode = JcrUtils.mkdirs(session, parentPath, null, - versioning); - // create node - if (primaryNodeType != null) - node = parentNode.addNode(JcrUtils.lastPathElement(path), - primaryNodeType); - else - node = parentNode.addNode(JcrUtils.lastPathElement(path)); - } - - beanToNode(session, beanWrapper, 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, + 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; @@ -82,18 +133,176 @@ public class BeanNodeMapper { } } - protected void beanToNode(Session session, BeanWrapper beanWrapper, - Node node) throws RepositoryException { - if (log.isDebugEnabled()) - log.debug("Map bean to node " + node.getPath()); + /** + * 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 { + + 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(); + + // Process case of List and its derived classes + // 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(); + } + + NodeIterator valuesIt = childNode.getNodes(); + while (valuesIt.hasNext()) { + Node lstValueNode = valuesIt.nextNode(); + Object lstValue = nodeToBean(lstValueNode); + lst.add(lstValue); + } + + 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) - continue properties;// skip + 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); @@ -110,7 +319,13 @@ public class BeanNodeMapper { continue properties; } - Value val = asValue(session, value); + // 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; @@ -118,68 +333,135 @@ public class BeanNodeMapper { if (value instanceof List) { List lst = (List) value; - 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(session, lstValue); - if (values[i] != null) { - atLeastOneSet = true; - } else { - Node childNode = findChildReference(session, - new BeanWrapperImpl(lstValue)); - if (childNode != null) { - values[i] = session.getValueFactory().createValue( - childNode); - atLeastOneSet = true; - } - } - } - - if (!atLeastOneSet && lst.size() != 0) - throw new ArgeoException( - "This type of list is not supported " - + lst.getClass()); - - node.setProperty(name, values); + addList(node, name, lst); continue properties; } if (value instanceof Map) { Map map = (Map) value; - // TODO: add map specific type - Node mapNode = node.addNode(name); - for (Object key : map.keySet()) { - Object mapValue = map.get(key); - PropertyEditor pe = beanWrapper.findCustomEditor(key - .getClass(), null); - String keyStr = pe.getAsText(); - // TODO: check string format - Node entryNode = mapNode.addNode(keyStr); - beanToNode(session, new BeanWrapperImpl(mapValue), - entryNode); - } - + addMap(node, name, map); continue properties; } - BeanWrapper child = new BeanWrapperImpl(value); + BeanWrapper child = createBeanWrapper(value); // TODO: delegate to another mapper - Node childNode = findChildReference(session, child); - if (childNode != null) { - node.setProperty(name, childNode); - continue properties; - } + // TODO: deal with references + // Node childNode = findChildReference(session, child); + // if (childNode != null) { + // node.setProperty(name, childNode); + // continue properties; + // } // default case (recursive) - childNode = node.addNode(name); - beanToNode(session, child, childNode); - // if (childNode.isNodeType(ArgeoJcrConstants.MIX_REFERENCEABLE)) { - // log.debug("Add reference to " + childNode.getPath()); - // node.setProperty(name, childNode); + 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 */ @@ -198,7 +480,11 @@ public class BeanNodeMapper { return valueFactory.createValue((Boolean) value); else if (value instanceof Calendar) return valueFactory.createValue((Calendar) value); - else if (value instanceof CharSequence) + 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) return valueFactory.createValue((InputStream) value); @@ -206,6 +492,52 @@ public class BeanNodeMapper { 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.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)) { @@ -221,6 +553,15 @@ public class BeanNodeMapper { 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; } @@ -229,4 +570,35 @@ public class BeanNodeMapper { 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; + } }