org.pentaho.platform.util.beans.BeanUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.platform.util.beans.BeanUtil.java

Source

/*!
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright (c) 2002-2013 Pentaho Corporation..  All rights reserved.
 */

package org.pentaho.platform.util.beans;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.platform.util.messages.Messages;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.Map;

/**
 * Utility methods for processing Java Beans in a consistent manner across all Pentaho projects. This is not an
 * attempt to duplicate the behavior of commons-beanutils, rather, a central spot for common operations on beans so
 * we can ensure that same bean property binding functionality and logic anytime we need to work with Java Beans.
 * <p>
 * This utility is especially important in dealing with Pentaho Action beans {@link IAction}s. See
 * {@link ActionHarness} for an IAction-specific flavor of this utility.
 * 
 * @author aphillips
 * 
 * @see ActionHarness
 */
public class BeanUtil {

    private static final Log logger = LogFactory.getLog(BeanUtil.class);

    private PropertyUtilsBean propUtil = new PropertyUtilsBean();

    private BeanUtilsBean typeConvertingBeanUtil;

    protected Object bean;

    protected ValueSetErrorCallback defaultCallback;

    public void setDefaultCallback(ValueSetErrorCallback defaultCallback) {
        this.defaultCallback = defaultCallback;
    }

    /**
     * Setup a new bean util for operating on the given bean
     * 
     * @param targetBean
     *          the bean on which to operate
     */
    public BeanUtil(final Object targetBean) {
        this.bean = targetBean;
        //
        // Configure a bean util that throws exceptions during type conversion
        //
        ConvertUtilsBean convertUtil = new ConvertUtilsBean();
        convertUtil.register(true, true, 0);

        typeConvertingBeanUtil = new BeanUtilsBean(convertUtil);
        setDefaultCallback(new EagerFailingCallback());
    }

    public boolean isReadable(String propertyName) {
        return propUtil.isReadable(bean, propertyName);
    }

    public Object getValue(String propertyName)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        if (logger.isTraceEnabled()) {
            logger.trace(MessageFormat.format("getting property \"{0}\" from bean \"{1}\"", propertyName, bean)); //$NON-NLS-1$
        }
        return propUtil.getSimpleProperty(bean, propertyName);
    }

    public Class<?> getPropertyType(String propertyName)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        PropertyDescriptor desc = propUtil.getPropertyDescriptor(bean, propertyName);
        return desc.getPropertyType();
    }

    /**
     * Returns <code>true</code> if a bean property can be written to (i.e. there is an accessible and appropriately
     * typed setter method on the bean). Note: this method will check for both scalar (normal) and indexed properties
     * before returning <code>false</code>.
     * 
     * @param propertyName
     *          the name of the bean property to check for write-ability
     * @return <code>true</code> if the bean property can be written to
     */
    public boolean isWriteable(String propertyName) {
        return propUtil.isWriteable(bean, propertyName)
                || (propUtil.getResolver().isIndexed(propertyName) && propUtil.isReadable(bean, propertyName));
    }

    /**
     * Set a bean property with a given value.
     * 
     * @param propertyName
     *          the bean property to set
     * @param value
     *          the value to set on the bean. If value is an instance of {@link ValueGenerator}, then the value will
     *          be derived by calling {@link ValueGenerator#getValue(String)}
     * 
     * @throws Exception
     *           if there was a problem setting the value on the bean
     * @see ValueGenerator
     */
    public void setValue(String propertyName, Object value) throws Exception {
        setValue(propertyName, value, defaultCallback);
    }

    /**
     * Set a bean property with a given value, allowing the caller to respond to various error states that may be
     * encountered during the attempt to set the value on the bean.
     * 
     * @param propertyName
     *          the bean property to set
     * @param value
     *          the value to set on the bean. If value is an instance of {@link ValueGenerator}, then the value will
     *          be derived by calling {@link ValueGenerator#getValue(String)}
     * @param callback
     *          a structure that alerts the caller of any error states and enables the caller to fail, log, proceed,
     *          etc
     * 
     * @throws Exception
     *           if there was a problem setting the value on the bean
     * @see ValueGenerator
     * @see ValueSetErrorCallback
     */
    public void setValue(final String propertyName, Object value, ValueSetErrorCallback callback) throws Exception {
        setValue(propertyName, value, callback, new PropertyNameFormatter[0]);
    }

    /**
     * Set a bean property with a given value, allowing the caller to respond to various error states that may be
     * encountered during the attempt to set the value on the bean. This method also allows the caller to specify
     * formatters that will modify the property name to match what the bean expects as the true name of the property.
     * This can be helpful when you are trying to map parameters from a source that follows a convention that is not
     * Java Bean spec compliant.
     * 
     * @param propertyName
     *          the bean property to set
     * @param value
     *          the value to set on the bean. If value is an instance of {@link ValueGenerator}, then the value will
     *          be derived by calling {@link ValueGenerator#getValue(String)}. Note: if value is <code>null</code>,
     *          we consciously bypass the set operation altogether since it leads to indeterminate behavior, i.e. it
     *          may fail or succeed.
     * @param callback
     *          a structure that alerts the caller of any error states and enables the caller to fail, log, proceed,
     *          etc
     * @param formatters
     *          a list of objects that can be used to modify the given property name prior to performing any
     *          operations on the bean itself. This new formatted property name will be used to identify the bean
     *          property. bean lookup and value setting
     * 
     * @throws Exception
     *           when something goes wrong (controlled by the callback object)
     * @see ValueGenerator
     * @see ValueSetErrorCallback
     * @see PropertyNameFormatter
     */
    public void setValue(String propertyName, Object value, PropertyNameFormatter... formatters) throws Exception {
        setValue(propertyName, value, defaultCallback, formatters);
    }

    /**
     * Set a bean property with a given value, allowing the caller to respond to various error states that may be
     * encountered during the attempt to set the value on the bean. This method also allows the caller to specify
     * formatters that will modify the property name to match what the bean expects as the true name of the property.
     * This can be helpful when you are trying to map parameters from a source that follows a convention that is not
     * Java Bean spec compliant.
     * 
     * @param propertyName
     *          the bean property to set
     * @param value
     *          the value to set on the bean. If value is an instance of {@link ValueGenerator}, then the value will
     *          be derived by calling {@link ValueGenerator#getValue(String)}. Note: if value is <code>null</code>,
     *          we consciously bypass the set operation altogether since it leads to indeterminate behavior, i.e. it
     *          may fail or succeed.
     * @param callback
     *          a structure that alerts the caller of any error states and enables the caller to fail, log, proceed,
     *          etc
     * @param formatters
     *          a list of objects that can be used to modify the given property name prior to performing any
     *          operations on the bean itself. This new formatted property name will be used to identify the bean
     *          property. bean lookup and value setting
     * 
     * @throws Exception
     *           when something goes wrong (controlled by the callback object)
     * @see ValueGenerator
     * @see ValueSetErrorCallback
     * @see PropertyNameFormatter
     */
    public void setValue(String propertyName, Object value, ValueSetErrorCallback callback,
            PropertyNameFormatter... formatters) throws Exception {
        if (logger.isTraceEnabled()) {
            logger.trace(MessageFormat.format("setting property \"{0}\" on bean \"{1}\"", propertyName, bean)); //$NON-NLS-1$
        }

        if (value == null) {
            // we are ignoring (not setting) null values because we could wind up with a class cast / converter
            // exception downstream which would imply an error condition, when an error is typically not what
            // we want here. If we did let this null continue on, we would have indeterminate behavior for nulls,
            // i.e. sometimes they would work, sometimes they would trigger an error
            logger.info(MessageFormat.format(
                    "value to set is null, skipping setting of \"{0}\" property on bean \"{1}\"", propertyName, //$NON-NLS-1$
                    bean));
            return;
        }

        String origPropertyName = propertyName;
        for (PropertyNameFormatter formatter : formatters) {
            propertyName = formatter.format(propertyName);
        }

        // here we check if we can set the input value on the bean. There are three ways that bean utils will go about
        // this
        // 1. use a simple property setter method
        // .. in the case of an indexed property there are two methods:
        // 2. if there is an indexed setter method bean utils will that (note: a simple getter is required as well
        // though it
        // will not be invoked)
        // 3. if there is an array-based getter like List<String> getNames(), bean utils will insert the new value into
        // the
        // array reference
        // it gets from the array getter.
        if (isWriteable(propertyName)) {

            // we get the value at the latest point possible
            Object val = value;
            if (value instanceof ValueGenerator) {
                val = ((ValueGenerator) value).getValue(propertyName);
            }
            try {
                // trying our best to set the input value to the type specified by the action bean
                typeConvertingBeanUtil.copyProperty(bean, propertyName, val);
            } catch (Exception e) {
                String propertyType = ""; //$NON-NLS-1$
                try {
                    propertyType = getPropertyType(propertyName).getName();
                } catch (Throwable t) {
                    // we are in a nested catch, we should never let an exception escape here
                }
                callback.failedToSetValue(bean, propertyName, val, propertyType, e);
            }
        } else {
            callback.propertyNotWritable(bean, origPropertyName);
        }
    }

    /**
     * Sets a number of bean properties based on given property-value map, where the key of the map is the bean
     * property and the value is the value to which to set that property.
     * 
     * @param propValueMap
     *          a map whose keys are property names and whose values are to be set on the associated property of the
     *          bean
     * @throws Exception
     *           if there was a problem setting the value on the bean
     */
    public void setValues(Map<String, Object> propValueMap) throws Exception {
        for (Map.Entry<String, Object> entry : propValueMap.entrySet()) {
            setValue(entry.getKey(), entry.getValue());
        }
    }

    public void setValues(Map<String, Object> propValueMap, ValueSetErrorCallback callback) throws Exception {
        for (Map.Entry<String, Object> entry : propValueMap.entrySet()) {
            setValue(entry.getKey(), entry.getValue(), callback);
        }
    }

    public void setValues(Map<String, Object> propValueMap, PropertyNameFormatter... formatters) throws Exception {
        for (Map.Entry<String, Object> entry : propValueMap.entrySet()) {
            setValue(entry.getKey(), entry.getValue(), formatters);
        }
    }

    public static class FeedbackValueGenerator {
        private Object value;

        public FeedbackValueGenerator(Object value) {
            this.value = value;
        }

        public Object getValueToSet(String name) throws Exception {
            return value;
        }
    };

    public static class EagerFailingCallback implements ValueSetErrorCallback {

        public void failedToSetValue(Object bean, String propertyName, Object value, String beanPropertyType,
                Throwable cause) throws Exception {
            String valueType = (value != null) ? value.getClass().getName() : "[ClassNameNotAvailable]"; //$NON-NLS-1$
            String beanType = (bean != null) ? bean.getClass().getName() : "[ClassNameNotAvailable]"; //$NON-NLS-1$
            throw new InvocationTargetException(cause,
                    Messages.getInstance().getErrorString("BeanUtil.ERROR_0001_FAILED_TO_SET_PROPERTY", beanType, //$NON-NLS-1$
                            propertyName, beanPropertyType, valueType));
        }

        public void propertyNotWritable(Object bean, String propertyName) throws Exception {
            String beanType = (bean != null) ? bean.getClass().getName() : "[ClassNameNotAvailable]"; //$NON-NLS-1$
            throw new IllegalAccessException(Messages.getInstance()
                    .getErrorString("BeanUtil.ERROR_0002_NO_METHOD_FOR_PROPERTY", beanType, propertyName)); //$NON-NLS-1$
        }
    }

}