提交 3f5c43aa 编写于 作者: K Keith Donald

message builder

上级 05e3c00a
......@@ -79,7 +79,7 @@ public interface Binder {
/**
* Bind source values in the map to the properties of the model object.
* @param values the source values to bind
* @param sourceValues the source values to bind
* @return the results of the binding operation
*/
BindingResults bind(Map<String, ? extends Object> sourceValues);
......
......@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.context.expression.MapAccessor;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.GenericTypeResolver;
......@@ -58,11 +59,12 @@ import org.springframework.ui.binding.BindingResults;
import org.springframework.ui.binding.FormatterRegistry;
import org.springframework.ui.format.AnnotationFormatterFactory;
import org.springframework.ui.format.Formatter;
import org.springframework.ui.message.MessageBuilder;
import org.springframework.ui.message.ResolvableArgument;
import org.springframework.util.Assert;
/**
* A generic {@link Binder binder} suitable for use in most environments.
* TODO - localization of alert messages using MessageResolver/MessageSource
* @author Keith Donald
* @since 3.0
* @see #configureBinding(BindingConfiguration)
......@@ -87,6 +89,8 @@ public class GenericBinder implements Binder {
private static Formatter defaultFormatter = new DefaultFormatter();
private MessageSource messageSource;
/**
* Creates a new binder for the model object.
* @param model the model object containing properties this binder will bind to
......@@ -118,6 +122,25 @@ public class GenericBinder implements Binder {
this.formatterRegistry = formatterRegistry;
}
/**
* Configure the MessageSource that resolves localized {@link BindingResult} alert messages.
* @param messageSource the message source
*/
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* Configure the TypeConverter that converts values as required by Binding setValue and getValue attempts.
* For a setValue attempt, the TypeConverter will be asked to perform a conversion if the value parsed by the Binding's Formatter is not assignable to the target property type.
* For a getValue attempt, the TypeConverter will be asked to perform a conversion if the property type does not match the type T required by the Binding's Formatter.
* @param typeConverter the type converter used by the binding system, which is based on Spring EL
* @see EvaluationContext#getTypeConverter()
*/
public void setTypeConverter(TypeConverter typeConverter) {
this.typeConverter = typeConverter;
}
public Binding configureBinding(BindingConfiguration configuration) {
Binding binding;
try {
......@@ -158,14 +181,17 @@ public class GenericBinder implements Binder {
} else {
results.add(new NoSuchBindingResult(property, value));
}
}
}
return results;
}
// subclassing hooks
/**
* Hook subclasses may use to filter the source values to bind.
* This hook allows the binder to pre-process the source values before binding occurs.
- * For example, a Binder might insert empty or default values for fields that are not present.
- * As another example, a Binder might collapse multiple source values into a single source value.
* @param sourceValues the original source values map provided by the caller
* @return the filtered source values map that will be used to bind
*/
......@@ -334,7 +360,7 @@ public class GenericBinder implements Binder {
try {
formatter = getFormatter();
} catch (EvaluationException e) {
// could occur the property was not found or is not readable
// could occur if the property was not found or is not readable
// TODO probably should not handle all EL failures, only type conversion & property not found?
return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e);
}
......@@ -352,7 +378,7 @@ public class GenericBinder implements Binder {
try {
formatter = getFormatter();
} catch (EvaluationException e) {
// could occur the property was not found or is not readable
// could occur if the property was not found or is not readable
// TODO probably should not handle all EL failures, only type conversion & property not found?
return new ExpressionEvaluationErrorResult(property.getExpressionString(), formatted, e);
}
......@@ -461,16 +487,17 @@ public class GenericBinder implements Binder {
}
}
static class NoSuchBindingResult implements BindingResult {
private String property;
class NoSuchBindingResult implements BindingResult {
private String property;
private Object sourceValue;
public NoSuchBindingResult(String property, Object sourceValue) {
this.property = property;
this.sourceValue = sourceValue;
}
public String getProperty() {
return property;
}
......@@ -486,7 +513,6 @@ public class GenericBinder implements Binder {
public Alert getAlert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
}
......@@ -499,21 +525,30 @@ public class GenericBinder implements Binder {
}
public String getMessage() {
return "Failed to bind to property '" + property + "'; no binding has been added for the property";
MessageBuilder builder = new MessageBuilder(messageSource);
builder.code(getCode());
builder.arg("label", new ResolvableArgument(property));
builder.arg("value", sourceValue);
builder.defaultMessage("Failed to bind to property '" + property
+ "'; no binding has been added for the property");
return builder.build();
}
};
}
}
}
static class InvalidFormatResult implements BindingResult {
class InvalidFormatResult implements BindingResult {
private String property;
private Object formatted;
private Object sourceValue;
private ParseException cause;
public InvalidFormatResult(String property, Object formatted, ParseException e) {
public InvalidFormatResult(String property, Object sourceValue, ParseException cause) {
this.property = property;
this.formatted = formatted;
this.sourceValue = sourceValue;
this.cause = cause;
}
public String getProperty() {
......@@ -521,7 +556,7 @@ public class GenericBinder implements Binder {
}
public Object getSourceValue() {
return formatted;
return sourceValue;
}
public boolean isFailure() {
......@@ -531,7 +566,6 @@ public class GenericBinder implements Binder {
public Alert getAlert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
}
......@@ -544,26 +578,31 @@ public class GenericBinder implements Binder {
}
public String getMessage() {
return "Failed to bind to property '" + property + "'; the user value "
+ StylerUtils.style(formatted) + " has an invalid format and could no be parsed";
MessageBuilder builder = new MessageBuilder(messageSource);
builder.code(getCode());
builder.arg("label", new ResolvableArgument(property));
builder.arg("value", sourceValue);
builder.arg("errorOffset", cause.getErrorOffset());
builder.defaultMessage("Failed to bind to property '" + property + "'; the user value "
+ StylerUtils.style(sourceValue) + " has an invalid format and could no be parsed");
return builder.build();
}
};
}
}
// TODO the if branching in here is not very clean
static class ExpressionEvaluationErrorResult implements BindingResult {
class ExpressionEvaluationErrorResult implements BindingResult {
private String property;
private Object formatted;
private Object sourceValue;
private EvaluationException e;
private EvaluationException cause;
public ExpressionEvaluationErrorResult(String property, Object formatted, EvaluationException e) {
public ExpressionEvaluationErrorResult(String property, Object sourceValue, EvaluationException cause) {
this.property = property;
this.formatted = formatted;
this.e = e;
this.sourceValue = sourceValue;
this.cause = cause;
}
public String getProperty() {
......@@ -571,7 +610,7 @@ public class GenericBinder implements Binder {
}
public Object getSourceValue() {
return formatted;
return sourceValue;
}
public boolean isFailure() {
......@@ -581,95 +620,97 @@ public class GenericBinder implements Binder {
public Alert getAlert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
}
public String getCode() {
return getFailureCode();
SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode();
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) {
return "conversionFailed";
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) {
return "propertyNotFound";
} else {
return "couldNotSetValue";
}
}
public Severity getSeverity() {
return getFailureSeverity();
SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode();
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) {
return Severity.FATAL;
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) {
return Severity.WARNING;
} else {
return Severity.FATAL;
}
}
public String getMessage() {
return getFailureMessage();
SpelMessage spelCode = ((SpelEvaluationException) cause).getMessageCode();
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) {
AccessException accessException = (AccessException) cause.getCause();
if (accessException.getCause() != null) {
Throwable cause = accessException.getCause();
if (cause instanceof SpelEvaluationException
&& ((SpelEvaluationException) cause).getMessageCode() == SpelMessage.TYPE_CONVERSION_ERROR) {
ConversionFailedException failure = (ConversionFailedException) cause.getCause();
MessageBuilder builder = new MessageBuilder(messageSource);
builder.code("conversionFailed");
builder.arg("label", new ResolvableArgument(property));
builder.arg("value", sourceValue);
builder.defaultMessage("Failed to bind to property '" + property + "'; user value "
+ StylerUtils.style(sourceValue) + " could not be converted to property type ["
+ failure.getTargetType().getName() + "]");
return builder.build();
}
}
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) {
MessageBuilder builder = new MessageBuilder(messageSource);
builder.code(getCode());
builder.arg("label", new ResolvableArgument(property));
builder.arg("value", sourceValue);
builder.defaultMessage("Failed to bind to property '" + property + "'; no such property exists on model");
return builder.build();
}
MessageBuilder builder = new MessageBuilder(messageSource);
builder.code("couldNotSetValue");
builder.arg("label", new ResolvableArgument(property));
builder.arg("value", sourceValue);
builder.defaultMessage("Failed to bind to property '" + property + "'; reason = " + cause.getLocalizedMessage());
return builder.build();
}
};
}
public String getFailureCode() {
SpelMessage spelCode = ((SpelEvaluationException) e).getMessageCode();
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) {
return "typeConversionFailure";
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) {
return "propertyNotFound";
} else {
// TODO return more specific code based on underlying EvaluationException error code
return "couldNotSetValue";
}
}
public Severity getFailureSeverity() {
SpelMessage spelCode = ((SpelEvaluationException) e).getMessageCode();
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) {
return Severity.FATAL;
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) {
return Severity.WARNING;
} else {
return Severity.FATAL;
}
}
public String getFailureMessage() {
SpelMessage spelCode = ((SpelEvaluationException) e).getMessageCode();
if (spelCode == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) {
AccessException accessException = (AccessException) e.getCause();
if (accessException.getCause() != null) {
Throwable cause = accessException.getCause();
if (cause instanceof SpelEvaluationException
&& ((SpelEvaluationException) cause).getMessageCode() == SpelMessage.TYPE_CONVERSION_ERROR) {
ConversionFailedException failure = (ConversionFailedException) cause.getCause();
return "Failed to bind to property '" + property + "'; user value "
+ StylerUtils.style(formatted) + " could not be converted to property type ["
+ failure.getTargetType().getName() + "]";
}
}
} else if (spelCode == SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE) {
return "Failed to bind to property '" + property + "'; no such property exists on model";
}
return "Failed to bind to property '" + property + "'; reason = " + e.getLocalizedMessage();
}
}
static class SuccessResult implements BindingResult {
class SuccessResult implements BindingResult {
private String property;
private Object formatted;
private Object sourceValue;
public SuccessResult(String property, Object formatted) {
public SuccessResult(String property, Object sourceValue) {
this.property = property;
this.formatted = formatted;
this.sourceValue = sourceValue;
}
public String getProperty() {
return property;
}
public Object getSourceValue() {
return formatted;
return sourceValue;
}
public boolean isFailure() {
return false;
}
public Alert getAlert() {
return new AbstractAlert() {
public String getElement() {
// TODO append model first? e.g. model.property
return getProperty();
}
......@@ -682,13 +723,18 @@ public class GenericBinder implements Binder {
}
public String getMessage() {
return "Sucessfully bound user value " + StylerUtils.style(formatted) + "to property '" + property + "'";
MessageBuilder builder = new MessageBuilder(messageSource);
builder.code("bindSuccess");
builder.arg("label", new ResolvableArgument(property));
builder.arg("value", sourceValue);
builder.defaultMessage("Successfully bound user value " + StylerUtils.style(sourceValue) + "to property '" + property + "'");
return builder.build();
}
};
}
}
static abstract class AbstractAlert implements Alert {
public String toString() {
return getElement() + ":" + getCode() + " - " + getMessage();
......
......@@ -39,7 +39,7 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso
private Map<String, Object> args;
private String defaultText;
private String defaultMessage;
private ExpressionParser expressionParser;
......@@ -47,18 +47,25 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso
ExpressionParser expressionParser) {
this.codes = codes;
this.args = args;
this.defaultText = defaultText;
this.defaultMessage = defaultText;
this.expressionParser = expressionParser;
}
// implementing MessageResolver
public String resolveMessage(MessageSource messageSource, Locale locale) {
if (messageSource == null) {
if (defaultMessage != null) {
return defaultMessage;
} else {
throw new MessageResolutionException("Unable to resolve message; MessagSource argument is null and no defaultMessage is configured");
}
}
String messageString;
try {
messageString = messageSource.getMessage(this, locale);
} catch (NoSuchMessageException e) {
throw new MessageResolutionException("Unable to resolve message in MessageSource [" + messageSource + "]", e);
throw new MessageResolutionException("Unable to resolve message in" + messageSource, e);
}
Expression message;
try {
......@@ -88,11 +95,11 @@ final class DefaultMessageResolver implements MessageResolver, MessageSourceReso
}
public String getDefaultMessage() {
return defaultText;
return defaultMessage;
}
public String toString() {
return new ToStringCreator(this).append("codes", codes).append("defaultText", defaultText).toString();
return new ToStringCreator(this).append("codes", codes).append("defaultText", defaultMessage).toString();
}
@SuppressWarnings("unchecked")
......
/*
* Copyright 2004-2009 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.ui.message;
import java.util.Locale;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
/**
* Builds a localized message for display in a user interface.
* Allows convenient specification of the codes to try to resolve the message.
* Also supports named arguments that can inserted into a message template using eval #{expressions}.
* <p>
* Usage example:
* <pre>
* String message = new MessageBuilder(messageSource).
* code("invalidFormat").
* arg("label", new ResolvableArgument("mathForm.decimalField")).
* arg("format", "#,###.##").
* defaultMessage("The decimal field must be in format #,###.##").
* build();
* </pre>
* Example messages.properties loaded by the MessageSource:
* <pre>
* invalidFormat=The #{label} must be in format #{format}.
* mathForm.decimalField=Decimal Field
* </pre>
* @author Keith Donald
* @since 3.0
* @see #code(String)
* @see #arg(String, Object)
* @see #defaultMessage(String)
* @see #locale(Locale)
*/
public class MessageBuilder {
private MessageSource messageSource;
private Locale locale;
private MessageResolverBuilder builder = new MessageResolverBuilder();
/**
* Create a new MessageBuilder that builds messages from message templates defined in the MessageSource
* @param messageSource the message source
*/
public MessageBuilder(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* Add a code that will be tried to lookup the message template used to create the localized message.
* Successive calls to this method add additional codes.
* Codes are tried in the order they are added.
* @param code a message code to try
* @return this, for fluent API usage
*/
public MessageBuilder code(String code) {
builder.code(code);
return this;
}
/**
* Add an argument to insert into the message.
* Named arguments are inserted by eval #{expressions} denoted within the message template.
* For example, the value of the 'format' argument would be inserted where a corresponding #{format} expression is defined in the message template.
* Successive calls to this method add additional arguments.
* May also add {@link ResolvableArgument resolvable arguments} whose values are resolved against the MessageSource.
* @param name the argument name
* @param value the argument value
* @return this, for fluent API usage
* @see ResolvableArgument
*/
public MessageBuilder arg(String name, Object value) {
builder.arg(name, value);
return this;
}
/**
* Set the default message.
* If there are no codes to try, this will be used as the message.
* If there are codes to try but none of those resolve to a message, this will be used as the message.
* @param message the default text
* @return this, for fluent API usage
*/
public MessageBuilder defaultMessage(String message) {
builder.defaultMessage(message);
return this;
}
/**
* Set the message locale.
* If not set, the default locale the Locale of the current request obtained from {@link LocaleContextHolder#getLocale()}.
* @param message the locale
* @return this, for fluent API usage
*/
public MessageBuilder locale(Locale locale) {
this.locale = locale;
return this;
}
/**
* Builds the resolver for the message.
* Call after recording all builder instructions.
* @return the built message resolver
* @throws IllegalStateException if no codes have been added and there is no default message
*/
public String build() {
return builder.build().resolveMessage(messageSource, getLocale());
}
// internal helpers
private Locale getLocale() {
if (locale != null) {
return locale;
} else {
return LocaleContextHolder.getLocale();
}
}
}
\ No newline at end of file
......@@ -23,6 +23,14 @@ package org.springframework.ui.message;
@SuppressWarnings("serial")
public class MessageResolutionException extends RuntimeException {
/**
* Creates a new message resolution exception.
* @param message a messaging describing the failure
*/
public MessageResolutionException(String message) {
super(message);
}
/**
* Creates a new message resolution exception.
* @param message a messaging describing the failure
......
......@@ -20,8 +20,7 @@ import java.util.Locale;
import org.springframework.context.MessageSource;
/**
* A factory for a localized message.
* TODO - consider putting this abstraction together with MessageSource; does it need to be in its own package?
* A factory for a localized message resolved from a MessageSource.
* @author Keith Donald
* @since 3.0
* @see MessageSource
......
......@@ -86,7 +86,7 @@ public class GenericBinderTests {
BindingResults results = binder.bind(values);
assertEquals(3, results.size());
assertTrue(results.get(1).isFailure());
assertEquals("typeConversionFailure", results.get(1).getAlert().getCode());
assertEquals("conversionFailed", results.get(1).getAlert().getCode());
}
@Test
......@@ -229,7 +229,7 @@ public class GenericBinderTests {
assertEquals(0, b.getCollectionValues().length);
BindingResult result = b.setValue(new String[] { "BAR", "BOGUS", "BOOP" });
assertTrue(result.isFailure());
assertEquals("typeConversionFailure", result.getAlert().getCode());
assertEquals("conversionFailed", result.getAlert().getCode());
}
@Test
......
package org.springframework.ui.message;
import static org.junit.Assert.assertEquals;
import java.util.Locale;
import org.junit.Test;
public class MessageBuilderTests {
@Test
public void buildMessage() {
MockMessageSource messageSource = new MockMessageSource();
messageSource.addMessage("invalidFormat", Locale.US, "#{label} must be in format #{format}");
messageSource.addMessage("mathForm.decimalField", Locale.US, "Decimal Field");
MessageBuilder builder = new MessageBuilder(messageSource);
String message = builder.code("invalidFormat").arg("label", new ResolvableArgument("mathForm.decimalField"))
.arg("format", "#,###.##").defaultMessage("Field must be in format #,###.##").build();
assertEquals("Decimal Field must be in format #,###.##", message);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册