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