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