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