View Javadoc

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