--- /dev/null
+/*
+ * 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<Object> lst = new ArrayList<Object>();
+ 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<Object> lst;
+ try {
+ lst = (List<Object>) loadClass(lstClass).newInstance();
+ } catch (Exception e) {
+ lst = new ArrayList<Object>();
+ }
+
+ 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<Object, Object> map;
+ try {
+ map = (Map<Object, Object>) loadClass(mapClass)
+ .newInstance();
+ } catch (Exception e) {
+ map = new HashMap<Object, Object>();
+ }
+
+ // 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;
+ }
+}