/*
* Copyright 2004-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.webflow.mvc.view;
import java.beans.PropertyEditor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.support.FluentParserContext;
import org.springframework.binding.mapping.MappingResult;
import org.springframework.binding.mapping.MappingResults;
import org.springframework.binding.mapping.MappingResultsCriteria;
import org.springframework.binding.message.Message;
import org.springframework.binding.message.MessageContext;
import org.springframework.binding.message.MessageCriteria;
import org.springframework.binding.message.Severity;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import org.springframework.validation.AbstractErrors;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.webflow.engine.builder.BinderConfiguration;
/**
* Makes the properties of the "model" object available to Spring views during rendering. Also makes data binding (aka
* mapping) results available after a form postback attempt. Also makes error messages available to the view.
*
* This class is a Spring Errors adapter, basically, for use with spring form and bind tags.
*
* @see AbstractMvcView
*
* @author Keith Donald
* @author Jeremy Grelle
* @author Phillip Webb
*/
public class BindingModel extends AbstractErrors implements BindingResult {
private String objectName;
private Object boundObject;
private ExpressionParser expressionParser;
private ConversionService conversionService;
private MappingResults mappingResults;
private MessageContext messageContext;
private BinderConfiguration binderConfiguration;
/**
* Creates a new Spring Binding model.
* @param objectName the name of the bound model object
* @param boundObject the bound model object
* @param expressionParser the expression parser used to access model object properties
* @param conversionService the registry used to access converters for formatting properties
* @param messageContext the message context containing flow messages to display
*/
public BindingModel(String objectName, Object boundObject, ExpressionParser expressionParser,
ConversionService conversionService, MessageContext messageContext) {
Assert.hasText(objectName, "The object name is required");
Assert.notNull(boundObject, "The bound object instance is required");
this.objectName = objectName;
this.boundObject = boundObject;
this.expressionParser = expressionParser;
this.conversionService = conversionService;
this.messageContext = messageContext;
}
/**
* Sets the results of a data mapping attempt onto the bound model object from the view.
* @see AbstractMvcView#processUserEvent()
* @param results
*/
public void setMappingResults(MappingResults results) {
this.mappingResults = results;
}
public void setBinderConfiguration(BinderConfiguration binderConfiguration) {
this.binderConfiguration = binderConfiguration;
}
// implementing Errors
public List<ObjectError> getAllErrors() {
return toErrors(messageContext.getMessagesByCriteria(ERRORS_ANY_SOURCE), ALL_ERRORS);
}
public List<ObjectError> getGlobalErrors() {
return toErrors(messageContext.getMessagesByCriteria(ERRORS_WITHOUT_FIELD_SOURCE), ALL_ERRORS);
}
public List<FieldError> getFieldErrors(String field) {
field = fixedField(field);
MessageCriteria messageCriteria;
if (field.endsWith("*")) {
String prefix = field.substring(0, field.length() - 1);
messageCriteria = new FieldPrefixErrorMessage(prefix);
} else {
messageCriteria = new FieldErrorMessage(field);
}
return toErrors(messageContext.getMessagesByCriteria(messageCriteria), FIELD_ERRORS);
}
public Class<?> getFieldType(String field) {
return parseFieldExpression(fixedField(field), false).getValueType(boundObject);
}
public Object getFieldValue(String field) {
field = fixedField(field);
if (mappingResults != null) {
List<MappingResult> results = mappingResults.getResults(new FieldErrorResult(field));
if (!results.isEmpty()) {
MappingResult fieldError = results.get(0);
return fieldError.getOriginalValue();
}
}
return getFormattedValue(field);
}
// not typically used by mvc views, but implemented to be on the safe side
public List<FieldError> getFieldErrors() {
return toErrors(messageContext.getMessagesByCriteria(ERRORS_FIELD_SOURCE), FIELD_ERRORS);
}
public String getObjectName() {
return objectName;
}
// never expected to be called by mvc views
public void addAllErrors(Errors errors) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public void reject(String errorCode, Object[] errorArgs, String defaultMessage) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
// implementing BindingResult
public Object getTarget() {
return boundObject;
}
public Object getRawFieldValue(String field) {
return parseFieldExpression(fixedField(field), false).getValue(boundObject);
}
public PropertyEditor findEditor(String field, Class<?> valueType) {
if (field != null) {
field = fixedField(field);
}
return findSpringConvertingPropertyEditor(field, valueType);
}
// never expected to be called by mvc views
public void addError(ObjectError error) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public Map<String, Object> getModel() {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public PropertyEditorRegistry getPropertyEditorRegistry() {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public String[] getSuppressedFields() {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public void recordSuppressedField(String field) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public String[] resolveMessageCodes(String errorCode, String field) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
public String[] resolveMessageCodes(String errorCode) {
throw new UnsupportedOperationException("Should not be called during view rendering");
}
// internal helpers
private Expression parseFieldExpression(String field, boolean useResultTypeHint) {
FluentParserContext parserContext = new FluentParserContext().evaluate(boundObject.getClass());
if (useResultTypeHint) {
parserContext.expectResult(String.class);
}
return expressionParser.parseExpression(field, parserContext);
}
private Object getFormattedValue(String field) {
Expression fieldExpression = parseFieldExpression(field, true);
Class<?> valueType = fieldExpression.getValueType(boundObject);
if (isCustomConverterConfigured(field) || avoidConversion(valueType)) {
fieldExpression = parseFieldExpression(fieldExpression.getExpressionString(), false);
}
Object value = fieldExpression.getValue(boundObject);
if ((value instanceof String) == false) {
if (avoidConversion(valueType) == false) {
PropertyEditor editor = findSpringConvertingPropertyEditor(field, valueType);
if (editor != null) {
editor.setValue(value);
value = editor.getAsText();
}
}
}
return value;
}
private boolean isCustomConverterConfigured(String field) {
if (binderConfiguration == null) {
return false;
}
return (binderConfiguration.getConverterId(field) != null);
}
private boolean avoidConversion(Class<?> valueType) {
// special handling for array, collection, map types
// necessary as getFieldValue is called by form tags for non-formattable properties, too
// TODO - investigate how to improve this in Spring MVC
if (valueType == null || valueType.isArray() || Collection.class.isAssignableFrom(valueType)
|| Map.class.isAssignableFrom(valueType)) {
return true;
}
return false;
}
private PropertyEditor findSpringConvertingPropertyEditor(String field, Class<?> valueType) {
if (conversionService != null) {
String converterId = null;
if (field != null) {
if (binderConfiguration != null) {
converterId = binderConfiguration.getConverterId(field);
}
if (valueType == null) {
valueType = parseFieldExpression(field, false).getValueType(boundObject);
}
}
if (valueType != null) {
BeanWrapper accessor = PropertyAccessorFactory.forBeanPropertyAccess(boundObject);
TypeDescriptor typeDescriptor = accessor.getPropertyTypeDescriptor(field);
return new ConvertingPropertyEditorAdapter(conversionService, converterId, typeDescriptor);
} else {
return null;
}
} else {
return null;
}
}
private <T extends ObjectError> List<T> toErrors(Message[] messages, ObjectErrorFactory<T> errorFactory) {
if (messages == null || messages.length == 0) {
return Collections.emptyList();
}
ArrayList<T> errors = new ArrayList<T>(messages.length);
for (Message message : messages) {
T error = errorFactory.get(objectName, message);
if (error != null) {
errors.add(error);
}
}
return Collections.unmodifiableList(errors);
}
private static class FieldErrorResult implements MappingResultsCriteria {
private String field;
public FieldErrorResult(String field) {
this.field = field;
}
public boolean test(MappingResult result) {
if (result.isError() && field.equals(result.getMapping().getTargetExpression().getExpressionString())) {
return true;
} else {
return false;
}
}
}
private static final MessageCriteria ERRORS_ANY_SOURCE = new MessageCriteria() {
public boolean test(Message message) {
return message.getSeverity() == Severity.ERROR;
}
};
private static final MessageCriteria ERRORS_WITHOUT_FIELD_SOURCE = new MessageCriteria() {
public boolean test(Message message) {
return (!message.hasField() && message.getSeverity() == Severity.ERROR);
}
};
private static final MessageCriteria ERRORS_FIELD_SOURCE = new MessageCriteria() {
public boolean test(Message message) {
return (message.hasField() && message.getSeverity() == Severity.ERROR);
}
};
private static class FieldErrorMessage implements MessageCriteria {
private String field;
public FieldErrorMessage(String field) {
Assert.hasText(field, "The field name is required");
this.field = field;
}
public boolean test(Message message) {
return message.getSeverity() == Severity.ERROR && field.equals(message.getSource());
}
}
private static class FieldPrefixErrorMessage implements MessageCriteria {
private String fieldPrefix;
public FieldPrefixErrorMessage(String fieldPrefix) {
Assert.hasText(fieldPrefix, "The fieldPrefix is required");
this.fieldPrefix = fieldPrefix;
}
public boolean test(Message message) {
return message.getSeverity() == Severity.ERROR && message.getSource() instanceof String
&& ((String) message.getSource()).startsWith(fieldPrefix);
}
}
private static interface ObjectErrorFactory<T extends ObjectError> {
T get(String objectName, Message message);
}
private static final ObjectErrorFactory<ObjectError> ALL_ERRORS = new ObjectErrorFactory<ObjectError>() {
public ObjectError get(String objectName, Message message) {
ObjectError error = FIELD_ERRORS.get(objectName, message);
if (error == null) {
error = new ObjectError(objectName, message.getText());
}
return error;
}
};
private static final ObjectErrorFactory<FieldError> FIELD_ERRORS = new ObjectErrorFactory<FieldError>() {
public FieldError get(String objectName, Message message) {
if (message.getSource() != null) {
return new FieldError(objectName, (String) message.getSource(), message.getText());
}
return null;
}
};
}