bind action

attribute mapper enhancements
This commit is contained in:
Keith Donald
2008-03-15 01:10:50 +00:00
parent 21d6318484
commit c5340f7f2b
39 changed files with 540 additions and 405 deletions

View File

@@ -117,6 +117,7 @@ public class GenericConversionService implements ConversionService {
* Add an alias for given target type.
*/
public void addAlias(String alias, Class targetType) {
Assert.isTrue(!targetType.isPrimitive(), "Primitive types cannot be registered");
aliasMap.put(alias, targetType);
}
@@ -137,6 +138,8 @@ public class GenericConversionService implements ConversionService {
if (targetClass.isAssignableFrom(sourceClass)) {
return new ConversionExecutorImpl(sourceClass, targetClass, new NoOpConverter(sourceClass, targetClass));
}
sourceClass = convertToWrapperClassIfNecessary(sourceClass);
targetClass = convertToWrapperClassIfNecessary(targetClass);
Map sourceTargetConverters = findConvertersForSource(sourceClass);
Converter converter = findTargetConverter(sourceTargetConverters, targetClass);
if (converter != null) {
@@ -220,6 +223,25 @@ public class GenericConversionService implements ConversionService {
}
}
// subclassing support
/**
* Returns an indexed map of converters. Each entry key is a source class that can be converted from, and each entry
* value is a map of target classes that can be convertered to, ultimately mapping to a specific converter that can
* perform the source->target conversion.
*/
protected Map getSourceClassConverters() {
return sourceClassConverters;
}
/**
* Returns a map of known aliases. Each entry key is a String alias and the associated value is either a target
* class or a converter.
*/
protected Map getAliasMap() {
return aliasMap;
}
// internal helpers
private Map findConvertersForSource(Class sourceClass) {
@@ -264,22 +286,29 @@ public class GenericConversionService implements ConversionService {
return null;
}
// subclassing support
/**
* Returns an indexed map of converters. Each entry key is a source class that can be converted from, and each entry
* value is a map of target classes that can be convertered to, ultimately mapping to a specific converter that can
* perform the source->target conversion.
*/
protected Map getSourceClassConverters() {
return sourceClassConverters;
}
/**
* Returns a map of known aliases. Each entry key is a String alias and the associated value is either a target
* class or a converter.
*/
protected Map getAliasMap() {
return aliasMap;
private Class convertToWrapperClassIfNecessary(Class targetType) {
if (targetType.isPrimitive()) {
if (targetType.equals(int.class)) {
return Integer.class;
} else if (targetType.equals(short.class)) {
return Short.class;
} else if (targetType.equals(long.class)) {
return Long.class;
} else if (targetType.equals(float.class)) {
return Float.class;
} else if (targetType.equals(double.class)) {
return Double.class;
} else if (targetType.equals(byte.class)) {
return Byte.class;
} else if (targetType.equals(boolean.class)) {
return Boolean.class;
} else if (targetType.equals(char.class)) {
return Character.class;
} else {
throw new IllegalStateException("Should never happen - primitive type is not a primitive?");
}
} else {
return targetType;
}
}
}

View File

@@ -50,7 +50,7 @@ public class TextToNumber extends AbstractFormattingConverter {
}
public Class[] getTargetClasses() {
return new Class[] { Integer.class, Short.class, Byte.class, Long.class, Float.class, Double.class,
return new Class[] { Integer.class, Short.class, Long.class, Float.class, Double.class, Byte.class,
BigInteger.class, BigDecimal.class };
}

View File

@@ -33,9 +33,24 @@ public interface Expression {
/**
* Set this expression in the provided context to the value provided.
* @param context the context to apply this value to
* @param context the context to set this value to
* @param value the new value to be set
* @throws EvaluationException an exception occurred during evaluation
*/
public void setValue(Object context, Object value) throws EvaluationException;
/**
* Returns the most general type that can be passed to the {@link #setValue(Object, Object)} method for the given
* context.
* @param context the context of expression evaluation
* @return the most general type of value that can be set
*/
public Class getValueType(Object context);
/**
* Returns the original string used to create this expression, unmodified.
* @return the original expression string
*/
public String getExpressionString();
}

View File

@@ -41,7 +41,11 @@ public class DefaultELResolver extends CompositeELResolver {
}
public Class getType(ELContext context, Object base, Object property) {
return super.getType(context, base, property);
if (base == null) {
return super.getType(context, target, property);
} else {
return super.getType(context, base, property);
}
}
public Object getValue(ELContext context, Object base, Object property) {

View File

@@ -21,16 +21,21 @@ public class ELExpression implements Expression {
private ValueExpression valueExpression;
private boolean template;
/**
* Creates a new el expression
* @param factory the el context factory for creating the EL context that will be used during expression evaluation
* @param valueExpression the value expression to evaluate
* @param template whether or not this expression is a template expression; if not it was parsed as an implict eval
* expression (without delimiters)
*/
public ELExpression(ELContextFactory factory, ValueExpression valueExpression) {
public ELExpression(ELContextFactory factory, ValueExpression valueExpression, boolean template) {
Assert.notNull(factory, "The ELContextFactory is required to evaluate EL expressions");
Assert.notNull(valueExpression, "The EL value expression is required for evaluation");
this.elContextFactory = factory;
this.valueExpression = valueExpression;
this.template = template;
}
public Object getValue(Object context) throws EvaluationException {
@@ -58,6 +63,24 @@ public class ELExpression implements Expression {
}
}
public Class getValueType(Object context) {
ELContext ctx = elContextFactory.getELContext(context);
try {
return valueExpression.getType(ctx);
} catch (ELException ex) {
throw new EvaluationException(new EvaluationAttempt(this, context), ex);
}
}
public String getExpressionString() {
if (template) {
return valueExpression.getExpressionString();
} else {
String rawExpressionString = valueExpression.getExpressionString();
return rawExpressionString.substring("#{".length(), rawExpressionString.length() - 1);
}
}
public int hashCode() {
return valueExpression.hashCode();
}

View File

@@ -56,20 +56,21 @@ public class ELExpressionParser implements ExpressionParser {
context = NullParserContext.INSTANCE;
}
if (context.isTemplate()) {
return parseTemplate(expressionString, context);
return parseExpressionInternal(expressionString, context, true);
} else {
assertNotDelimited(expressionString);
assertHasText(expressionString);
return parseTemplate("#{" + expressionString + "}", context);
return parseExpressionInternal("#{" + expressionString + "}", context, false);
}
}
private Expression parseTemplate(String expressionString, ParserContext context) throws ParserException {
private Expression parseExpressionInternal(String expressionString, ParserContext context, boolean template)
throws ParserException {
Assert.notNull(expressionString, "The expression string to parse is required");
try {
ValueExpression expression = parseValueExpression(expressionString, context);
ELContextFactory contextFactory = getContextFactory(context.getEvaluationContextType(), expressionString);
return new ELExpression(contextFactory, expression);
return new ELExpression(contextFactory, expression, template);
} catch (ELException e) {
throw new ParserException(expressionString, e);
}

View File

@@ -52,13 +52,18 @@ class OgnlExpression implements Expression {
private Class expectedResultType;
/**
* Creates a new OGNL expression.
* @param expression the parsed expression
* The original expression string.
*/
public OgnlExpression(Object expression, Map variableExpressions, Class expectedResultType) {
private String expressionString;
/**
* Creates a new OGNL expression.
*/
public OgnlExpression(Object expression, Map variableExpressions, Class expectedResultType, String expressionString) {
this.expression = expression;
this.variableExpressions = variableExpressions;
this.expectedResultType = expectedResultType;
this.expressionString = expressionString;
}
public int hashCode() {
@@ -98,6 +103,23 @@ class OgnlExpression implements Expression {
}
}
public Class getValueType(Object context) {
try {
if (Ognl.isSimpleProperty(expression)) {
// TODO
throw new UnsupportedOperationException("Not yet implemented - in progress");
} else {
return null;
}
} catch (OgnlException e) {
throw new EvaluationException(new EvaluationAttempt(this, context), e);
}
}
public String getExpressionString() {
return expressionString;
}
private Map getVariables(Object context) {
if (variableExpressions == null) {
return Collections.EMPTY_MAP;

View File

@@ -244,7 +244,7 @@ public class OgnlExpressionParser implements ExpressionParser {
}
try {
return new OgnlExpression(Ognl.parseExpression(expressionString), parseVariableExpressions(context
.getExpressionVariables()), context.getExpectedEvaluationResultType());
.getExpressionVariables()), context.getExpectedEvaluationResultType(), expressionString);
} catch (OgnlException e) {
throw new ParserException(expressionString, e);
}

View File

@@ -19,4 +19,12 @@ public abstract class AbstractGetValueExpression implements Expression {
throw new UnsupportedOperationException("Setting this expression's value is not supported");
}
public Class getValueType(Object context) {
return null;
}
public String getExpressionString() {
return null;
}
}

View File

@@ -60,6 +60,14 @@ public class CollectionAddingExpression implements Expression {
}
}
public Class getValueType(Object context) {
return Object.class;
}
public String getExpressionString() {
return null;
}
public String toString() {
return new ToStringCreator(this).append("collectionExpression", collectionExpression).toString();
}

View File

@@ -54,6 +54,14 @@ public class CompositeStringExpression implements Expression {
"Cannot set a composite string expression value"));
}
public Class getValueType(Object context) {
return String.class;
}
public String getExpressionString() {
return null;
}
public String toString() {
return new ToStringCreator(this).append("expressions", expressions).toString();
}

View File

@@ -43,6 +43,14 @@ public class LiteralExpression implements Expression {
+ "If so, should the expression string be enclosed in eval delimiters?"));
}
public Class getValueType(Object context) {
return String.class;
}
public String getExpressionString() {
return null;
}
public String toString() {
return "literal('" + literal + "')";
}

View File

@@ -55,14 +55,22 @@ public final class StaticExpression implements Expression {
return ObjectUtils.nullSafeEquals(value, other.value);
}
public Object getValue(Object target) throws EvaluationException {
public Object getValue(Object context) throws EvaluationException {
return value;
}
public void setValue(Object target, Object value) throws EvaluationException {
public void setValue(Object context, Object value) throws EvaluationException {
this.value = value;
}
public Class getValueType(Object context) {
return Object.class;
}
public String getExpressionString() {
return null;
}
public String toString() {
return String.valueOf(value);
}

View File

@@ -19,7 +19,6 @@ import java.text.ParseException;
import org.springframework.binding.format.Formatter;
import org.springframework.binding.format.InvalidFormatException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -29,38 +28,29 @@ import org.springframework.util.StringUtils;
*/
public abstract class AbstractFormatter implements Formatter {
/**
* Does this formatter allow empty values?
*/
private boolean allowEmpty = true;
/**
* Constructs a formatter.
*/
protected AbstractFormatter() {
}
/**
* Constructs a formatter.
* @param allowEmpty allow formatting of empty (null or blank) values?
*/
protected AbstractFormatter(boolean allowEmpty) {
this.allowEmpty = allowEmpty;
}
/**
* Allow formatting of empty (null or blank) values?
*/
public boolean isAllowEmpty() {
return allowEmpty;
}
public final String formatValue(Object value) {
if (allowEmpty && isEmpty(value)) {
if (isEmpty(value)) {
return getEmptyFormattedValue();
} else {
return doFormatValue(value);
}
}
public final Object parseValue(String formattedString, Class targetClass) throws InvalidFormatException {
try {
if (isEmpty(formattedString)) {
return getEmptyValue();
}
return doParseValue(formattedString, targetClass);
} catch (ParseException ex) {
throw new InvalidFormatException(formattedString, getExpectedFormat(targetClass), ex);
}
Assert.isTrue(!isEmpty(value), "Object to format cannot be empty");
return doFormatValue(value);
}
/**
@@ -77,22 +67,12 @@ public abstract class AbstractFormatter implements Formatter {
return "";
}
public final Object parseValue(String formattedString, Class targetClass) throws InvalidFormatException {
try {
if (allowEmpty && isEmpty(formattedString)) {
return getEmptyValue();
}
return doParseValue(formattedString, targetClass);
} catch (ParseException ex) {
throw new InvalidFormatException(formattedString, getExpectedFormat(targetClass), ex);
}
}
/**
* Template method subclasses should override to encapsulate parsing logic.
* @param formattedString the formatted string to parse
* @param targetClass the target class to convert the formatted value to
* @return the parsed value
* @throws InvalidFormatException an exception occured parsing
* @throws InvalidFormatException an exception occurred parsing
* @throws ParseException when parse exceptions occur
*/
protected abstract Object doParseValue(String formattedString, Class targetClass) throws InvalidFormatException,

View File

@@ -38,16 +38,6 @@ public class DateFormatter extends AbstractFormatter {
this.dateFormat = dateFormat;
}
/**
* Constructs a date formatter that will delegate to the specified date format.
* @param dateFormat the date format to use
* @param allowEmpty should this formatter allow empty input arguments?
*/
public DateFormatter(DateFormat dateFormat, boolean allowEmpty) {
super(allowEmpty);
this.dateFormat = dateFormat;
}
// convert from date to string
protected String doFormatValue(Object date) {
return dateFormat.format((Date) date);

View File

@@ -36,14 +36,6 @@ public class LabeledEnumFormatter extends AbstractFormatter {
public LabeledEnumFormatter() {
}
/**
* Create a new LabeledEnum formatter.
* @param allowEmpty should this formatter allow empty input arguments?
*/
public LabeledEnumFormatter(boolean allowEmpty) {
super(allowEmpty);
}
/**
* Set the LabeledEnumResolver used. Defaults to {@link StaticLabeledEnumResolver}.
*/
@@ -58,13 +50,7 @@ public class LabeledEnumFormatter extends AbstractFormatter {
}
protected Object doParseValue(String formattedString, Class targetClass) throws IllegalArgumentException {
LabeledEnum labeledEnum = labeledEnumResolver.getLabeledEnumByLabel(targetClass, formattedString);
if (!isAllowEmpty()) {
Assert.notNull(labeledEnum, "The label '" + formattedString
+ "' did not map to a valid enum instance for type " + targetClass);
Assert.isInstanceOf(targetClass, labeledEnum);
}
return labeledEnum;
return labeledEnumResolver.getLabeledEnumByLabel(targetClass, formattedString);
}
/**

View File

@@ -31,13 +31,6 @@ public class NumberFormatter extends AbstractFormatter {
private NumberFormat numberFormat;
/**
* Default constructor. The formatter will use "toString" when formatting a value and "valueOf" when parsing a
* value.
*/
public NumberFormatter() {
}
/**
* Create a new number formatter.
* @param numberFormat the number format to use
@@ -46,16 +39,6 @@ public class NumberFormatter extends AbstractFormatter {
this.numberFormat = numberFormat;
}
/**
* Create a new number formatter.
* @param numberFormat the number format to use
* @param allowEmpty should this formatter allow empty input arguments?
*/
public NumberFormatter(NumberFormat numberFormat, boolean allowEmpty) {
super(allowEmpty);
this.numberFormat = numberFormat;
}
protected String doFormatValue(Object number) {
if (this.numberFormat != null) {
// use NumberFormat for rendering value
@@ -67,12 +50,11 @@ public class NumberFormatter extends AbstractFormatter {
}
protected Object doParseValue(String text, Class targetClass) throws IllegalArgumentException {
// use given NumberFormat for parsing text
if (this.numberFormat != null) {
// use given NumberFormat for parsing text
return NumberUtils.parseNumber(text, targetClass, this.numberFormat);
}
// use default valueOf methods for parsing text
else {
} else {
// use default valueOf methods for parsing text
return NumberUtils.parseNumber(text, targetClass);
}
}

View File

@@ -42,7 +42,11 @@ public class SimpleFormatterFactory extends AbstractFormatterFactory {
}
public Formatter getNumberFormatter(Class numberClass) {
return new NumberFormatter(NumberFormat.getNumberInstance(getLocale()));
if (numberClass.equals(Integer.class) || numberClass.equals(int.class)) {
return new NumberFormatter(NumberFormat.getIntegerInstance(getLocale()));
} else {
return new NumberFormatter(NumberFormat.getNumberInstance(getLocale()));
}
}
public Formatter getCurrencyFormatter() {

View File

@@ -30,6 +30,7 @@ public interface AttributeMapper {
* @param source the source
* @param target the target
* @param context the mapping context
* @throws AttributeMappingException if errors occurred during the mapping process
*/
public void map(Object source, Object target, MappingContext context);
public void map(Object source, Object target, MappingContext context) throws AttributeMappingException;
}

View File

@@ -0,0 +1,5 @@
package org.springframework.binding.mapping;
public class AttributeMappingException extends RuntimeException {
}

View File

@@ -38,10 +38,10 @@ public class DefaultAttributeMapper implements AttributeMapper {
/**
* Add a mapping to this mapper.
* @param mapping the mapping to add (as an AttributeMapper)
* @param mapping the mapping to add
* @return this, to support convenient call chaining
*/
public DefaultAttributeMapper addMapping(AttributeMapper mapping) {
public DefaultAttributeMapper addMapping(Mapping mapping) {
mappings.add(mapping);
return this;
}
@@ -50,7 +50,7 @@ public class DefaultAttributeMapper implements AttributeMapper {
* Add a set of mappings.
* @param mappings the mappings
*/
public void addMappings(AttributeMapper[] mappings) {
public void addMappings(Mapping[] mappings) {
if (mappings == null) {
return;
}
@@ -61,18 +61,25 @@ public class DefaultAttributeMapper implements AttributeMapper {
* Returns this mapper's list of mappings.
* @return the list of mappings
*/
public AttributeMapper[] getMappings() {
return (AttributeMapper[]) mappings.toArray(new AttributeMapper[mappings.size()]);
public Mapping[] getMappings() {
return (Mapping[]) mappings.toArray(new Mapping[mappings.size()]);
}
public void map(Object source, Object target, MappingContext context) {
public void map(Object source, Object target, MappingContext context) throws AttributeMappingException {
boolean mappingFailure = false;
if (mappings != null) {
Iterator it = mappings.iterator();
while (it.hasNext()) {
AttributeMapper mapping = (AttributeMapper) it.next();
mapping.map(source, target, context);
Mapping mapping = (Mapping) it.next();
boolean result = mapping.map(source, target, context);
if (!result && !mappingFailure) {
mappingFailure = true;
}
}
}
if (mappingFailure) {
throw new AttributeMappingException();
}
}
public String toString() {

View File

@@ -17,10 +17,14 @@ package org.springframework.binding.mapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.binding.convert.ConversionException;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageResolver;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A single mapping definition, encapsulating the information necessary to map the result of evaluating an expression on
@@ -28,7 +32,7 @@ import org.springframework.util.Assert;
*
* @author Keith Donald
*/
public class Mapping implements AttributeMapper {
public class Mapping {
private static final Log logger = LogFactory.getLog(Mapping.class);
@@ -85,29 +89,54 @@ public class Mapping implements AttributeMapper {
* @param source The source data structure
* @param target The target data structure
*/
public void map(Object source, Object target, MappingContext context) {
// get source value
public boolean map(Object source, Object target, MappingContext context) {
Assert.notNull(source, "The source to map from is required");
Assert.notNull(target, "The target to map to is required");
Assert.notNull(context, "The mapping context is required");
Object sourceValue = sourceExpression.getValue(source);
if (sourceValue == null) {
if (required) {
throw new RequiredMappingException("This mapping is required; evaluation of expression '"
+ sourceExpression + "' against source of type [" + source.getClass()
+ "] must return a non-null value");
} else {
// source expression returned no value, simply abort mapping
return;
if (required && sourceValue == null || isEmptyString(sourceValue)) {
String defaultText = "'" + targetExpression.getExpressionString() + "' is required";
MessageResolver message = new MessageBuilder().error().source(targetExpression.getExpressionString())
.codes(createMessageCodes("required", target, targetExpression)).defaultText(defaultText).build();
context.getMessageContext().addMessage(message);
return false;
}
Object targetValue;
if (sourceValue != null && typeConverter != null) {
try {
targetValue = typeConverter.execute(sourceValue);
} catch (ConversionException e) {
String defaultText = "The '" + targetExpression.getExpressionString() + "' value is the wrong type";
MessageResolver message = new MessageBuilder().error().source(targetExpression.getExpressionString())
.codes(createMessageCodes("typeMismatch", target, targetExpression)).defaultText(defaultText)
.build();
context.getMessageContext().addMessage(message);
return false;
}
} else {
targetValue = sourceValue;
}
Object targetValue = sourceValue;
if (typeConverter != null) {
targetValue = typeConverter.execute(sourceValue);
}
// set target value
if (logger.isDebugEnabled()) {
logger.debug("Mapping '" + sourceExpression + "' value [" + sourceValue + "] to target property '"
+ targetExpression + "'; setting property value to [" + targetValue + "]");
logger.debug("Mapping '" + sourceExpression + "' value [" + sourceValue + "] to target '"
+ targetExpression + "'; setting target value to [" + targetValue + "]");
}
targetExpression.setValue(target, targetValue);
return true;
}
private boolean isEmptyString(Object sourceValue) {
if (sourceValue instanceof CharSequence) {
return ((CharSequence) sourceValue).length() == 0;
} else {
return false;
}
}
private String[] createMessageCodes(String errorCode, Object target, Expression targetExpression) {
String[] codes = new String[2];
codes[0] = ClassUtils.getShortName(target.getClass()) + "." + targetExpression.getExpressionString();
codes[1] = errorCode;
return codes;
}
public boolean equals(Object o) {

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.binding.mapping;
import org.springframework.binding.message.MessageContext;
/**
* A context object with two main responsibities:
* <ol>
@@ -27,4 +29,10 @@ package org.springframework.binding.mapping;
*/
public interface MappingContext {
/**
* Returns the message context to use to record errors during the mapping process.
* @return the message context
*/
public MessageContext getMessageContext();
}

View File

@@ -0,0 +1,17 @@
package org.springframework.binding.mapping;
import org.springframework.binding.message.MessageContext;
public class MappingContextImpl implements MappingContext {
private MessageContext messageContext;
public MappingContextImpl(MessageContext messageContext) {
this.messageContext = messageContext;
}
public MessageContext getMessageContext() {
return messageContext;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2004-2007 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.binding.mapping;
import junit.framework.TestCase;
/**
* Unit tests for the {@link org.springframework.binding.mapping.RequiredMapping}.
*/
public class MappingTests extends TestCase {
public void testMapping() {
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright 2004-2007 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.binding.mapping;
import java.util.HashMap;
import junit.framework.TestCase;
import org.springframework.binding.expression.ognl.OgnlExpressionParser;
/**
* Unit tests for the {@link org.springframework.binding.mapping.RequiredMapping}.
*/
public class RequiredMappingTests extends TestCase {
public void testRequired() {
MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser());
Mapping mapping = builder.source("foo").target("bar").required().value();
HashMap source = new HashMap();
source.put("foo", "baz");
HashMap target = new HashMap();
mapping.map(source, target, null);
assertEquals("baz", target.get("bar"));
}
public void testRequiredExceptionOnNull() {
MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser());
Mapping mapping = builder.source("foo").target("bar").required().value();
HashMap source = new HashMap();
source.put("foo", null);
HashMap target = new HashMap();
try {
mapping.map(source, target, null);
} catch (RequiredMappingException e) {
}
}
public void testRequiredExceptionOnNoKey() {
MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser());
Mapping mapping = builder.source("foo").target("bar").required().value();
HashMap source = new HashMap();
HashMap target = new HashMap();
try {
mapping.map(source, target, null);
} catch (RequiredMappingException e) {
}
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.webflow.action;
import org.springframework.binding.mapping.AttributeMapper;
import org.springframework.binding.mapping.MappingContext;
import org.springframework.binding.mapping.MappingContextImpl;
import org.springframework.util.Assert;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
@@ -63,6 +64,6 @@ public class AttributeMapperAction extends AbstractAction {
* returns null. Subclasses can override this if necessary.
*/
protected MappingContext getMappingContext(RequestContext context) {
return null;
return new MappingContextImpl(context.getMessageContext());
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.webflow.action;
import java.util.Iterator;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.convert.support.RuntimeBindingConversionExecutor;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.support.ParserContextImpl;
import org.springframework.binding.mapping.AttributeMappingException;
import org.springframework.binding.mapping.DefaultAttributeMapper;
import org.springframework.binding.mapping.Mapping;
import org.springframework.binding.mapping.MappingContextImpl;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
public class BindAction extends AbstractAction {
private Expression target;
private ExpressionParser expressionParser;
private ConversionService conversionService;
public BindAction(Expression target, ExpressionParser expressionParser, ConversionService conversionService) {
this.target = target;
this.expressionParser = expressionParser;
this.conversionService = conversionService;
}
protected Event doExecute(final RequestContext context) throws Exception {
Object target = this.target.getValue(context);
if (target == null) {
throw new IllegalStateException(
"The bind target cannot be null - check your expression. Bind target expression = " + target);
}
DefaultAttributeMapper mapper = new DefaultAttributeMapper();
AttributeMap eventAttributes = context.getLastEvent().getAttributes();
for (Iterator it = eventAttributes.asMap().keySet().iterator(); it.hasNext();) {
String name = (String) it.next();
Expression sourceAttribute = expressionParser.parseExpression(name, new ParserContextImpl()
.eval(AttributeMap.class));
Expression targetAttribute = expressionParser.parseExpression(name, new ParserContextImpl().eval(target
.getClass()));
Class targetType = targetAttribute.getValueType(target);
mapper.addMapping(new Mapping(sourceAttribute, targetAttribute, new RuntimeBindingConversionExecutor(
targetType, conversionService), false));
}
try {
mapper.map(context.getLastEvent().getAttributes(), target, new MappingContextImpl(context
.getMessageContext()));
return success();
} catch (AttributeMappingException e) {
return error();
}
}
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.webflow.core;
import org.springframework.core.NestedRuntimeException;
/**
* Root class for exceptions thrown by the Spring Web Flow system. All other exceptions within the system should be
* assignable to this class.
@@ -24,7 +22,7 @@ import org.springframework.core.NestedRuntimeException;
* @author Keith Donald
* @author Erwin Vervaet
*/
public abstract class FlowException extends NestedRuntimeException {
public abstract class FlowException extends RuntimeException {
/**
* Creates a new flow exception.

View File

@@ -16,9 +16,8 @@
package org.springframework.webflow.engine;
import org.springframework.binding.mapping.AttributeMapper;
import org.springframework.binding.mapping.MappingContext;
import org.springframework.binding.mapping.MappingContextImpl;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.execution.Action;
import org.springframework.webflow.execution.Event;
@@ -49,12 +48,12 @@ public class EndState extends State {
/**
* The renderer that will render the final response when a flow execution terminates.
*/
private Action finalResponseAction = new NullFinalResponseAction();
private Action finalResponseAction;
/**
* The attribute mapper for mapping output attributes exposed by this end state when it is entered.
*/
private AttributeMapper outputMapper = new NoOutputMapper();
private AttributeMapper outputMapper;
/**
* Create a new end state with no associated view.
@@ -73,7 +72,6 @@ public class EndState extends State {
* Sets the renderer that will render the final flow execution response.
*/
public void setFinalResponseAction(Action finalResponseAction) {
Assert.notNull(finalResponseAction, "The final response action is required");
this.finalResponseAction = finalResponseAction;
}
@@ -81,7 +79,6 @@ public class EndState extends State {
* Sets the attribute mapper to use for mapping output attributes exposed by this end state when it is entered.
*/
public void setOutputMapper(AttributeMapper outputMapper) {
Assert.notNull(outputMapper, "The flow session output mapper is required");
this.outputMapper = outputMapper;
}
@@ -99,7 +96,9 @@ public class EndState extends State {
FlowSession activeSession = context.getFlowExecutionContext().getActiveSession();
if (activeSession.isRoot()) {
// entire flow execution is ending; issue the final response
ActionExecutor.execute(finalResponseAction, context);
if (finalResponseAction != null) {
ActionExecutor.execute(finalResponseAction, context);
}
context.endActiveFlowSession(createSessionOutput(context));
} else {
// there is a parent flow that will resume (this flow is a subflow)
@@ -115,7 +114,9 @@ public class EndState extends State {
*/
protected LocalAttributeMap createSessionOutput(RequestContext context) {
LocalAttributeMap outputMap = new LocalAttributeMap();
outputMapper.map(context, outputMap, null);
if (outputMapper != null) {
outputMapper.map(context, outputMap, new MappingContextImpl(context.getMessageContext()));
}
return outputMap;
}
@@ -123,29 +124,4 @@ public class EndState extends State {
creator.append("finalResponseAction", finalResponseAction).append("outputMapper", outputMapper);
}
/**
* Renders no response. The default implementation.
*/
private class NullFinalResponseAction implements Action {
public Event execute(RequestContext context) {
logger.debug("Not issuing a final response");
return new Event(this, "success");
}
public String toString() {
return "none";
}
}
/**
* Maps no output attributes. The default implementation.
*/
private class NoOutputMapper implements AttributeMapper {
public void map(Object source, Object target, MappingContext context) {
}
public String toString() {
return "none";
}
}
}

View File

@@ -26,6 +26,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.binding.mapping.AttributeMapper;
import org.springframework.binding.mapping.MappingContextImpl;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.style.StylerUtils;
@@ -511,7 +512,7 @@ public class Flow extends AnnotatedObject implements FlowDefinition {
assertStartStateSet();
createVariables(context);
if (inputMapper != null) {
inputMapper.map(input, context, null);
inputMapper.map(input, context, new MappingContextImpl(context.getMessageContext()));
}
startActionList.execute(context);
startState.enter(context);
@@ -563,7 +564,7 @@ public class Flow extends AnnotatedObject implements FlowDefinition {
public void end(RequestControlContext context, MutableAttributeMap output) throws FlowExecutionException {
endActionList.execute(context);
if (outputMapper != null) {
outputMapper.map(context, output, null);
outputMapper.map(context, output, new MappingContextImpl(context.getMessageContext()));
}
}

View File

@@ -983,6 +983,14 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
public void setValue(Object context, Object value) throws EvaluationException {
throw new UnsupportedOperationException("Cannot set a subflow expression");
}
public Class getValueType(Object context) {
return null;
}
public String getExpressionString() {
return null;
}
}
public String toString() {

View File

@@ -121,6 +121,15 @@ Goes out of scope when the governing conversation ends.
<xsd:group name="actionTypes">
<xsd:choice>
<xsd:element ref="bind">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Binds current event parameters to a target object associated with the flow. Also capable of performing a validation post-binding.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="evaluate">
<xsd:annotation>
<xsd:documentation>
@@ -649,7 +658,21 @@ Handles exceptions that occur within this state.
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="bind">
<xsd:complexType>
<xsd:attribute name="target" type="expression" use="required">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
The bind target.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="evaluate">
<xsd:complexType>
<xsd:attribute name="expression" type="expression" use="required">
@@ -737,7 +760,7 @@ The expected value type. If the actual value type does not match the expected, a
<xsd:annotation>
<xsd:documentation>
<![CDATA[
An attribute describing this transition.
A meta attribute describing or otherwise annotating this transition.
]]>
</xsd:documentation>
</xsd:annotation>

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2004-2007 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.engine.support;
import java.io.Serializable;
import org.springframework.binding.mapping.AttributeMapper;
import org.springframework.binding.mapping.MappingContext;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.engine.SubflowAttributeMapper;
import org.springframework.webflow.execution.RequestContext;
/**
* Convenient base class for subflow attribute mapper implementations. Encapsulates common attribute mapper workflow.
* Contains no state. Subclasses must override the {@link #getInputMapper()} and {@link #getOutputMapper()} methods to
* return the input mapper and output mapper, respectively.
*
* @author Keith Donald
*/
public abstract class AbstractSubflowAttributeMapper implements SubflowAttributeMapper, Serializable {
/**
* Returns the input mapper to use to map attributes of a parent flow {@link RequestContext} to a subflow input
* {@link AttributeMap attribute map}.
* @return the input mapper, or null if none
* @see #createFlowInput(RequestContext)
*/
protected abstract AttributeMapper getInputMapper();
/**
* Returns the output mapper to use to map attributes from a subflow output {@link AttributeMap attribute map} to
* the {@link RequestContext}.
* @return the output mapper, or null if none
* @see #mapFlowOutput(AttributeMap, RequestContext)
*/
protected abstract AttributeMapper getOutputMapper();
public MutableAttributeMap createFlowInput(RequestContext context) {
AttributeMapper inputMapper = getInputMapper();
if (inputMapper != null) {
LocalAttributeMap input = new LocalAttributeMap();
inputMapper.map(context, input, getMappingContext(context));
return input;
} else {
return new LocalAttributeMap();
}
}
public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) {
AttributeMapper outputMapper = getOutputMapper();
if (getOutputMapper() != null && subflowOutput != null) {
outputMapper.map(subflowOutput, context, getMappingContext(context));
}
}
/**
* Returns a map of contextual data available during mapping. This implementation just returns null.
*/
protected MappingContext getMappingContext(RequestContext context) {
return null;
}
}

View File

@@ -18,14 +18,20 @@ package org.springframework.webflow.engine.support;
import java.io.Serializable;
import org.springframework.binding.mapping.AttributeMapper;
import org.springframework.binding.mapping.MappingContextImpl;
import org.springframework.core.style.ToStringCreator;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.engine.SubflowAttributeMapper;
import org.springframework.webflow.execution.RequestContext;
/**
* Simple flow attribute mapper that holds an input and output mapper strategy.
*
* @author Keith Donald
*/
public final class GenericSubflowAttributeMapper extends AbstractSubflowAttributeMapper implements Serializable {
public final class GenericSubflowAttributeMapper implements SubflowAttributeMapper, Serializable {
private final AttributeMapper inputMapper;
@@ -41,12 +47,20 @@ public final class GenericSubflowAttributeMapper extends AbstractSubflowAttribut
this.outputMapper = outputMapper;
}
protected AttributeMapper getInputMapper() {
return inputMapper;
public MutableAttributeMap createFlowInput(RequestContext context) {
if (inputMapper != null) {
LocalAttributeMap input = new LocalAttributeMap();
inputMapper.map(context, input, new MappingContextImpl(context.getMessageContext()));
return input;
} else {
return new LocalAttributeMap();
}
}
protected AttributeMapper getOutputMapper() {
return outputMapper;
public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) {
if (outputMapper != null && subflowOutput != null) {
outputMapper.map(subflowOutput, context, new MappingContextImpl(context.getMessageContext()));
}
}
public String toString() {

View File

@@ -20,8 +20,6 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.JdkVersion;
import org.springframework.core.NestedRuntimeException;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.webflow.engine.ActionList;
@@ -137,63 +135,24 @@ public class TransitionExecutingFlowExecutionExceptionHandler implements FlowExe
* Find the mapped target state resolver for given exception. Returns <code>null</code> if no mapping can be found
* for given exception. Will try all exceptions in the exception cause chain.
*/
protected TargetStateResolver getTargetStateResolver(FlowExecutionException e) {
if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) {
return getTargetStateResolver13(e);
} else {
return getTargetStateResolver14(e);
}
}
/**
* Internal getTargetStateResolver implementation for use with JDK 1.3.
*/
private TargetStateResolver getTargetStateResolver13(NestedRuntimeException e) {
protected TargetStateResolver getTargetStateResolver(Throwable e) {
TargetStateResolver targetStateResolver;
if (isRootCause13(e)) {
if (isRootCause(e)) {
return findTargetStateResolver(e.getClass());
} else {
targetStateResolver = (TargetStateResolver) exceptionTargetStateMappings.get(e.getClass());
if (targetStateResolver != null) {
return targetStateResolver;
} else {
if (e.getCause() instanceof NestedRuntimeException) {
return getTargetStateResolver13((NestedRuntimeException) e.getCause());
} else {
return null;
}
return getTargetStateResolver(e.getCause());
}
}
}
/**
* Internal getTargetStateResolver implementation for use with JDK 1.4 or later.
*/
private TargetStateResolver getTargetStateResolver14(Throwable t) {
TargetStateResolver targetStateResolver;
if (isRootCause14(t)) {
return findTargetStateResolver(t.getClass());
} else {
targetStateResolver = (TargetStateResolver) exceptionTargetStateMappings.get(t.getClass());
if (targetStateResolver != null) {
return targetStateResolver;
} else {
return getTargetStateResolver14(t.getCause());
}
}
}
/**
* Check if given exception is the root of the exception cause chain. For use with JDK 1.3.
*/
private boolean isRootCause13(NestedRuntimeException e) {
return e.getCause() == null;
}
/**
* Check if given exception is the root of the exception cause chain. For use with JDK 1.4 or later.
*/
private boolean isRootCause14(Throwable t) {
private boolean isRootCause(Throwable t) {
return t.getCause() == null;
}
@@ -214,43 +173,15 @@ public class TransitionExecutingFlowExecutionExceptionHandler implements FlowExe
return null;
}
/**
* Find the root cause of given throwable.
*/
protected Throwable findRootCause(Throwable t) {
if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) {
return findRootCause13(t);
} else {
return findRootCause14(t);
}
}
/**
* Find the root cause of given throwable. For use on JDK 1.3.
*/
private Throwable findRootCause13(Throwable t) {
if (t instanceof NestedRuntimeException) {
NestedRuntimeException nre = (NestedRuntimeException) t;
Throwable cause = nre.getCause();
if (cause == null) {
return nre;
} else {
return findRootCause13(cause);
}
} else {
return t;
}
}
/**
* Find the root cause of given throwable. For use on JDK 1.4 or later.
*/
private Throwable findRootCause14(Throwable e) {
private Throwable findRootCause(Throwable e) {
Throwable cause = e.getCause();
if (cause == null) {
return e;
} else {
return findRootCause14(cause);
return findRootCause(cause);
}
}

View File

@@ -0,0 +1,92 @@
package org.springframework.webflow.action;
import junit.framework.TestCase;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.convert.support.DefaultConversionService;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.support.ParserContextImpl;
import org.springframework.binding.message.Severity;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.expression.DefaultExpressionParserFactory;
import org.springframework.webflow.test.MockRequestContext;
public class BindActionTests extends TestCase {
private ExpressionParser expressionParser = DefaultExpressionParserFactory.getExpressionParser();
private ConversionService conversionService = new DefaultConversionService();
private BindAction action;
public void testSuccessfulBind() throws Exception {
MockRequestContext context = new MockRequestContext();
context.getFlowScope().put("bindTarget", new BindBean());
Expression target = expressionParser.parseExpression("bindTarget", new ParserContextImpl()
.eval(RequestContext.class));
action = new BindAction(target, expressionParser, conversionService);
LocalAttributeMap eventData = new LocalAttributeMap();
eventData.put("stringProperty", "foo");
eventData.put("integerProperty", "3");
Event event = new Event(this, "submit", eventData);
context.setLastEvent(event);
Event result = action.execute(context);
assertEquals("success", result.getId());
BindBean bean = (BindBean) context.getFlowScope().get("bindTarget");
assertEquals("foo", bean.getStringProperty());
assertEquals(3, bean.getIntegerProperty());
}
public void testBindWithErrors() throws Exception {
MockRequestContext context = new MockRequestContext();
context.getFlowScope().put("bindTarget", new BindBean());
Expression target = expressionParser.parseExpression("bindTarget", new ParserContextImpl()
.eval(RequestContext.class));
action = new BindAction(target, expressionParser, conversionService);
LocalAttributeMap eventData = new LocalAttributeMap();
eventData.put("stringProperty", "foo");
eventData.put("integerProperty", "malformed");
Event event = new Event(this, "submit", eventData);
context.setLastEvent(event);
Event result = action.execute(context);
assertEquals("error", result.getId());
BindBean bean = (BindBean) context.getFlowScope().get("bindTarget");
assertEquals("foo", bean.getStringProperty());
assertEquals(0, bean.getIntegerProperty());
assertEquals(1, context.getMessageContext().getMessages().length);
assertEquals("integerProperty", context.getMessageContext().getMessages()[0].getSource());
assertEquals(Severity.ERROR, context.getMessageContext().getMessages()[0].getSeverity());
assertEquals("The 'integerProperty' value is the wrong type", context.getMessageContext().getMessages()[0]
.getText());
}
public static class BindBean {
private String stringProperty;
private int integerProperty;
public String getStringProperty() {
return stringProperty;
}
public void setStringProperty(String stringProperty) {
this.stringProperty = stringProperty;
}
public int getIntegerProperty() {
return integerProperty;
}
public void setIntegerProperty(int integerProperty) {
this.integerProperty = integerProperty;
}
}
}

View File

@@ -213,7 +213,8 @@ public class FlowTests extends TestCase {
MockRequestControlContext context = new MockRequestControlContext(flow);
LocalAttributeMap sessionInput = new LocalAttributeMap();
flow.start(context, sessionInput);
assertFalse(context.getFlowScope().contains("attr"));
assertTrue(context.getFlowScope().contains("attr"));
assertNull(context.getFlowScope().get("attr"));
}
public void testOnEventNullCurrentState() {

View File

@@ -3,7 +3,7 @@ package org.springframework.webflow.engine.builder.xml;
import junit.framework.TestCase;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.binding.mapping.RequiredMappingException;
import org.springframework.binding.mapping.AttributeMappingException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.webflow.action.ExternalRedirectAction;
import org.springframework.webflow.action.FlowDefinitionRedirectAction;
@@ -123,7 +123,7 @@ public class XmlFlowBuilderTests extends TestCase {
execution.start(input, context);
fail("Should have failed");
} catch (FlowExecutionException e) {
RequiredMappingException me = (RequiredMappingException) e.getRootCause();
AttributeMappingException me = (AttributeMappingException) e.getCause();
}
}
@@ -141,7 +141,7 @@ public class XmlFlowBuilderTests extends TestCase {
execution.start(input, context);
fail("Should have failed");
} catch (FlowExecutionException e) {
RequiredMappingException me = (RequiredMappingException) e.getRootCause();
AttributeMappingException me = (AttributeMappingException) e.getCause();
}
}