]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/BeanNodeMapper.java
Improve webdav
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / BeanNodeMapper.java
1 package org.argeo.jcr;
2
3 import java.beans.PropertyDescriptor;
4 import java.io.InputStream;
5 import java.util.ArrayList;
6 import java.util.Calendar;
7 import java.util.Date;
8 import java.util.GregorianCalendar;
9 import java.util.HashMap;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.StringTokenizer;
13
14 import javax.jcr.ItemNotFoundException;
15 import javax.jcr.Node;
16 import javax.jcr.NodeIterator;
17 import javax.jcr.Property;
18 import javax.jcr.PropertyIterator;
19 import javax.jcr.PropertyType;
20 import javax.jcr.RepositoryException;
21 import javax.jcr.Session;
22 import javax.jcr.Value;
23 import javax.jcr.ValueFactory;
24
25 import org.apache.commons.logging.Log;
26 import org.apache.commons.logging.LogFactory;
27 import org.argeo.ArgeoException;
28 import org.springframework.beans.BeanWrapper;
29 import org.springframework.beans.BeanWrapperImpl;
30
31 public class BeanNodeMapper implements NodeMapper {
32 private final static Log log = LogFactory.getLog(BeanNodeMapper.class);
33
34 private final static String NODE_VALUE = "value";
35
36 // private String keyNode = "bean:key";
37 private String uuidProperty = "uuid";
38 private String classProperty = "class";
39
40 private Boolean versioning = false;
41 private Boolean strictUuidReference = false;
42
43 // TODO define a primaryNodeType Strategy
44 private String primaryNodeType = null;
45
46 private ClassLoader classLoader = getClass().getClassLoader();
47
48 private NodeMapperProvider nodeMapperProvider;
49
50 /**
51 * exposed method to retrieve a bean from a node
52 */
53 public Object load(Node node) {
54 try {
55 if (nodeMapperProvider != null) {
56 NodeMapper nodeMapper = nodeMapperProvider.findNodeMapper(node);
57 if (nodeMapper != this) {
58 return nodeMapper.load(node);
59 }
60 }
61 return nodeToBean(node);
62 } catch (RepositoryException e) {
63 throw new ArgeoException("Cannot load object from node " + node, e);
64 }
65 }
66
67 /** Update an existing node with an object */
68 public void update(Node node, Object obj) {
69 try {
70 if (nodeMapperProvider != null) {
71
72 NodeMapper nodeMapper = nodeMapperProvider.findNodeMapper(node);
73 if (nodeMapper != this) {
74 nodeMapper.update(node, obj);
75 } else
76 beanToNode(createBeanWrapper(obj), node);
77 } else
78 beanToNode(createBeanWrapper(obj), node);
79 } catch (RepositoryException e) {
80 throw new ArgeoException("Cannot update node " + node + " with "
81 + obj, e);
82 }
83 }
84
85 /**
86 * if no storage path is given; we use canonical path
87 *
88 * @see this.storagePath()
89 */
90 public Node save(Session session, Object obj) {
91 return save(session, storagePath(obj), obj);
92 }
93
94 /**
95 * Create a new node to store an object. If the parentNode doesn't exist, it
96 * is created
97 *
98 * the primaryNodeType may be initialized before
99 */
100 public Node save(Session session, String path, Object obj) {
101 try {
102 final Node node;
103 String parentPath = JcrUtils.parentPath(path);
104 // find or create parent node
105 Node parentNode;
106 if (session.itemExists(path))
107 parentNode = (Node) session.getItem(parentPath);
108 else {
109 parentNode = JcrUtils.mkdirs(session, parentPath, null,
110 versioning);
111 }
112 // create node
113
114 if (primaryNodeType != null)
115 node = parentNode.addNode(JcrUtils.lastPathElement(path),
116 primaryNodeType);
117 else
118 node = parentNode.addNode(JcrUtils.lastPathElement(path));
119
120 // Check specific cases
121 if (nodeMapperProvider != null) {
122 NodeMapper nodeMapper = nodeMapperProvider.findNodeMapper(node);
123 if (nodeMapper != this) {
124 nodeMapper.update(node, obj);
125 return node;
126 }
127 }
128 update(node, obj);
129 return node;
130 } catch (ArgeoException e) {
131 throw e;
132 } catch (Exception e) {
133 throw new ArgeoException("Cannot save or update " + obj + " under "
134 + path, e);
135 }
136 }
137
138 /**
139 * Parse the FQN of a class to string with '/' delimiters Prefix the
140 * returned string with "/objects/"
141 */
142 public String storagePath(Object obj) {
143 String clss = obj.getClass().getName();
144 StringBuffer buf = new StringBuffer("/objects/");
145 StringTokenizer st = new StringTokenizer(clss, ".");
146 while (st.hasMoreTokens()) {
147 buf.append(st.nextToken()).append('/');
148 }
149 buf.append(obj.toString());
150 return buf.toString();
151 }
152
153 @SuppressWarnings("unchecked")
154 /**
155 * Transforms a node into an object of the class defined by classProperty Property
156 */
157 protected Object nodeToBean(Node node) throws RepositoryException {
158 if (log.isTraceEnabled())
159 log.trace("Load " + node);
160
161 try {
162 String clssName = node.getProperty(classProperty).getValue()
163 .getString();
164
165 BeanWrapper beanWrapper = createBeanWrapper(loadClass(clssName));
166
167 // process properties
168 PropertyIterator propIt = node.getProperties();
169 props: while (propIt.hasNext()) {
170 Property prop = propIt.nextProperty();
171 if (!beanWrapper.isWritableProperty(prop.getName()))
172 continue props;
173
174 PropertyDescriptor pd = beanWrapper.getPropertyDescriptor(prop
175 .getName());
176 Class propClass = pd.getPropertyType();
177
178 if (log.isTraceEnabled())
179 log.trace("Load " + prop + ", propClass=" + propClass
180 + ", property descriptor=" + pd);
181
182 // primitive list
183 if (propClass != null && List.class.isAssignableFrom(propClass)) {
184 List<Object> lst = new ArrayList<Object>();
185 Class<?> valuesClass = classFromProperty(prop);
186 if (valuesClass != null)
187 for (Value value : prop.getValues()) {
188 lst.add(asObject(value, valuesClass));
189 }
190 continue props;
191 }
192
193 // Case of other type of property accepted by jcr
194 // Long, Double, String, Binary, Date, Boolean, Name
195 Object value = asObject(prop.getValue(), pd.getPropertyType());
196 if (value != null)
197 beanWrapper.setPropertyValue(prop.getName(), value);
198 }
199
200 // process children nodes
201 NodeIterator nodeIt = node.getNodes();
202 nodes: while (nodeIt.hasNext()) {
203 Node childNode = nodeIt.nextNode();
204 String name = childNode.getName();
205 if (!beanWrapper.isWritableProperty(name))
206 continue nodes;
207
208 PropertyDescriptor pd = beanWrapper.getPropertyDescriptor(name);
209 Class propClass = pd.getPropertyType();
210
211 // objects list
212 if (propClass != null && List.class.isAssignableFrom(propClass)) {
213 String lstClass = childNode.getProperty(classProperty)
214 .getString();
215 List<Object> lst;
216 try {
217 lst = (List<Object>) loadClass(lstClass).newInstance();
218 } catch (Exception e) {
219 lst = new ArrayList<Object>();
220 }
221
222 if (childNode.hasNodes()) {
223 // Look for children nodes
224 NodeIterator valuesIt = childNode.getNodes();
225 while (valuesIt.hasNext()) {
226 Node lstValueNode = valuesIt.nextNode();
227 Object lstValue = nodeToBean(lstValueNode);
228 lst.add(lstValue);
229 }
230 } else {
231 // look for a property with the same name which will
232 // provide
233 // primitives
234 Property childProp = childNode.getProperty(childNode
235 .getName());
236 Class<?> valuesClass = classFromProperty(childProp);
237 if (valuesClass != null)
238 if (childProp.getDefinition().isMultiple())
239 for (Value value : childProp.getValues()) {
240 lst.add(asObject(value, valuesClass));
241 }
242 else
243 lst.add(asObject(childProp.getValue(),
244 valuesClass));
245 }
246 beanWrapper.setPropertyValue(name, lst);
247 continue nodes;
248 }
249
250 // objects map
251 if (propClass != null && Map.class.isAssignableFrom(propClass)) {
252 String mapClass = childNode.getProperty(classProperty)
253 .getString();
254 Map<Object, Object> map;
255 try {
256 map = (Map<Object, Object>) loadClass(mapClass)
257 .newInstance();
258 } catch (Exception e) {
259 map = new HashMap<Object, Object>();
260 }
261
262 // properties
263 PropertyIterator keysPropIt = childNode.getProperties();
264 keyProps: while (keysPropIt.hasNext()) {
265 Property keyProp = keysPropIt.nextProperty();
266 // FIXME: use property editor
267 String key = keyProp.getName();
268 if (classProperty.equals(key))
269 continue keyProps;
270
271 Class keyPropClass = classFromProperty(keyProp);
272 if (keyPropClass != null) {
273 Object mapValue = asObject(keyProp.getValue(),
274 keyPropClass);
275 map.put(key, mapValue);
276 }
277 }
278
279 // node
280 NodeIterator keysIt = childNode.getNodes();
281 while (keysIt.hasNext()) {
282 Node mapValueNode = keysIt.nextNode();
283 // FIXME: use property editor
284 Object key = mapValueNode.getName();
285
286 Object mapValue = nodeToBean(mapValueNode);
287
288 map.put(key, mapValue);
289 }
290 beanWrapper.setPropertyValue(name, map);
291 continue nodes;
292 }
293
294 // default
295 Object value = nodeToBean(childNode);
296 beanWrapper.setPropertyValue(name, value);
297
298 }
299 return beanWrapper.getWrappedInstance();
300 } catch (Exception e) {
301 throw new ArgeoException("Cannot map node " + node, e);
302 }
303 }
304
305 /**
306 * Transforms an object to the specified jcr Node in order to persist it.
307 *
308 * @param beanWrapper
309 * @param node
310 * @throws RepositoryException
311 */
312 protected void beanToNode(BeanWrapper beanWrapper, Node node)
313 throws RepositoryException {
314 properties: for (PropertyDescriptor pd : beanWrapper
315 .getPropertyDescriptors()) {
316 String name = pd.getName();
317 if (!beanWrapper.isReadableProperty(name))
318 continue properties;// skip
319
320 Object value = beanWrapper.getPropertyValue(name);
321 if (value == null) {
322 // remove values when updating
323 if (node.hasProperty(name))
324 node.setProperty(name, (Value) null);
325 if (node.hasNode(name))
326 node.getNode(name).remove();
327
328 continue properties;
329 }
330
331 // if (uuidProperty != null && uuidProperty.equals(name)) {
332 // // node.addMixin(ArgeoJcrConstants.MIX_REFERENCEABLE);
333 // node.setProperty(ArgeoJcrConstants.JCR_UUID, value.toString());
334 // continue properties;
335 // }
336
337 if ("class".equals(name)) {
338 if (classProperty != null) {
339 node.setProperty(classProperty, ((Class<?>) value)
340 .getName());
341 // TODO: store a class hierarchy?
342 }
343 continue properties;
344 }
345
346 // Some bean reference other classes. We must deal with this case
347 if (value instanceof Class<?>) {
348 node.setProperty(name, ((Class<?>) value).getName());
349 continue properties;
350 }
351
352 Value val = asValue(node.getSession(), value);
353 if (val != null) {
354 node.setProperty(name, val);
355 continue properties;
356 }
357
358 if (value instanceof List<?>) {
359 List<?> lst = (List<?>) value;
360 addList(node, name, lst);
361 continue properties;
362 }
363
364 if (value instanceof Map<?, ?>) {
365 Map<?, ?> map = (Map<?, ?>) value;
366 addMap(node, name, map);
367 continue properties;
368 }
369
370 BeanWrapper child = createBeanWrapper(value);
371 // TODO: delegate to another mapper
372
373 // TODO: deal with references
374 // Node childNode = findChildReference(session, child);
375 // if (childNode != null) {
376 // node.setProperty(name, childNode);
377 // continue properties;
378 // }
379
380 // default case (recursive)
381 if (node.hasNode(name)) {// update
382 // TODO: optimize
383 node.getNode(name).remove();
384 }
385 Node childNode = node.addNode(name);
386 beanToNode(child, childNode);
387 }
388 }
389
390 /**
391 * Process specific case of list
392 *
393 * @param node
394 * @param name
395 * @param lst
396 * @throws RepositoryException
397 */
398 protected void addList(Node node, String name, List<?> lst)
399 throws RepositoryException {
400 if (node.hasNode(name)) {// update
401 // TODO: optimize
402 node.getNode(name).remove();
403 }
404
405 Node listNode = node.addNode(name);
406 listNode.setProperty(classProperty, lst.getClass().getName());
407 Value[] values = new Value[lst.size()];
408 boolean atLeastOneSet = false;
409 for (int i = 0; i < lst.size(); i++) {
410 Object lstValue = lst.get(i);
411 values[i] = asValue(node.getSession(), lstValue);
412 if (values[i] != null) {
413 atLeastOneSet = true;
414 } else {
415 Node childNode = findChildReference(node.getSession(),
416 createBeanWrapper(lstValue));
417 if (childNode != null) {
418 values[i] = node.getSession().getValueFactory()
419 .createValue(childNode);
420 atLeastOneSet = true;
421 }
422 }
423 }
424
425 // will be either properties or nodes, not both
426 if (!atLeastOneSet && lst.size() != 0) {
427 for (Object lstValue : lst) {
428 Node childNode = listNode.addNode(NODE_VALUE);
429 beanToNode(createBeanWrapper(lstValue), childNode);
430 }
431 } else {
432 listNode.setProperty(name, values);
433 }
434 }
435
436 /**
437 * Process specific case of maps.
438 *
439 * @param node
440 * @param name
441 * @param map
442 * @throws RepositoryException
443 */
444 protected void addMap(Node node, String name, Map<?, ?> map)
445 throws RepositoryException {
446 if (node.hasNode(name)) {// update
447 // TODO: optimize
448 node.getNode(name).remove();
449 }
450
451 Node mapNode = node.addNode(name);
452 mapNode.setProperty(classProperty, map.getClass().getName());
453 for (Object key : map.keySet()) {
454 Object mapValue = map.get(key);
455 // PropertyEditor pe = beanWrapper.findCustomEditor(key.getClass(),
456 // null);
457 String keyStr;
458 // if (pe == null) {
459 if (key instanceof CharSequence)
460 keyStr = key.toString();
461 else
462 throw new ArgeoException(
463 "Cannot find property editor for class "
464 + key.getClass());
465 // } else {
466 // pe.setValue(key);
467 // keyStr = pe.getAsText();
468 // }
469 // TODO: check string format
470
471 Value mapVal = asValue(node.getSession(), mapValue);
472 if (mapVal != null)
473 mapNode.setProperty(keyStr, mapVal);
474 else {
475 Node entryNode = mapNode.addNode(keyStr);
476 beanToNode(createBeanWrapper(mapValue), entryNode);
477 }
478
479 }
480
481 }
482
483 protected BeanWrapper createBeanWrapper(Object obj) {
484 return new BeanWrapperImpl(obj);
485 }
486
487 protected BeanWrapper createBeanWrapper(Class<?> clss) {
488 return new BeanWrapperImpl(clss);
489 }
490
491 /** Returns null if value cannot be found */
492 protected Value asValue(Session session, Object value)
493 throws RepositoryException {
494 ValueFactory valueFactory = session.getValueFactory();
495 if (value instanceof Integer)
496 return valueFactory.createValue((Integer) value);
497 else if (value instanceof Long)
498 return valueFactory.createValue((Long) value);
499 else if (value instanceof Float)
500 return valueFactory.createValue((Float) value);
501 else if (value instanceof Double)
502 return valueFactory.createValue((Double) value);
503 else if (value instanceof Boolean)
504 return valueFactory.createValue((Boolean) value);
505 else if (value instanceof Calendar)
506 return valueFactory.createValue((Calendar) value);
507 else if (value instanceof Date) {
508 Calendar cal = new GregorianCalendar();
509 cal.setTime((Date) value);
510 return valueFactory.createValue(cal);
511 } else if (value instanceof CharSequence)
512 return valueFactory.createValue(value.toString());
513 else if (value instanceof InputStream)
514 return valueFactory.createValue((InputStream) value);
515 else
516 return null;
517 }
518
519 protected Class<?> classFromProperty(Property property)
520 throws RepositoryException {
521 switch (property.getType()) {
522 case PropertyType.LONG:
523 return Long.class;
524 case PropertyType.DOUBLE:
525 return Double.class;
526 case PropertyType.STRING:
527 return String.class;
528 case PropertyType.BOOLEAN:
529 return Boolean.class;
530 case PropertyType.DATE:
531 return Calendar.class;
532 case PropertyType.NAME:
533 return null;
534 default:
535 throw new ArgeoException("Cannot find class for property "
536 + property + ", type="
537 + PropertyType.nameFromValue(property.getType()));
538 }
539 }
540
541 protected Object asObject(Value value, Class<?> propClass)
542 throws RepositoryException {
543 if (propClass.equals(Integer.class))
544 return (int) value.getLong();
545 else if (propClass.equals(Long.class))
546 return value.getLong();
547 else if (propClass.equals(Float.class))
548 return (float) value.getDouble();
549 else if (propClass.equals(Double.class))
550 return value.getDouble();
551 else if (propClass.equals(Boolean.class))
552 return value.getBoolean();
553 else if (CharSequence.class.isAssignableFrom(propClass))
554 return value.getString();
555 else if (InputStream.class.isAssignableFrom(propClass))
556 return value.getStream();
557 else if (Calendar.class.isAssignableFrom(propClass))
558 return value.getDate();
559 else if (Date.class.isAssignableFrom(propClass))
560 return value.getDate().getTime();
561 else
562 return null;
563 }
564
565 protected Node findChildReference(Session session, BeanWrapper child)
566 throws RepositoryException {
567 if (child.isReadableProperty(uuidProperty)) {
568 String childUuid = child.getPropertyValue(uuidProperty).toString();
569 try {
570 return session.getNodeByUUID(childUuid);
571 } catch (ItemNotFoundException e) {
572 if (strictUuidReference)
573 throw new ArgeoException("No node found with uuid "
574 + childUuid, e);
575 }
576 }
577 return null;
578 }
579
580 protected Class<?> loadClass(String name) {
581 // log.debug("Class loader: " + classLoader);
582 try {
583 return classLoader.loadClass(name);
584 } catch (ClassNotFoundException e) {
585 throw new ArgeoException("Cannot load class " + name, e);
586 }
587 }
588
589 protected String propertyName(String name) {
590 return name;
591 }
592
593 public void setVersioning(Boolean versioning) {
594 this.versioning = versioning;
595 }
596
597 public void setUuidProperty(String uuidProperty) {
598 this.uuidProperty = uuidProperty;
599 }
600
601 public void setClassProperty(String classProperty) {
602 this.classProperty = classProperty;
603 }
604
605 public void setStrictUuidReference(Boolean strictUuidReference) {
606 this.strictUuidReference = strictUuidReference;
607 }
608
609 public void setPrimaryNodeType(String primaryNodeType) {
610 this.primaryNodeType = primaryNodeType;
611 }
612
613 public void setClassLoader(ClassLoader classLoader) {
614 this.classLoader = classLoader;
615 }
616
617 public void setNodeMapperProvider(NodeMapperProvider nodeMapperProvider) {
618 this.nodeMapperProvider = nodeMapperProvider;
619 }
620
621 public String getPrimaryNodeType() {
622 return this.primaryNodeType;
623 }
624
625 public String getClassProperty() {
626 return this.classProperty;
627 }
628 }