2 * Copyright (C) 2007-2012 Argeo GmbH
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org
.argeo
.jcr
.spring
;
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
;
27 import java
.util
.StringTokenizer
;
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
;
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
;
50 public class BeanNodeMapper
implements NodeMapper
{
51 private final static Log log
= LogFactory
.getLog(BeanNodeMapper
.class);
53 private final static String NODE_VALUE
= "value";
55 // private String keyNode = "bean:key";
56 private String uuidProperty
= "uuid";
57 private String classProperty
= "class";
59 private Boolean versioning
= false;
60 private Boolean strictUuidReference
= false;
62 // TODO define a primaryNodeType Strategy
63 private String primaryNodeType
= null;
65 private ClassLoader classLoader
= getClass().getClassLoader();
67 private NodeMapperProvider nodeMapperProvider
;
70 * exposed method to retrieve a bean from a node
72 public Object
load(Node node
) {
74 if (nodeMapperProvider
!= null) {
75 NodeMapper nodeMapper
= nodeMapperProvider
.findNodeMapper(node
);
76 if (nodeMapper
!= this) {
77 return nodeMapper
.load(node
);
80 return nodeToBean(node
);
81 } catch (RepositoryException e
) {
82 throw new ArgeoException("Cannot load object from node " + node
, e
);
86 /** Update an existing node with an object */
87 public void update(Node node
, Object obj
) {
89 if (nodeMapperProvider
!= null) {
91 NodeMapper nodeMapper
= nodeMapperProvider
.findNodeMapper(node
);
92 if (nodeMapper
!= this) {
93 nodeMapper
.update(node
, obj
);
95 beanToNode(createBeanWrapper(obj
), node
);
97 beanToNode(createBeanWrapper(obj
), node
);
98 } catch (RepositoryException e
) {
99 throw new ArgeoException("Cannot update node " + node
+ " with "
105 * if no storage path is given; we use canonical path
107 * @see this.storagePath()
109 public Node
save(Session session
, Object obj
) {
110 return save(session
, storagePath(obj
), obj
);
114 * Create a new node to store an object. If the parentNode doesn't exist, it
117 * the primaryNodeType may be initialized before
119 public Node
save(Session session
, String path
, Object obj
) {
122 String parentPath
= JcrUtils
.parentPath(path
);
123 // find or create parent node
125 if (session
.itemExists(path
))
126 parentNode
= (Node
) session
.getItem(parentPath
);
128 parentNode
= JcrUtils
.mkdirs(session
, parentPath
, null, null,
133 if (primaryNodeType
!= null)
134 node
= parentNode
.addNode(JcrUtils
.lastPathElement(path
),
137 node
= parentNode
.addNode(JcrUtils
.lastPathElement(path
));
139 // Check specific cases
140 if (nodeMapperProvider
!= null) {
141 NodeMapper nodeMapper
= nodeMapperProvider
.findNodeMapper(node
);
142 if (nodeMapper
!= this) {
143 nodeMapper
.update(node
, obj
);
149 } catch (ArgeoException e
) {
151 } catch (Exception e
) {
152 throw new ArgeoException("Cannot save or update " + obj
+ " under "
158 * Parse the FQN of a class to string with '/' delimiters Prefix the
159 * returned string with "/objects/"
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('/');
168 buf
.append(obj
.toString());
169 return buf
.toString();
172 @SuppressWarnings("unchecked")
174 * Transforms a node into an object of the class defined by classProperty Property
176 protected Object
nodeToBean(Node node
) throws RepositoryException
{
177 if (log
.isTraceEnabled())
178 log
.trace("Load " + node
);
181 String clssName
= node
.getProperty(classProperty
).getValue()
184 BeanWrapper beanWrapper
= createBeanWrapper(loadClass(clssName
));
186 // process properties
187 PropertyIterator propIt
= node
.getProperties();
188 props
: while (propIt
.hasNext()) {
189 Property prop
= propIt
.nextProperty();
190 if (!beanWrapper
.isWritableProperty(prop
.getName()))
193 PropertyDescriptor pd
= beanWrapper
.getPropertyDescriptor(prop
195 Class
<?
> propClass
= pd
.getPropertyType();
197 if (log
.isTraceEnabled())
198 log
.trace("Load " + prop
+ ", propClass=" + propClass
199 + ", property descriptor=" + pd
);
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
));
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());
216 beanWrapper
.setPropertyValue(prop
.getName(), value
);
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
))
227 PropertyDescriptor pd
= beanWrapper
.getPropertyDescriptor(name
);
228 Class
<?
> propClass
= pd
.getPropertyType();
231 if (propClass
!= null && List
.class.isAssignableFrom(propClass
)) {
232 String lstClass
= childNode
.getProperty(classProperty
)
236 lst
= (List
<Object
>) loadClass(lstClass
).newInstance();
237 } catch (Exception e
) {
238 lst
= new ArrayList
<Object
>();
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
);
250 // look for a property with the same name which will
253 Property childProp
= childNode
.getProperty(childNode
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
));
262 lst
.add(asObject(childProp
.getValue(),
265 beanWrapper
.setPropertyValue(name
, lst
);
270 if (propClass
!= null && Map
.class.isAssignableFrom(propClass
)) {
271 String mapClass
= childNode
.getProperty(classProperty
)
273 Map
<Object
, Object
> map
;
275 map
= (Map
<Object
, Object
>) loadClass(mapClass
)
277 } catch (Exception e
) {
278 map
= new HashMap
<Object
, Object
>();
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
))
290 Class
<?
> keyPropClass
= classFromProperty(keyProp
);
291 if (keyPropClass
!= null) {
292 Object mapValue
= asObject(keyProp
.getValue(),
294 map
.put(key
, mapValue
);
299 NodeIterator keysIt
= childNode
.getNodes();
300 while (keysIt
.hasNext()) {
301 Node mapValueNode
= keysIt
.nextNode();
302 // FIXME: use property editor
303 Object key
= mapValueNode
.getName();
305 Object mapValue
= nodeToBean(mapValueNode
);
307 map
.put(key
, mapValue
);
309 beanWrapper
.setPropertyValue(name
, map
);
314 Object value
= nodeToBean(childNode
);
315 beanWrapper
.setPropertyValue(name
, value
);
318 return beanWrapper
.getWrappedInstance();
319 } catch (Exception e
) {
320 throw new ArgeoException("Cannot map node " + node
, e
);
325 * Transforms an object to the specified jcr Node in order to persist it.
329 * @throws RepositoryException
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
339 Object value
= beanWrapper
.getPropertyValue(name
);
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();
350 // if (uuidProperty != null && uuidProperty.equals(name)) {
351 // // node.addMixin(ArgeoJcrConstants.MIX_REFERENCEABLE);
352 // node.setProperty(ArgeoJcrConstants.JCR_UUID, value.toString());
353 // continue properties;
356 if ("class".equals(name
)) {
357 if (classProperty
!= null) {
358 node
.setProperty(classProperty
,
359 ((Class
<?
>) value
).getName());
360 // TODO: store a class hierarchy?
365 // Some bean reference other classes. We must deal with this case
366 if (value
instanceof Class
<?
>) {
367 node
.setProperty(name
, ((Class
<?
>) value
).getName());
371 Value val
= asValue(node
.getSession(), value
);
373 node
.setProperty(name
, val
);
377 if (value
instanceof List
<?
>) {
378 List
<?
> lst
= (List
<?
>) value
;
379 addList(node
, name
, lst
);
383 if (value
instanceof Map
<?
, ?
>) {
384 Map
<?
, ?
> map
= (Map
<?
, ?
>) value
;
385 addMap(node
, name
, map
);
389 BeanWrapper child
= createBeanWrapper(value
);
390 // TODO: delegate to another mapper
392 // TODO: deal with references
393 // Node childNode = findChildReference(session, child);
394 // if (childNode != null) {
395 // node.setProperty(name, childNode);
396 // continue properties;
399 // default case (recursive)
400 if (node
.hasNode(name
)) {// update
402 node
.getNode(name
).remove();
404 Node childNode
= node
.addNode(name
);
405 beanToNode(child
, childNode
);
410 * Process specific case of list
415 * @throws RepositoryException
417 protected void addList(Node node
, String name
, List
<?
> lst
)
418 throws RepositoryException
{
419 if (node
.hasNode(name
)) {// update
421 node
.getNode(name
).remove();
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;
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;
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
);
451 listNode
.setProperty(name
, values
);
456 * Process specific case of maps.
461 * @throws RepositoryException
463 protected void addMap(Node node
, String name
, Map
<?
, ?
> map
)
464 throws RepositoryException
{
465 if (node
.hasNode(name
)) {// update
467 node
.getNode(name
).remove();
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(),
478 if (key
instanceof CharSequence
)
479 keyStr
= key
.toString();
481 throw new ArgeoException(
482 "Cannot find property editor for class "
486 // keyStr = pe.getAsText();
488 // TODO: check string format
490 Value mapVal
= asValue(node
.getSession(), mapValue
);
492 mapNode
.setProperty(keyStr
, mapVal
);
494 Node entryNode
= mapNode
.addNode(keyStr
);
495 beanToNode(createBeanWrapper(mapValue
), entryNode
);
502 protected BeanWrapper
createBeanWrapper(Object obj
) {
503 return new BeanWrapperImpl(obj
);
506 protected BeanWrapper
createBeanWrapper(Class
<?
> clss
) {
507 return new BeanWrapperImpl(clss
);
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
);
540 protected Class
<?
> classFromProperty(Property property
)
541 throws RepositoryException
{
542 switch (property
.getType()) {
543 case PropertyType
.LONG
:
545 case PropertyType
.DOUBLE
:
547 case PropertyType
.STRING
:
549 case PropertyType
.BOOLEAN
:
550 return Boolean
.class;
551 case PropertyType
.DATE
:
552 return Calendar
.class;
553 case PropertyType
.NAME
:
556 throw new ArgeoException("Cannot find class for property "
557 + property
+ ", type="
558 + PropertyType
.nameFromValue(property
.getType()));
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();
586 protected Node
findChildReference(Session session
, BeanWrapper child
)
587 throws RepositoryException
{
588 if (child
.isReadableProperty(uuidProperty
)) {
589 String childUuid
= child
.getPropertyValue(uuidProperty
).toString();
591 return session
.getNodeByIdentifier(childUuid
);
592 } catch (ItemNotFoundException e
) {
593 if (strictUuidReference
)
594 throw new ArgeoException("No node found with uuid "
601 protected Class
<?
> loadClass(String name
) {
602 // log.debug("Class loader: " + classLoader);
604 return classLoader
.loadClass(name
);
605 } catch (ClassNotFoundException e
) {
606 throw new ArgeoException("Cannot load class " + name
, e
);
610 protected String
propertyName(String name
) {
614 public void setVersioning(Boolean versioning
) {
615 this.versioning
= versioning
;
618 public void setUuidProperty(String uuidProperty
) {
619 this.uuidProperty
= uuidProperty
;
622 public void setClassProperty(String classProperty
) {
623 this.classProperty
= classProperty
;
626 public void setStrictUuidReference(Boolean strictUuidReference
) {
627 this.strictUuidReference
= strictUuidReference
;
630 public void setPrimaryNodeType(String primaryNodeType
) {
631 this.primaryNodeType
= primaryNodeType
;
634 public void setClassLoader(ClassLoader classLoader
) {
635 this.classLoader
= classLoader
;
638 public void setNodeMapperProvider(NodeMapperProvider nodeMapperProvider
) {
639 this.nodeMapperProvider
= nodeMapperProvider
;
642 public String
getPrimaryNodeType() {
643 return this.primaryNodeType
;
646 public String
getClassProperty() {
647 return this.classProperty
;