View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.content2bean.impl;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.util.ContentUtil;
38  import info.magnolia.cms.util.SystemContentWrapper;
39  import info.magnolia.content2bean.Content2BeanException;
40  import info.magnolia.content2bean.Content2BeanTransformer;
41  import info.magnolia.content2bean.PropertyTypeDescriptor;
42  import info.magnolia.content2bean.TransformationState;
43  import info.magnolia.content2bean.TypeDescriptor;
44  import info.magnolia.content2bean.TypeMapping;
45  import info.magnolia.objectfactory.Classes;
46  import info.magnolia.objectfactory.ComponentProvider;
47  
48  import java.lang.reflect.Method;
49  import java.util.Collection;
50  import java.util.Iterator;
51  import java.util.LinkedHashMap;
52  import java.util.Locale;
53  import java.util.Map;
54  
55  import javax.inject.Inject;
56  import javax.inject.Singleton;
57  import javax.jcr.RepositoryException;
58  
59  import org.apache.commons.beanutils.BeanUtilsBean;
60  import org.apache.commons.beanutils.MethodUtils;
61  import org.apache.commons.beanutils.PropertyUtils;
62  import org.apache.commons.beanutils.PropertyUtilsBean;
63  import org.apache.commons.lang.LocaleUtils;
64  import org.apache.commons.lang.StringUtils;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  /**
69   * Concrete implementation using reflection and adder methods.
70   * 
71   * @author philipp
72   * @version $Id$
73   */
74  @Singleton
75  public class Content2BeanTransformerImpl implements Content2BeanTransformer, Content.ContentFilter {
76  
77      private static final Logger log = LoggerFactory.getLogger(Content2BeanTransformerImpl.class);
78  
79      private final BeanUtilsBean beanUtilsBean;
80  
81      /**
82       * @deprecated should not be needed since we pass it around now... or will we ? ... TODO MAGNOLIA-3525
83       */
84      @Inject
85      private TypeMapping typeMapping;
86  
87      public Content2BeanTransformerImpl() {
88          super();
89  
90          // We use non-static BeanUtils conversion, so we can
91          // * use our custom ConvertUtilsBean
92          // * control converters (convertUtilsBean.register()) - we can register them here, locally, as opposed to a
93          // global ConvertUtils.register()
94          final EnumAwareConvertUtilsBean convertUtilsBean = new EnumAwareConvertUtilsBean();
95  
96          // de-register the converter for Class, we do our own conversion in convertPropertyValue()
97          convertUtilsBean.deregister(Class.class);
98  
99          this.beanUtilsBean = new BeanUtilsBean(convertUtilsBean, new PropertyUtilsBean());
100     }
101 
102     @Override
103     @Deprecated
104     public TypeDescriptor resolveType(TransformationState state) throws ClassNotFoundException {
105         throw new UnsupportedOperationException();
106     }
107 
108     /**
109      * Resolves the <code>TypeDescriptor</code> from current transformation state. Resolving happens in the following
110      * order:
111      * <ul>
112      * <li>checks the class property of the current node
113      * <li>calls onResolve subclasses should override
114      * <li>reflection on the parent bean
115      * <li>in case of a collection/map type call getClassForCollectionProperty
116      * <li>otherwise use a Map
117      * </ul>
118      */
119     @Override
120     public TypeDescriptor resolveType(TypeMapping typeMapping, TransformationState state, ComponentProvider componentProvider) throws ClassNotFoundException {
121         TypeDescriptor typeDscr = null;
122         Content node = state.getCurrentContent();
123 
124         try {
125             if (node.hasNodeData("class")) {
126                 String className = node.getNodeData("class").getString();
127                 if (StringUtils.isBlank(className)) {
128                     throw new ClassNotFoundException("(no value for class property)");
129                 }
130                 Class<?> clazz = Classes.getClassFactory().forName(className);
131                 typeDscr = typeMapping.getTypeDescriptor(clazz);
132             }
133         } catch (RepositoryException e) {
134             // ignore
135             log.warn("can't read class property", e);
136         }
137 
138         if (typeDscr == null && state.getLevel() > 1) {
139             TypeDescriptor parentTypeDscr = state.getCurrentType();
140             PropertyTypeDescriptor propDscr;
141 
142             if (parentTypeDscr.isMap() || parentTypeDscr.isCollection()) {
143                 if (state.getLevel() > 2) {
144                     // this is not necessarily the parent node of the current
145                     String mapProperyName = state.peekContent(1).getName();
146                     propDscr = state.peekType(1).getPropertyTypeDescriptor(mapProperyName, typeMapping);
147                     if (propDscr != null) {
148                         typeDscr = propDscr.getCollectionEntryType();
149                     }
150                 }
151             } else {
152                 propDscr = state.getCurrentType().getPropertyTypeDescriptor(node.getName(), typeMapping);
153                 if (propDscr != null) {
154                     typeDscr = propDscr.getType();
155                 }
156             }
157         }
158 
159         typeDscr = onResolveType(typeMapping, state, typeDscr, componentProvider);
160 
161         if (typeDscr != null) {
162             // might be that the factory util defines a default implementation for interfaces
163             final Class<?> type = typeDscr.getType();
164             typeDscr = typeMapping.getTypeDescriptor(componentProvider.getImplementation(type));
165 
166             // now that we know the property type we should delegate to the custom transformer if any defined
167             Content2BeanTransformer customTransformer = typeDscr.getTransformer();
168             if (customTransformer != null && customTransformer != this) {
169                 TypeDescriptor typeFoundByCustomTransformer = customTransformer.resolveType(typeMapping, state, componentProvider);
170                 // if no specific type has been provided by the
171                 // TODO - is this comparison working ?
172                 if (typeFoundByCustomTransformer != TypeMapping.MAP_TYPE) {
173                     // might be that the factory util defines a default implementation for interfaces
174                     Class<?> implementation = componentProvider.getImplementation(typeFoundByCustomTransformer.getType());
175                     typeDscr = typeMapping.getTypeDescriptor(implementation);
176                 }
177             }
178         }
179 
180         if (typeDscr == null || typeDscr.needsDefaultMapping()) {
181             if (typeDscr == null) {
182                 log.debug("was not able to resolve type for node [{}] will use a map", node);
183             }
184             typeDscr = TypeMapping.MAP_TYPE;
185         }
186 
187         log.debug("{} --> {}", node.getHandle(), typeDscr.getType());
188 
189         return typeDscr;
190     }
191 
192     /**
193      * Called once the type should have been resolved. The resolvedType might be null if no type has been resolved.
194      * After the call the FactoryUtil and custom transformers are used to get the final type. TODO - check javadoc
195      */
196     protected TypeDescriptor onResolveType(TypeMapping typeMapping, TransformationState state, TypeDescriptor resolvedType, ComponentProvider componentProvider) {
197         return resolvedType;
198     }
199 
200     /**
201      * @deprecated since 4.5, use {@link #onResolveType(info.magnolia.content2bean.TypeMapping, info.magnolia.content2bean.TransformationState, info.magnolia.content2bean.TypeDescriptor, info.magnolia.objectfactory.ComponentProvider)}
202      */
203     protected TypeDescriptor onResolveType(TransformationState state, TypeDescriptor resolvedType, ComponentProvider componentProvider) {
204         return onResolveType(getTypeMapping(), state, resolvedType, componentProvider);
205     }
206 
207     @Override
208     public Collection<Content> getChildren(Content node) {
209         return node.getChildren(this);
210     }
211 
212     /**
213      * Process all nodes except MetaData and nodes with names prefixed by "jcr:".
214      */
215     @Override
216     public boolean accept(Content content) {
217         return ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER.accept(content);
218     }
219 
220     @Override
221     public void setProperty(TransformationState state, PropertyTypeDescriptor descriptor, Map<String, Object> values) {
222         throw new UnsupportedOperationException();
223     }
224 
225     /**
226      * Do not set class property. In case of a map/collection try to use adder method.
227      */
228     @Override
229     public void setProperty(TypeMapping mapping, TransformationState state, PropertyTypeDescriptor descriptor, Map<String, Object> values) {
230         String propertyName = descriptor.getName();
231         if (propertyName.equals("class")) {
232             return;
233         }
234         Object value = values.get(propertyName);
235         Object bean = state.getCurrentBean();
236 
237         if (propertyName.equals("content") && value == null) {
238             value = new SystemContentWrapper(state.getCurrentContent());
239         } else if (propertyName.equals("name") && value == null) {
240             value = state.getCurrentContent().getName();
241         } else if (propertyName.equals("className") && value == null) {
242             value = values.get("class");
243         }
244 
245         // do no try to set a bean-property that has no corresponding node-property
246         // else if (!values.containsKey(propertyName)) {
247         if (value == null) {
248             return;
249         }
250 
251         log.debug("try to set {}.{} with value {}", new Object[] { bean, propertyName, value });
252 
253         // if the parent bean is a map, we can't guess the types.
254         if (!(bean instanceof Map)) {
255             try {
256                 PropertyTypeDescriptor dscr = mapping.getPropertyTypeDescriptor(bean.getClass(), propertyName);
257                 if (dscr.getType() != null) {
258 
259                     // try to use an adder method for a Collection property of the bean
260                     if (dscr.isCollection() || dscr.isMap()) {
261                         log.debug("{} is of type collection, map or /array", propertyName);
262                         Method method = dscr.getAddMethod();
263 
264                         if (method != null) {
265                             log.debug("clearing the current content of the collection/map");
266                             try {
267                                 Object col = PropertyUtils.getProperty(bean, propertyName);
268                                 if (col != null) {
269                                     MethodUtils.invokeExactMethod(col, "clear", new Object[] {});
270                                 }
271                             } catch (Exception e) {
272                                 log.debug("no clear method found on collection {}", propertyName);
273                             }
274 
275                             Class<?> entryClass = dscr.getCollectionEntryType().getType();
276 
277                             log.debug("will add values by using adder method {}", method.getName());
278                             for (Iterator<Object> iter = ((Map<Object, Object>) value).keySet().iterator(); iter
279                                     .hasNext();) {
280                                 Object key = iter.next();
281                                 Object entryValue = ((Map<Object, Object>) value).get(key);
282                                 entryValue = convertPropertyValue(entryClass, entryValue);
283                                 if (entryClass.isAssignableFrom(entryValue.getClass())) {
284                                     if (dscr.isCollection()) {
285                                         log.debug("will add value {}", entryValue);
286                                         method.invoke(bean, new Object[] { entryValue });
287                                     }
288                                     // is a map
289                                     else {
290                                         log.debug("will add key {} with value {}", key, entryValue);
291                                         method.invoke(bean, new Object[] { key, entryValue });
292                                     }
293                                 }
294                             }
295 
296                             return;
297                         }
298                         log.debug("no add method found for property {}", propertyName);
299                         if (dscr.isCollection()) {
300                             log.debug("transform the values to a collection", propertyName);
301                             value = ((Map<Object, Object>) value).values();
302                         }
303                     } else {
304                         value = convertPropertyValue(dscr.getType().getType(), value);
305                     }
306                 }
307             } catch (Exception e) {
308                 // do it better
309                 log.error("Can't set property [{}] to value [{}] in bean [{}] for node {} due to {}",
310                         new Object[] { propertyName, value, bean.getClass().getName(),
311                                 state.getCurrentContent().getHandle(), e.toString() });
312                 log.debug("stacktrace", e);
313             }
314         }
315 
316         try {
317             // This uses the converters registered in beanUtilsBean.convertUtilsBean (see constructor of this class)
318             // If a converter is registered, beanutils will convert value.toString(), not the value object as-is.
319             // If no converter is registered, then the value Object is set as-is.
320             // If convertPropertyValue() already converted this value, you'll probably want to unregister the beanutils
321             // converter.
322             // some conversions like string to class. Performance of PropertyUtils.setProperty() would be better
323             beanUtilsBean.setProperty(bean, propertyName, value);
324 
325             // TODO this also does things we probably don't want/need, i.e nested and indexed properties
326 
327         } catch (Exception e) {
328             // do it better
329             log.error("Can't set property [{}] to value [{}] in bean [{}] for node {} due to {}",
330                     new Object[] { propertyName, value, bean.getClass().getName(),
331                             state.getCurrentContent().getHandle(), e.toString() });
332             log.debug("stacktrace", e);
333         }
334 
335     }
336 
337     /**
338      * Most of the conversion is done by the BeanUtils. TODO don't use bean utils conversion since it can't be used for
339      * the adder methods
340      */
341     @Override
342     public Object convertPropertyValue(Class<?> propertyType, Object value) throws Content2BeanException {
343         if (Class.class.equals(propertyType)) {
344             try {
345                 return Classes.getClassFactory().forName(value.toString());
346             } catch (ClassNotFoundException e) {
347                 log.error(e.getMessage());
348                 throw new Content2BeanException(e);
349             }
350         }
351 
352         if (Locale.class.equals(propertyType)) {
353             if (value instanceof String) {
354                 String localeStr = (String) value;
355                 if (StringUtils.isNotEmpty(localeStr)) {
356                     return LocaleUtils.toLocale(localeStr);
357                 }
358             }
359         }
360 
361         if (Collection.class.equals(propertyType) && value instanceof Map) {
362             // TODO never used ?
363             return ((Map) value).values();
364         }
365 
366         // this is mainly the case when we are flattening node hierarchies
367         if (String.class.equals(propertyType) && value instanceof Map && ((Map) value).size() == 1) {
368             return ((Map) value).values().iterator().next();
369         }
370 
371         return value;
372     }
373 
374     /**
375      * Use the factory util to instantiate. This is useful to get default implementation of interfaces
376      */
377     @Override
378     public Object newBeanInstance(TransformationState state, Map properties, ComponentProvider componentProvider) throws Content2BeanException {
379         // we try first to use conversion (Map --> primitive type)
380         // this is the case when we flattening the hierarchy?
381         final Object bean = convertPropertyValue(state.getCurrentType().getType(), properties);
382         // were the properties transformed?
383         if (bean == properties) {
384             try {
385                 // TODO MAGNOLIA-2569 MAGNOLIA-3525 what is going on here ? (added the following if to avoid permanently
386                 // requesting LinkedHashMaps to ComponentFactory)
387                 final Class<?> type = state.getCurrentType().getType();
388                 if (LinkedHashMap.class.equals(type)) {
389                     // TODO - as far as I can tell, "bean" and "properties" are already the same instance of a
390                     // LinkedHashMap, so what are we doing in here ?
391                     return new LinkedHashMap();
392                 } else if (Map.class.isAssignableFrom(type)) {
393                     // TODO ?
394                     log.warn("someone wants another type of map ? " + type);
395                 }
396                 return componentProvider.newInstance(type);
397             } catch (Throwable e) {
398                 throw new Content2BeanException(e);
399             }
400         }
401         return bean;
402     }
403 
404     /**
405      * Initializes bean by calling its init method if present.
406      */
407     @Override
408     public void initBean(TransformationState state, Map properties) throws Content2BeanException {
409         Object bean = state.getCurrentBean();
410 
411         Method init;
412         try {
413             init = bean.getClass().getMethod("init", new Class[] {});
414             try {
415                 init.invoke(bean); // no parameters
416             } catch (Exception e) {
417                 throw new Content2BeanException("can't call init method", e);
418             }
419         } catch (SecurityException e) {
420             return;
421         } catch (NoSuchMethodException e) {
422             return;
423         }
424         log.debug("{} is initialized", bean);
425     }
426 
427     @Override
428     public TransformationState newState() {
429         return new TransformationStateImpl();
430         // TODO - do we really need different impls for TransformationState ?
431         // if so, this was defined in mgnl-beans.properties
432         // Components.getComponentProvider().newInstance(TransformationState.class);
433     }
434 
435     /**
436      * Returns the default mapping.
437      * 
438      * @deprecated since 4.5, do not use.
439      */
440     @Override
441     public TypeMapping getTypeMapping() {
442         return typeMapping;// TypeMapping.Factory.getDefaultMapping();
443     }
444 
445 }