/**
* This file Copyright (c) 2012 Magnolia International
* Ltd. (http://www.magnolia-cms.com). All rights reserved.
*
*
* This file is dual-licensed under both the Magnolia
* Network Agreement and the GNU General Public License.
* You may elect to use one or the other of these licenses.
*
* This file is distributed in the hope that it will be
* useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
* Redistribution, except as permitted by whichever of the GPL
* or MNA you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or
* modify this file under the terms of the GNU General
* Public License, Version 3, as published by the Free Software
* Foundation. You should have received a copy of the GNU
* General Public License, Version 3 along with this program;
* if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 2. For the Magnolia Network Agreement (MNA), this file
* and the accompanying materials are made available under the
* terms of the MNA which accompanies this distribution, and
* is available at http://www.magnolia-cms.com/mna.html
*
* Any modifications to this file must keep this entire header
* intact.
*
*/
package info.magnolia.jcr.node2bean.impl;
import info.magnolia.cms.util.ContentUtil;
import info.magnolia.cms.util.SystemContentWrapper;
import info.magnolia.jcr.iterator.FilteringNodeIterator;
import info.magnolia.jcr.node2bean.Node2BeanException;
import info.magnolia.jcr.node2bean.Node2BeanTransformer;
import info.magnolia.jcr.node2bean.PropertyTypeDescriptor;
import info.magnolia.jcr.node2bean.TransformationState;
import info.magnolia.jcr.node2bean.TypeDescriptor;
import info.magnolia.jcr.node2bean.TypeMapping;
import info.magnolia.jcr.predicate.AbstractPredicate;
import info.magnolia.jcr.util.NodeTypes;
import info.magnolia.objectfactory.Classes;
import info.magnolia.objectfactory.ComponentProvider;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.MethodUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang.LocaleUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Iterables;
/**
* Concrete implementation using reflection, generics and setter methods.
*/
public class Node2BeanTransformerImpl implements Node2BeanTransformer {
private static final Logger log = LoggerFactory.getLogger(Node2BeanTransformerImpl.class);
private final BeanUtilsBean beanUtilsBean;
private final Class<?> defaultListImpl;
private final Class<?> defaultSetImpl;
private final Class<?> defaultQueueImpl;
@Inject
public Node2BeanTransformerImpl() {
this(LinkedList.class, HashSet.class, LinkedList.class);
}
public Node2BeanTransformerImpl(Class<?> defaultListImpl, Class<?> defaultSetImpl, Class<?> defaultQueueImpl) {
this.defaultListImpl = defaultListImpl;
this.defaultSetImpl = defaultSetImpl;
this.defaultQueueImpl = defaultQueueImpl;
// We use non-static BeanUtils conversion, so we can
// * use our custom ConvertUtilsBean
// * control converters (convertUtilsBean.register()) - we can register them here, locally, as opposed to a
// global ConvertUtils.register()
final EnumAwareConvertUtilsBean convertUtilsBean = new EnumAwareConvertUtilsBean();
// de-register the converter for Class, we do our own conversion in convertPropertyValue()
convertUtilsBean.deregister(Class.class);
convertUtilsBean.register(new Converter() {
@Override
public Object convert(Class type, Object value) {
return new MessageFormat((String) value);
}
}, MessageFormat.class);
convertUtilsBean.register(new Converter() {
@Override
public Object convert(Class type, Object value) {
return Pattern.compile((String) value);
}
}, Pattern.class);
this.beanUtilsBean = new BeanUtilsBean(convertUtilsBean, new PropertyUtilsBean());
}
@Override
public TransformationState newState() {
return new TransformationStateImpl();
}
@Override
public TypeDescriptor resolveType(TypeMapping typeMapping, TransformationState state, ComponentProvider componentProvider) throws ClassNotFoundException, RepositoryException {
TypeDescriptor typeDscr = null;
Node node = state.getCurrentNode();
try {
if (node.hasProperty("class")) {
String className = node.getProperty("class").getString();
if (StringUtils.isBlank(className)) {
log.warn("Cannot resolve type for node [" + node + "] because class property has empty value.");
} else {
Class<?> clazz = Classes.getClassFactory().forName(className);
typeDscr = typeMapping.getTypeDescriptor(clazz);
}
}
} catch (RepositoryException e) {
log.warn("Can't read class property from node [{}]", node.getPath(), e);
}
if (typeDscr == null && state.getLevel() > 1) {
TypeDescriptor parentTypeDscr = state.getCurrentType();
PropertyTypeDescriptor propDscr;
if (parentTypeDscr.isMap() || parentTypeDscr.isCollection()) {
if (state.getLevel() > 2) {
// this is not necessarily the parent node of the current
String mapProperyName = state.peekNode(1).getName();
propDscr = state.peekType(1).getPropertyTypeDescriptor(mapProperyName, typeMapping);
if (propDscr != null) {
typeDscr = propDscr.getCollectionEntryType();
}
}
} else {
propDscr = state.getCurrentType().getPropertyTypeDescriptor(node.getName(), typeMapping);
if (propDscr != null) {
typeDscr = propDscr.getType();
}
}
}
typeDscr = onResolveType(typeMapping, state, typeDscr, componentProvider);
if (typeDscr != null) {
// might be that the factory util defines a default implementation for interfaces
final Class<?> type = typeDscr.getType();
typeDscr = typeMapping.getTypeDescriptor(componentProvider.getImplementation(type));
// now that we know the property type we should delegate to the custom transformer if any defined
Node2BeanTransformer customTransformer = typeDscr.getTransformer();
if (customTransformer != null && customTransformer != this) {
TypeDescriptor typeFoundByCustomTransformer = customTransformer.resolveType(typeMapping, state, componentProvider);
// if no specific type has been provided by the
// TODO - TypeDescriptor - equals and hashCode impl and use
// not equals instead of !=
if (typeFoundByCustomTransformer != TypeMapping.MAP_TYPE) {
// might be that the factory util defines a default implementation for interfaces
Class<?> implementation = componentProvider.getImplementation(typeFoundByCustomTransformer.getType());
typeDscr = typeMapping.getTypeDescriptor(implementation);
}
}
}
if (typeDscr == null || typeDscr.needsDefaultMapping()) {
if (typeDscr == null) {
log.debug("Was not able to resolve type for node [{}] will use a map", node);
}
typeDscr = TypeMapping.MAP_TYPE;
}
log.debug("Resolved type [{}] for node [{}]", typeDscr.getType(), node.getPath());
return typeDscr;
}
@Override
public NodeIterator getChildren(Node node) throws RepositoryException {
// TODO create predicate into separate class, <? extends Item> ItemHidingPredicate (regexp)
return new FilteringNodeIterator(node.getNodes(), new AbstractPredicate<Node>() {
@Override
public boolean evaluateTyped(Node t) {
try {
return !(t.getName().startsWith(NodeTypes.JCR_PREFIX) ||
t.getName().startsWith(NodeTypes.MGNL_PREFIX) ||
t.isNodeType(NodeTypes.MetaData.NAME));
} catch (RepositoryException e) {
return false;
}
}
});
}
@Override
public Object newBeanInstance(TransformationState state, Map<String, Object> values, ComponentProvider componentProvider) throws Node2BeanException {
// we try first to use conversion (Map --> primitive type)
// this is the case when we flattening the hierarchy?
final Object bean = convertPropertyValue(state.getCurrentType().getType(), values);
// were the properties transformed?
if (bean == values) {
try {
// is this property remove necessary?
values.remove("class");
final Class<?> type = state.getCurrentType().getType();
if (LinkedHashMap.class.equals(type)) {
// TODO - as far as I can tell, "bean" and "properties" are already the same instance of a
// LinkedHashMap, so what are we doing in here ?
return new LinkedHashMap();
} else if (Map.class.isAssignableFrom(type)) {
// TODO ?
log.warn("someone wants another type of map ? " + type);
} else if (Collection.class.isAssignableFrom(type)) {
// someone wants specific collection
return type.newInstance();
}
return componentProvider.newInstance(type);
} catch (Throwable e) {
throw new Node2BeanException(e);
}
}
return bean;
}
@Override
public void initBean(TransformationState state, Map values) throws Node2BeanException {
Object bean = state.getCurrentBean();
Method init;
try {
init = bean.getClass().getMethod("init", new Class[] {});
try {
init.invoke(bean); // no parameters
} catch (Exception e) {
throw new Node2BeanException("can't call init method", e);
}
} catch (SecurityException e) {
return;
} catch (NoSuchMethodException e) {
return;
}
log.debug("{} is initialized", bean);
}
@Override
public Object convertPropertyValue(Class<?> propertyType, Object value) throws Node2BeanException {
if (Class.class.equals(propertyType)) {
try {
return Classes.getClassFactory().forName(value.toString());
} catch (ClassNotFoundException e) {
log.error("Can't convert property. Class for type [{}] not found.", propertyType);
throw new Node2BeanException(e);
}
}
if (Locale.class.equals(propertyType)) {
if (value instanceof String) {
String localeStr = (String) value;
if (StringUtils.isNotEmpty(localeStr)) {
return LocaleUtils.toLocale(localeStr);
}
}
}
if (Collection.class.equals(propertyType) && value instanceof Map) {
// TODO never used ?
return ((Map) value).values();
}
// this is mainly the case when we are flattening node hierarchies
if (String.class.equals(propertyType) && value instanceof Map && ((Map) value).size() == 1) {
return ((Map) value).values().iterator().next();
}
return value;
}
/**
* Called once the type should have been resolved. The resolvedType might be
* null if no type has been resolved. Every subclass should override this method.
*/
protected TypeDescriptor onResolveType(TypeMapping typeMapping, TransformationState state, TypeDescriptor resolvedType, ComponentProvider componentProvider) {
return resolvedType;
}
@Override
public void setProperty(TypeMapping mapping, TransformationState state, PropertyTypeDescriptor descriptor, Map<String, Object> values) throws RepositoryException {
String propertyName = descriptor.getName();
if (propertyName.equals("class")) {
return;
}
Object value = values.get(propertyName);
Object bean = state.getCurrentBean();
if (propertyName.equals("content") && value == null) {
// TODO this should be changed to node but this would require to
// rewrite some classes to use node instead of content
value = new SystemContentWrapper(ContentUtil.asContent(state.getCurrentNode()));
} else if (propertyName.equals("name") && value == null) {
value = state.getCurrentNode().getName();
} else if (propertyName.equals("className") && value == null) {
value = values.get("class");
}
// do no try to set a bean-property that has no corresponding node-property
if (value == null) {
return;
}
log.debug("try to set {}.{} with value {}", new Object[] { bean, propertyName, value });
// if the parent bean is a map, we can't guess the types.
if (!(bean instanceof Map)) {
try {
PropertyTypeDescriptor dscr = mapping.getPropertyTypeDescriptor(bean.getClass(), propertyName);
if (dscr.getType() != null) {
// try to use a setter method for a Collection property of
// the bean
if (dscr.isCollection() || dscr.isMap() || dscr.isArray()) {
log.debug("{} is of type collection, map or array", propertyName);
if (dscr.getWriteMethod() != null) {
Method method = dscr.getWriteMethod();
clearCollection(bean, propertyName);
filterOutWrongValues(dscr, value);
if (dscr.isMap()) {
method.invoke(bean, value);
} else if (dscr.isArray()) {
Class<?> entryClass = dscr.getCollectionEntryType().getType();
Collection<Object> list = new LinkedList<Object>(((Map<Object, Object>) value).values());
Object[] arr = (Object[]) Array.newInstance(entryClass, list.size());
for (int i = 0; i < arr.length; i++) {
arr[i] = Iterables.get(list, i);
}
method.invoke(bean, new Object[] { arr });
} else if (dscr.isCollection()) {
if (value instanceof Map) {
value = createCollectionFromMap((Map<Object, Object>) value, dscr.getType().getType());
}
method.invoke(bean, value);
}
return;
} else if (dscr.getAddMethod() != null) {
Method method = dscr.getAddMethod();
clearCollection(bean, propertyName);
Class<?> entryClass = dscr.getCollectionEntryType().getType();
log.warn("Will use deprecated add method [" + method.getName() + "] to populate [" + propertyName + "] in bean class [" + bean.getClass().getName() + "].");
for (Iterator<Object> iter = ((Map<Object, Object>) value).keySet().iterator(); iter.hasNext();) {
Object key = iter.next();
Object entryValue = ((Map<Object, Object>) value).get(key);
entryValue = convertPropertyValue(entryClass, entryValue);
if (entryClass.isAssignableFrom(entryValue.getClass())) {
if (dscr.isCollection() || dscr.isArray()) {
log.debug("will add value {}", entryValue);
method.invoke(bean, new Object[] { entryValue });
}
// is a map
else {
log.debug("will add key {} with value {}", key, entryValue);
method.invoke(bean, new Object[] { key, entryValue });
}
}
}
return;
}
if (dscr.isCollection()) {
log.debug("transform the values to a collection", propertyName);
value = ((Map<Object, Object>) value).values();
}
} else {
value = convertPropertyValue(dscr.getType().getType(), value);
}
}
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Can't set property [{}] to value [{}] in bean [{}] for node {} due to {}",
new Object[] { propertyName, value, bean.getClass().getName(),
state.getCurrentNode().getPath(), e.toString() });
} else {
log.error("Can't set property [{}] to value [{}] in bean [{}] for node {} due to {}",
new Object[] { propertyName, value, bean.getClass().getName(),
state.getCurrentNode().getPath(), e.toString() });
}
}
}
try {
// This uses the converters registered in beanUtilsBean.convertUtilsBean (see constructor of this class)
// If a converter is registered, beanutils will convert value.toString(), not the value object as-is.
// If no converter is registered, then the value Object is set as-is.
// If convertPropertyValue() already converted this value, you'll probably want to unregister the beanutils
// converter.
// some conversions like string to class. Performance of PropertyUtils.setProperty() would be better
beanUtilsBean.setProperty(bean, propertyName, value);
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Can't set property [{}] to value [{}] in bean [{}] for node {} due to {}",
new Object[] { propertyName, value, bean.getClass().getName(),
state.getCurrentNode().getPath(), e.toString() });
} else {
log.error("Can't set property [{}] to value [{}] in bean [{}] for node {} due to {}",
new Object[] { propertyName, value, bean.getClass().getName(),
state.getCurrentNode().getPath(), e.toString() });
}
}
}
private void filterOutWrongValues(PropertyTypeDescriptor dscr, Object value) {
if (dscr.getCollectionEntryType() != null) {
Iterator<?> it = null;
Class<?> entryClass = dscr.getCollectionEntryType().getType();
if (dscr.getType().isCollection() && value instanceof Collection) {
it = ((Collection) value).iterator();
}
if (value instanceof Map) {
it = ((Map<Object, Object>) value).values().iterator();
}
if (it != null) {
while (it.hasNext()) {
Object obj = it.next();
if (!entryClass.isAssignableFrom(obj.getClass())) {
it.remove();
}
}
}
}
}
/**
* @param bean
* @param propertyName
*/
private void clearCollection(Object bean, String propertyName) {
log.debug("clearing the current content of the collection/map");
try {
Object col = PropertyUtils.getProperty(bean, propertyName);
if (col != null) {
MethodUtils.invokeExactMethod(col, "clear", new Object[] {});
}
} catch (Exception e) {
log.debug("no clear method found on collection {}", propertyName);
}
}
/**
* Creates collection from map. Collection type depends on passed class parameter. If passed class parameter is
* interface, then default implementation will be used for creating collection.<br/>
* By default
* <ul>
* <li>{@link LinkedList} is used for creating List and Queue collections.</li>
* <li>{@link HashSet} is used for creating Set collection.</li>
* </ul>
* If passed class parameter is an implementation of any collection type, then this method will create
* this implementation and returns it.
*
* @param map a map which values will be converted to a collection
* @param clazz collection type
* @return Collection of elements or null.
*/
protected Collection<?> createCollectionFromMap(Map<?, ?> map, Class<?> clazz) throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
Collection<?> collection = null;
Constructor<?> constructor = null;
if (clazz.isInterface()) {
// class is an interface, we need to decide which implementation of interface we will use
if (List.class.isAssignableFrom(clazz)) {
constructor = defaultListImpl.getConstructor(Collection.class);
} else if (clazz.isAssignableFrom(Queue.class)) {
constructor = defaultQueueImpl.getConstructor(Collection.class);
} else if (Set.class.isAssignableFrom(clazz)) {
constructor = defaultSetImpl.getConstructor(Collection.class);
}
} else {
if (Collection.class.isAssignableFrom(clazz)) {
constructor = clazz.getConstructor(Collection.class);
}
}
if (constructor != null) {
collection = (Collection<?>) constructor.newInstance(map.values());
}
return collection;
}
}