org.springframework.springfaces.mvc.bind.ReverseDataBinder.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.springfaces.mvc.bind.ReverseDataBinder.java

Source

/*
 * Copyright 2010-2012 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.springfaces.mvc.bind;

import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.PropertyEditorRegistrySupport;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.ConvertingPropertyEditorAdapter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;

/**
 * Utility class that can be used to perform a reverse bind for a given {@link DataBinder}. This class can be used to
 * obtain {@link PropertyValues} for a given a {@link DataBinder} based on the current values of its <tt>target</tt> or
 * perform a simple reverse conversion for plain parameter values when the binders <tt>target</tt> is <tt>null</tt>.
 * 
 * @author Phillip Webb
 */
public class ReverseDataBinder {

    Log logger = LogFactory.getLog(getClass());

    /**
     * Set of properties that are always skipped.
     */
    private static final Set<String> SKIPPED_PROPERTIES;
    static {
        SKIPPED_PROPERTIES = new HashSet<String>();
        SKIPPED_PROPERTIES.add("class");
    }

    private DataBinder dataBinder;

    private SimpleTypeConverter simpleTypeConverter;

    private boolean skipDefaultValues = true;

    /**
     * Default constructor.
     * @param dataBinder a non null dataBinder
     */
    public ReverseDataBinder(DataBinder dataBinder) {
        Assert.notNull(dataBinder, "DataBinder must not be null");
        this.dataBinder = dataBinder;
    }

    /**
     * Reverse convert a simple object value.
     * @param value the value to convert
     * @return the converted value
     */
    public String reverseConvert(Object value) {
        if (value == null) {
            return null;
        }
        PropertyEditor propertyEditor = findEditor(null, null, null, value.getClass(),
                TypeDescriptor.forObject(value));
        return convertToStringUsingPropertyEditor(value, propertyEditor);
    }

    /**
     * Perform the reverse bind on the <tt>dataBinder</tt> provided in the constructor. Note: Calling with method will
     * also trigger a <tt>bind</tt> operation on the <tt>dataBinder</tt>. This method returns {@link PropertyValues}
     * containing a name/value pairs for each property that can be bound. Property values are encoded as Strings using
     * the property editors bound to the original dataBinder.
     * @return property values that could be re-bound using the data binder
     * @throws IllegalStateException if the target object values cannot be bound
     */
    public PropertyValues reverseBind() {
        Assert.notNull(this.dataBinder.getTarget(),
                "ReverseDataBinder.reverseBind can only be used with a DataBinder that has a target object");

        MutablePropertyValues rtn = new MutablePropertyValues();
        BeanWrapper target = PropertyAccessorFactory.forBeanPropertyAccess(this.dataBinder.getTarget());

        ConversionService conversionService = this.dataBinder.getConversionService();
        if (conversionService != null) {
            target.setConversionService(conversionService);
        }

        PropertyDescriptor[] propertyDescriptors = target.getPropertyDescriptors();

        BeanWrapper defaultValues = null;
        if (this.skipDefaultValues) {
            defaultValues = newDefaultTargetValues(this.dataBinder.getTarget());
        }

        for (int i = 0; i < propertyDescriptors.length; i++) {
            PropertyDescriptor property = propertyDescriptors[i];
            String propertyName = PropertyAccessorUtils.canonicalPropertyName(property.getName());
            Object propertyValue = target.getPropertyValue(propertyName);

            if (isSkippedProperty(property)) {
                continue;
            }

            if (!isMutableProperty(property)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Ignoring '" + propertyName + "' due to missing read/write methods");
                }
                continue;
            }

            if (defaultValues != null
                    && ObjectUtils.nullSafeEquals(defaultValues.getPropertyValue(propertyName), propertyValue)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Skipping '" + propertyName + "' as property contains default value");
                }
                continue;
            }

            // Find a property editor
            PropertyEditorRegistrySupport propertyEditorRegistrySupport = null;
            if (target instanceof PropertyEditorRegistrySupport) {
                propertyEditorRegistrySupport = (PropertyEditorRegistrySupport) target;
            }

            PropertyEditor propertyEditor = findEditor(propertyName, propertyEditorRegistrySupport,
                    target.getWrappedInstance(), target.getPropertyType(propertyName),
                    target.getPropertyTypeDescriptor(propertyName));

            // Convert and store the value
            String convertedPropertyValue = convertToStringUsingPropertyEditor(propertyValue, propertyEditor);
            if (convertedPropertyValue != null) {
                rtn.addPropertyValue(propertyName, convertedPropertyValue);
            }
        }

        this.dataBinder.bind(rtn);
        BindingResult bindingResult = this.dataBinder.getBindingResult();
        if (bindingResult.hasErrors()) {
            throw new IllegalStateException("Unable to reverse bind from target '" + this.dataBinder.getObjectName()
                    + "', the properties '" + rtn + "' will result in binding errors when re-bound "
                    + bindingResult.getAllErrors());
        }
        return rtn;
    }

    /**
     * Find a property editor by searching custom editors or falling back to default editors.
     * @param propertyName the property name or <tt>null</tt> if looking for an editor for all properties of the given
     * type
     * @param propertyEditorRegistrySupport an optional {@link PropertyEditorRegistrySupport} instance. If <tt>null</tt>
     * a {@link SimpleTypeConverter} instance will be used
     * @param targetObject the target object or <tt>null</tt>
     * @param requiredType the required type.
     * @param typeDescriptor the type descriptor
     * @return the corresponding editor, or <code>null</code> if none
     */
    protected PropertyEditor findEditor(String propertyName,
            PropertyEditorRegistrySupport propertyEditorRegistrySupport, Object targetObject, Class<?> requiredType,
            TypeDescriptor typeDescriptor) {

        Assert.notNull(requiredType, "RequiredType must not be null");
        Assert.notNull(typeDescriptor, "TypeDescription must not be null");

        // Use the custom editor if there is one
        PropertyEditor editor = this.dataBinder.findCustomEditor(requiredType, propertyName);
        if (editor != null) {
            return editor;
        }

        // Use the conversion service
        ConversionService conversionService = this.dataBinder.getConversionService();
        if (conversionService != null) {
            if (conversionService.canConvert(TypeDescriptor.valueOf(String.class), typeDescriptor)) {
                return new ConvertingPropertyEditorAdapter(conversionService, typeDescriptor);
            }
        }

        // Fall back to default editors
        if (propertyEditorRegistrySupport == null) {
            propertyEditorRegistrySupport = getSimpleTypeConverter();
        }
        return findDefaultEditor(propertyEditorRegistrySupport, targetObject, requiredType, typeDescriptor);
    }

    /**
     * Gets the {@link SimpleTypeConverter} that should be used for conversion.
     * @return the simple type converter
     */
    protected SimpleTypeConverter getSimpleTypeConverter() {
        if (this.simpleTypeConverter == null) {
            this.simpleTypeConverter = new SimpleTypeConverter();
        }
        return this.simpleTypeConverter;
    }

    /**
     * Find a default editor for the given type. This code is based on <tt>TypeConverterDelegate.findDefaultEditor</tt>.
     * @param requiredType the type to find an editor for
     * @param typeDescriptor the type description of the property
     * @return the corresponding editor, or <code>null</code> if none
     * 
     * @param propertyEditorRegistry
     * @param targetObject
     * 
     * @author Juergen Hoeller
     * @author Rob Harrop
     */
    protected PropertyEditor findDefaultEditor(PropertyEditorRegistrySupport propertyEditorRegistry,
            Object targetObject, Class<?> requiredType, TypeDescriptor typeDescriptor) {
        PropertyEditor editor = null;
        if (requiredType != null) {
            // No custom editor -> check BeanWrapperImpl's default editors.
            editor = propertyEditorRegistry.getDefaultEditor(requiredType);
            if (editor == null && !String.class.equals(requiredType)) {
                // No BeanWrapper default editor -> check standard JavaBean editor.
                editor = BeanUtils.findEditorByConvention(requiredType);
            }
        }
        return editor;
    }

    /**
     * Utility method to convert a given value into a string using a property editor.
     * @param value the value to convert (can be <tt>null</tt>)
     * @param propertyEditor the property editor or <tt>null</tt> if no suitable property editor exists
     * @return the converted value
     */
    private String convertToStringUsingPropertyEditor(Object value, PropertyEditor propertyEditor) {
        if (propertyEditor != null) {
            propertyEditor.setValue(value);
            return propertyEditor.getAsText();
        }
        if (value instanceof String) {
            return value == null ? null : value.toString();
        }
        return null;
    }

    private BeanWrapper newDefaultTargetValues(Object target) {
        try {
            Object defaultValues = target.getClass().newInstance();
            return PropertyAccessorFactory.forBeanPropertyAccess(defaultValues);
        } catch (Exception e) {
            this.logger.warn("Unable to construct default values target instance for class " + target.getClass()
                    + ", default values will not be skipped");
            return null;
        }
    }

    /**
     * Determine if a property should be skipped. Used to ignore object properties.
     * @param property the property descriptor
     * @return <tt>true</tt> if the property is skipped
     */
    private boolean isSkippedProperty(PropertyDescriptor property) {
        return SKIPPED_PROPERTIES.contains(property.getName());
    }

    /**
     * Determine if a property contains both read and write methods.
     * @param descriptor the property descriptor
     * @return <tt>true</tt> if the property is mutable
     */
    private boolean isMutableProperty(PropertyDescriptor descriptor) {
        return descriptor.getReadMethod() != null && descriptor.getWriteMethod() != null;
    }

    /**
     * Skip any bound values when the current value is identical to the value of a newly constructed instance. This
     * setting can help to reduce the number of superfluous bound properties. Note: If the target object class does not
     * have a default (no-args) constructor this setting will be ignored. The default setting is <tt>true</tt>.
     * @param skipDefaultValues <tt>true</tt> if default properties should be ignored, otherwise <tt>false</tt>
     */
    public void setSkipDefaultValues(boolean skipDefaultValues) {
        this.skipDefaultValues = skipDefaultValues;
    }
}