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