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