1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.jcr.node2bean.impl;
35
36 import static info.magnolia.transformer.TransformationProblem.error;
37
38 import info.magnolia.cms.util.ContentUtil;
39 import info.magnolia.cms.util.SystemContentWrapper;
40 import info.magnolia.jcr.iterator.FilteringNodeIterator;
41 import info.magnolia.jcr.node2bean.Node2BeanException;
42 import info.magnolia.jcr.node2bean.Node2BeanTransformer;
43 import info.magnolia.jcr.node2bean.PropertyTypeDescriptor;
44 import info.magnolia.jcr.node2bean.TransformationState;
45 import info.magnolia.jcr.node2bean.TypeDescriptor;
46 import info.magnolia.jcr.node2bean.TypeMapping;
47 import info.magnolia.jcr.predicate.AbstractPredicate;
48 import info.magnolia.jcr.util.NodeTypes;
49 import info.magnolia.objectfactory.Classes;
50 import info.magnolia.objectfactory.ComponentProvider;
51 import info.magnolia.objectfactory.Components;
52
53 import java.lang.reflect.Array;
54 import java.lang.reflect.Constructor;
55 import java.lang.reflect.InvocationTargetException;
56 import java.lang.reflect.Method;
57 import java.util.ArrayList;
58 import java.util.Collection;
59 import java.util.HashSet;
60 import java.util.Iterator;
61 import java.util.LinkedHashMap;
62 import java.util.LinkedList;
63 import java.util.List;
64 import java.util.Locale;
65 import java.util.Map;
66 import java.util.Map.Entry;
67 import java.util.Queue;
68 import java.util.Set;
69
70 import javax.inject.Inject;
71 import javax.jcr.Node;
72 import javax.jcr.NodeIterator;
73 import javax.jcr.RepositoryException;
74
75 import org.apache.commons.beanutils.BeanUtilsBean;
76 import org.apache.commons.beanutils.Converter;
77 import org.apache.commons.beanutils.MethodUtils;
78 import org.apache.commons.beanutils.PropertyUtils;
79 import org.apache.commons.lang3.LocaleUtils;
80 import org.apache.commons.lang3.StringUtils;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
83
84 import com.google.common.collect.Iterables;
85
86
87
88
89 public class Node2BeanTransformerImpl implements Node2BeanTransformer {
90
91 private static final Logger log = LoggerFactory.getLogger(Node2BeanTransformerImpl.class);
92
93 private final BeanUtilsBean beanUtilsBean;
94
95 private final Class<?> defaultListImpl;
96
97 private final Class<?> defaultSetImpl;
98
99 private final Class<?> defaultQueueImpl;
100
101
102
103
104
105 @Deprecated
106 public Node2BeanTransformerImpl() {
107 this(Components.getComponent(PreConfiguredBeanUtils.class));
108 }
109
110 @Inject
111 public Node2BeanTransformerImpl(PreConfiguredBeanUtils beanUtilsBean) {
112 this(beanUtilsBean, LinkedList.class, HashSet.class, LinkedList.class);
113 }
114
115
116
117
118
119 @Deprecated
120 public Node2BeanTransformerImpl(Class<?> defaultListImpl, Class<?> defaultSetImpl, Class<?> defaultQueueImpl) {
121 this(Components.getComponent(PreConfiguredBeanUtils.class), defaultListImpl, defaultSetImpl, defaultQueueImpl);
122 }
123
124 protected Node2BeanTransformerImpl(PreConfiguredBeanUtils beanUtilsBean, Class<?> defaultListImpl, Class<?> defaultSetImpl, Class<?> defaultQueueImpl) {
125 this.beanUtilsBean = beanUtilsBean;
126 this.defaultListImpl = defaultListImpl;
127 this.defaultSetImpl = defaultSetImpl;
128 this.defaultQueueImpl = defaultQueueImpl;
129
130 beanUtilsBean.getConvertUtils().deregister(Class.class);
131 }
132
133 @Override
134 public TransformationState newState() {
135 return new TransformationStateImpl();
136 }
137
138 @Override
139 public TypeDescriptor resolveType(TypeMapping typeMapping, TransformationState state, ComponentProvider componentProvider) throws ClassNotFoundException, RepositoryException {
140 TypeDescriptor typeDscr = null;
141 Node node = state.getCurrentNode();
142
143 try {
144 if (node.hasProperty("class")) {
145 String className = StringUtils.trim(node.getProperty("class").getString());
146 if (StringUtils.isBlank(className)) {
147 state.trackProblem(error("Cannot resolve type for node [%s] because class property has empty value", node.getPath()));
148 } else {
149 Class<?> clazz = Classes.getClassFactory().forName(className);
150 typeDscr = typeMapping.getTypeDescriptor(clazz);
151 }
152 }
153 } catch (RepositoryException e) {
154 state.trackProblem(error("Failed to read a class property value from the node [%d]", node.getPath()));
155 }
156
157 if (typeDscr == null && state.getLevel() > 1) {
158 TypeDescriptor parentTypeDscr = state.getCurrentType();
159 PropertyTypeDescriptor propDscr;
160
161 if (parentTypeDscr.isMap() || parentTypeDscr.isCollection()) {
162 if (state.getLevel() > 2) {
163
164 String mapProperyName = state.peekNode(1).getName();
165 propDscr = state.peekType(1).getPropertyTypeDescriptor(mapProperyName, typeMapping);
166 if (propDscr != null) {
167 typeDscr = propDscr.getCollectionEntryType();
168 }
169 }
170 } else {
171 propDscr = state.getCurrentType().getPropertyTypeDescriptor(node.getName(), typeMapping);
172 if (propDscr != null) {
173 typeDscr = propDscr.getType();
174 }
175 }
176 }
177
178 typeDscr = onResolveType(typeMapping, state, typeDscr, componentProvider);
179
180 if (typeDscr != null) {
181
182 final Class<?> type = typeDscr.getType();
183 typeDscr = typeMapping.getTypeDescriptor(componentProvider.getImplementation(type));
184
185
186 Node2BeanTransformer customTransformer = typeDscr.getTransformer();
187 if (customTransformer != null && customTransformer != this) {
188 TypeDescriptor typeFoundByCustomTransformer = customTransformer.resolveType(typeMapping, state, componentProvider);
189
190
191
192 if (typeFoundByCustomTransformer != TypeMapping.MAP_TYPE) {
193
194 Class<?> implementation = componentProvider.getImplementation(typeFoundByCustomTransformer.getType());
195 typeDscr = typeMapping.getTypeDescriptor(implementation);
196 }
197 }
198 }
199
200 if (typeDscr == null || typeDscr.needsDefaultMapping()) {
201 if (typeDscr == null) {
202 log.debug("Was not able to resolve type for node [{}] will use a map", node);
203 }
204 typeDscr = TypeMapping.MAP_TYPE;
205 }
206 log.debug("Resolved type [{}] for node [{}]", typeDscr.getType(), node.getPath());
207
208 return typeDscr;
209 }
210
211 @Override
212 public NodeIterator getChildren(Node node) throws RepositoryException {
213
214 return new FilteringNodeIterator(node.getNodes(), new AbstractPredicate<Node>() {
215 @Override
216 public boolean evaluateTyped(Node t) {
217 try {
218 return !(t.getName().startsWith(NodeTypes.JCR_PREFIX) || t.isNodeType(NodeTypes.MetaData.NAME));
219 } catch (RepositoryException e) {
220 return false;
221 }
222 }
223 });
224 }
225
226 @Override
227 public Object newBeanInstance(TransformationState state, Map<String, Object> values, ComponentProvider componentProvider) throws Node2BeanException {
228
229
230 final Object bean = convertPropertyValue(state.getCurrentType().getType(), values);
231
232 if (bean == values) {
233 try {
234
235 values.remove("class");
236 final Class<?> type = state.getCurrentType().getType();
237 if (LinkedHashMap.class.equals(type)) {
238
239
240 return new LinkedHashMap();
241 } else if (Map.class.isAssignableFrom(type)) {
242
243 log.warn("someone wants another type of map ? {}", type);
244 } else if (Collection.class.isAssignableFrom(type)) {
245
246 return type.newInstance();
247 }
248 return componentProvider.newInstance(type);
249 } catch (Exception e) {
250
251
252
253 throw new Node2BeanException(
254 String.format("Failed to instantiate a bean of type [%s] due to: [%s]", state.getCurrentType().getType().getName(), e.getMessage()), e);
255 }
256 }
257 return bean;
258 }
259
260 @Override
261 public void initBean(TransformationState state, Map values) throws Node2BeanException {
262 Object bean = state.getCurrentBean();
263
264 Method init;
265 try {
266 init = bean.getClass().getMethod("init");
267 try {
268 init.invoke(bean);
269 } catch (Exception e) {
270 throw new Node2BeanException("can't call init method", e);
271 }
272 } catch (SecurityException | NoSuchMethodException e) {
273 return;
274 }
275 log.debug("{} is initialized", bean);
276
277 }
278
279 @Override
280 public Object convertPropertyValue(Class<?> propertyType, Object value) throws Node2BeanException {
281 if (Class.class.equals(propertyType)) {
282 try {
283 return Classes.getClassFactory().forName(value.toString());
284 } catch (ClassNotFoundException e) {
285
286 throw new Node2BeanException(String.format("Can't convert property. Class for type [%s] not found.", propertyType), e);
287 }
288 }
289
290 if (Locale.class.equals(propertyType)) {
291 if (value instanceof String) {
292 String localeStr = (String) value;
293 if (StringUtils.isNotEmpty(localeStr)) {
294 return LocaleUtils.toLocale(localeStr);
295 }
296 }
297 }
298
299 if (Collection.class.equals(propertyType) && value instanceof Map) {
300
301 return ((Map) value).values();
302 }
303
304
305 if (String.class.equals(propertyType) && value instanceof Map && ((Map) value).size() == 1) {
306 return ((Map) value).values().iterator().next();
307 }
308
309 return value;
310 }
311
312
313
314
315
316 protected TypeDescriptor onResolveType(TypeMapping typeMapping, TransformationState state, TypeDescriptor resolvedType, ComponentProvider componentProvider) {
317 return resolvedType;
318 }
319
320 @Override
321 public void setProperty(TypeMapping mapping, TransformationState state, PropertyTypeDescriptor descriptor, Map<String, Object> values) throws RepositoryException {
322 String propertyName = descriptor.getName();
323 if (propertyName.equals("class")) {
324 return;
325 }
326 Object value = values.get(propertyName);
327 Object bean = state.getCurrentBean();
328
329 if (propertyName.equals("content") && value == null) {
330 PropertyTypeDescriptor dscr = mapping.getPropertyTypeDescriptor(bean.getClass(), propertyName);
331
332
333 if (dscr.getType().getType().isAssignableFrom(SystemContentWrapper.class)) {
334 value = new SystemContentWrapper(ContentUtil.asContent(state.getCurrentNode()));
335 } else if (dscr.getType().getType().isAssignableFrom(Node.class)) {
336 value = state.getCurrentNode();
337 }
338
339 } else if (propertyName.equals("name") && value == null) {
340 value = state.getCurrentNode().getName();
341 } else if (propertyName.equals("className") && value == null) {
342 value = values.get("class");
343 }
344
345
346 if (value == null) {
347 return;
348 }
349
350 log.debug("try to set {}.{} with value {}", bean, propertyName, value);
351
352 if (!(bean instanceof Map)) {
353 try {
354 PropertyTypeDescriptor dscr = mapping.getPropertyTypeDescriptor(bean.getClass(), propertyName);
355 if (dscr.getType() != null) {
356
357
358 if (dscr.isCollection() || dscr.isMap() || dscr.isArray()) {
359 log.debug("{} is of type collection, map or array", propertyName);
360 if (dscr.getWriteMethod() != null) {
361 Method method = dscr.getWriteMethod();
362 clearCollection(bean, propertyName);
363 filterOrConvertCollectionsAndMaps(dscr, value);
364 if (dscr.isMap()) {
365 method.invoke(bean, value);
366 } else if (dscr.isArray()) {
367 Class<?> entryClass = dscr.getCollectionEntryType().getType();
368 Collection<Object> list = new LinkedList<Object>(((Map<Object, Object>) value).values());
369
370 Object[] arr = (Object[]) Array.newInstance(entryClass, list.size());
371 for (int i = 0; i < arr.length; i++) {
372 arr[i] = Iterables.get(list, i);
373 }
374 method.invoke(bean, new Object[]{arr});
375 } else if (dscr.isCollection()) {
376 if (value instanceof Map) {
377 value = createCollectionFromMap((Map<Object, Object>) value, dscr.getType().getType());
378 }
379 method.invoke(bean, value);
380 }
381 return;
382 } else if (dscr.getAddMethod() != null) {
383 Method method = dscr.getAddMethod();
384 clearCollection(bean, propertyName);
385 Class<?> entryClass = dscr.getCollectionEntryType().getType();
386
387 log.warn("Will use deprecated add method [{}] to populate [{}] in bean class [{}].", method.getName(), propertyName, bean.getClass().getName());
388 for (Iterator<Object> iter = ((Map<Object, Object>) value).keySet().iterator(); iter.hasNext(); ) {
389 Object key = iter.next();
390 Object entryValue = ((Map<Object, Object>) value).get(key);
391 entryValue = convertPropertyValue(entryClass, entryValue);
392 if (entryClass.isAssignableFrom(entryValue.getClass())) {
393 if (dscr.isCollection() || dscr.isArray()) {
394 log.debug("will add value {}", entryValue);
395 method.invoke(bean, entryValue);
396 }
397
398 else {
399 log.debug("will add key {} with value {}", key, entryValue);
400 method.invoke(bean, key, entryValue);
401 }
402 }
403 }
404 return;
405 }
406 if (dscr.isCollection()) {
407 log.debug("transform the values to a collection", propertyName);
408 value = ((Map<Object, Object>) value).values();
409 }
410 } else {
411 value = convertPropertyValue(dscr.getType().getType(), value);
412 }
413 }
414 } catch (Exception e) {
415 handleSetPropertyException(state, propertyName, value, bean, e);
416 return;
417 }
418 }
419
420 try {
421
422
423
424
425
426
427 beanUtilsBean.setProperty(bean, propertyName, value);
428 } catch (Exception e) {
429 handleSetPropertyException(state, propertyName, value, bean, e);
430 }
431 }
432
433 @Override
434 public boolean canHandleValue(TypeMapping typeMapping, TransformationState state, PropertyTypeDescriptor descriptor, Entry<String, Object> value) {
435 return descriptor.getName().equals(value.getKey())
436 || isCollectionOfStrings(typeMapping, state, value);
437 }
438
439 private boolean isCollectionOfStrings(TypeMapping typeMapping, TransformationState state, Entry<String, Object> value) {
440 if (state.getCurrentBean() instanceof String && state.getLevel() > 2) {
441 try {
442 PropertyTypeDescriptor propertyTypeDescriptor = state.peekType(2).getPropertyTypeDescriptor(state.peekNode(1).getName(), typeMapping);
443 if (propertyTypeDescriptor != null) {
444 TypeDescriptor entryType = propertyTypeDescriptor.getCollectionEntryType();
445 if (entryType != null) {
446 return entryType.getType().isAssignableFrom(value.getValue().getClass());
447 }
448 }
449 } catch (RepositoryException e) {
450 log.error("Could not resolve property type for node [{}]", state.getCurrentNode(), e);
451 }
452 }
453 return false;
454 }
455
456 @Override
457 public boolean supportsValueClaims() {
458
459 return getClass().equals(Node2BeanTransformerImpl.class);
460 }
461
462 protected boolean isBeanEnabled(Object bean) {
463 Method method = null;
464 Object isEnabled = null;
465 try {
466 method = bean.getClass().getMethod("isEnabled");
467
468
469 if (method.getReturnType().equals(Boolean.class)) {
470 return true;
471 }
472 isEnabled = method.invoke(bean);
473 } catch (NoSuchMethodException e) {
474
475 return true;
476 } catch (IllegalArgumentException e) {
477
478 return true;
479 } catch (IllegalAccessException e) {
480 log.warn("Can't access method [{}#isEnabled]. Maybe it's private/protected?", bean.getClass());
481 return true;
482 } catch (InvocationTargetException e) {
483 log.error("An exception was thrown by [{}]#isEnabled method.", bean.getClass(), e);
484 return true;
485 }
486 return (Boolean) isEnabled;
487 }
488
489
490
491
492
493
494
495 private void filterOrConvertCollectionsAndMaps(final PropertyTypeDescriptor dscr, final Object value) {
496 if (dscr.getCollectionEntryType() != null) {
497 Class<?> entryClass = dscr.getCollectionEntryType().getType();
498 if (dscr.getType().isCollection() && value instanceof Collection) {
499 final Collection<?> collection = ((Collection) value);
500 final Collection convertedCollection = filterOrConvert(collection, entryClass);
501 collection.clear();
502 collection.addAll(convertedCollection);
503 } else if (value instanceof Map) {
504 final Map map = (Map) value;
505 final Map convertedMap = filterOrConvert(map, entryClass);
506 map.clear();
507 map.putAll(convertedMap);
508 }
509 }
510 }
511
512 private Collection filterOrConvert(final Collection collection, final Class entryClass) {
513 final Collection converted = new ArrayList();
514 final Iterator<?> it = collection.iterator();
515 while (it.hasNext()) {
516 Object obj = it.next();
517 if (isBeanEnabled(obj)) {
518 if (!entryClass.isAssignableFrom(obj.getClass())) {
519 final Converter converter = beanUtilsBean.getConvertUtils().lookup(entryClass);
520 if (converter != null) {
521 converted.add(converter.convert(entryClass, obj));
522 }
523 } else {
524 converted.add(obj);
525 }
526 }
527 }
528 return converted;
529 }
530
531 private Map filterOrConvert(final Map map, final Class entryClass) {
532 final Map converted = new LinkedHashMap();
533 for (Object key : map.keySet()) {
534 Object obj = map.get(key);
535 if (isBeanEnabled(obj)) {
536 if (!entryClass.isAssignableFrom(obj.getClass())) {
537 final Converter converter = beanUtilsBean.getConvertUtils().lookup(entryClass);
538 if (converter != null) {
539 converted.put(key, converter.convert(entryClass, obj));
540 }
541 } else {
542 converted.put(key, obj);
543 }
544 }
545 }
546 return converted;
547 }
548
549 private void clearCollection(Object bean, String propertyName) {
550 log.debug("clearing the current content of the collection/map");
551 try {
552 Object col = PropertyUtils.getProperty(bean, propertyName);
553 if (col != null) {
554 MethodUtils.invokeExactMethod(col, "clear", new Object[]{});
555 }
556 } catch (Exception e) {
557 log.debug("no clear method found on collection {}", propertyName);
558 }
559 }
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576 protected Collection<?> createCollectionFromMap(Map<?, ?> map, Class<?> clazz) throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
577 Collection<?> collection = null;
578 Constructor<?> constructor = null;
579 if (clazz.isInterface()) {
580
581 if (List.class.isAssignableFrom(clazz)) {
582 constructor = defaultListImpl.getConstructor(Collection.class);
583 } else if (clazz.isAssignableFrom(Queue.class)) {
584 constructor = defaultQueueImpl.getConstructor(Collection.class);
585 } else if (Set.class.isAssignableFrom(clazz)) {
586 constructor = defaultSetImpl.getConstructor(Collection.class);
587 }
588 } else {
589 if (Collection.class.isAssignableFrom(clazz)) {
590 constructor = clazz.getConstructor(Collection.class);
591 }
592 }
593 if (constructor != null) {
594 collection = (Collection<?>) constructor.newInstance(map.values());
595 }
596 return collection;
597 }
598
599 private void handleSetPropertyException(TransformationState state, String propertyName, Object value, Object bean, Exception e) throws RepositoryException {
600 if (e instanceof InvocationTargetException) {
601 e = (Exception) e.getCause();
602 }
603 final String errorMessage = String.format("Can't set property [%s] to value [%s] in bean [%s] for node [%s] due to: [%s]", propertyName, value, bean.getClass().getName(), state.getCurrentNode().getPath(), e.toString());
604
605 state.trackProblem(error(errorMessage).withException(e));
606 }
607
608 }