/*! * 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-2016 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 ) { try { return propUtil.isWriteable( bean, propertyName ) || ( propUtil.getResolver().isIndexed( propertyName ) && propUtil.isReadable( bean, propertyName ) ); } catch ( IllegalArgumentException exception ) { logger.debug( "Exception rose. Return false value", exception ); return false; } } /** * 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, bean ) ); //$NON-NLS-1$ 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$ } } }