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