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