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