View Javadoc

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