info.magnolia.jcr.node2bean.impl.Node2BeanTransformerImpl.java Source code

Java tutorial

Introduction

Here is the source code for info.magnolia.jcr.node2bean.impl.Node2BeanTransformerImpl.java

Source

/**
 * 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;
    }

}