From 78304c0ccfeef00e48e6c9f167501a3721764487 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Thu, 20 Aug 2009 03:40:23 +0000 Subject: [PATCH] core context.message, context.alert, model.binder modules; includes SpEL-based GenericBinder implementation --- .../springframework/context/alert/Alert.java | 42 +++ .../context/alert/AlertContext.java | 49 ++++ .../springframework/context/alert/Alerts.java | 104 +++++++ .../context/alert/Severity.java | 51 ++++ .../context/alert/package-info.java | 5 + .../alert/support/DefaultAlertContext.java | 66 +++++ .../context/alert/support/package-info.java | 5 + .../message/DefaultMessageFactory.java | 31 +++ .../message/DefaultMessageResolver.java | 153 ++++++++++ .../context/message/MessageBuilder.java | 148 ++++++++++ .../message/MessageResolutionException.java | 43 +++ .../context/message/MessageResolver.java | 38 +++ .../message/MessageResolverBuilder.java | 130 +++++++++ .../context/message/ResolvableArgument.java | 56 ++++ .../message/StaticDefaultMessageFactory.java | 30 ++ .../context/message/package-info.java | 5 + .../springframework/model/binder/Binder.java | 37 +++ .../model/binder/BindingResult.java | 53 ++++ .../model/binder/BindingResults.java | 68 +++++ .../model/binder/MissingFieldException.java | 58 ++++ .../model/binder/package-info.java | 5 + .../model/binder/support/AbstractBinder.java | 127 +++++++++ .../binder/support/AlertBindingResult.java | 56 ++++ .../support/ArrayListBindingResults.java | 104 +++++++ .../model/binder/support/FieldBinder.java | 35 +++ .../support/FieldNotEditableResult.java | 86 ++++++ .../binder/support/FieldNotFoundResult.java | 86 ++++++ .../model/binder/support/GenericBinder.java | 212 ++++++++++++++ .../model/binder/support/package-info.java | 5 + .../context/alert/AlertsTests.java | 34 +++ .../support/DefaultAlertContextTests.java | 39 +++ .../context/message/MessageBuilderTests.java | 23 ++ .../message/MessageResolverBuilderTests.java | 26 ++ .../context/message/MockMessageSource.java | 47 ++++ .../binder/support/GenericBinderTests.java | 261 ++++++++++++++++++ 35 files changed, 2318 insertions(+) create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/Alert.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/AlertContext.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/Alerts.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/Severity.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/package-info.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/support/DefaultAlertContext.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/alert/support/package-info.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageFactory.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageResolver.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/MessageBuilder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/MessageResolutionException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/MessageResolver.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/MessageResolverBuilder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/ResolvableArgument.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/StaticDefaultMessageFactory.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/message/package-info.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/Binder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/BindingResult.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/BindingResults.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/MissingFieldException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/package-info.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/AbstractBinder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/AlertBindingResult.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/ArrayListBindingResults.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldBinder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotEditableResult.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotFoundResult.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/GenericBinder.java create mode 100644 org.springframework.context/src/main/java/org/springframework/model/binder/support/package-info.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/alert/AlertsTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/alert/support/DefaultAlertContextTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/message/MessageBuilderTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/message/MessageResolverBuilderTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/message/MockMessageSource.java create mode 100644 org.springframework.context/src/test/java/org/springframework/model/binder/support/GenericBinderTests.java diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/Alert.java b/org.springframework.context/src/main/java/org/springframework/context/alert/Alert.java new file mode 100644 index 00000000000..a1af26c7b3f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/Alert.java @@ -0,0 +1,42 @@ +/* + * 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.context.alert; + +/** + * Communicates an event of interest to the user. + * For example, an alert may inform a user of a web application a business rule was violated. + * @author Keith Donald + * @since 3.0 + */ +public interface Alert { + + /** + * The code uniquely identifying this kind of alert; for example, "weakPassword". + * May be used as a key to lookup additional alert details. + */ + public String getCode(); + + /** + * The level of impact this alert has on the user. + */ + public Severity getSeverity(); + + /** + * The localized message to display to the user; for example, "Please enter a stronger password". + */ + public String getMessage(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/AlertContext.java b/org.springframework.context/src/main/java/org/springframework/context/alert/AlertContext.java new file mode 100644 index 00000000000..ea9764f3deb --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/AlertContext.java @@ -0,0 +1,49 @@ +/* + * 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.context.alert; + +import java.util.List; +import java.util.Map; + +/** + * A context for adding and getting alerts for display in a user interface. + * @author Keith Donald + * @since 3.0 + */ +public interface AlertContext { + + /** + * Return all alerts in this context indexed by the UI element they are associated with. + * @return the message map + */ + public Map> getAlerts(); + + /** + * Get all alerts on the UI element provided. + * Returns an empty list if no alerts have been added for the element. + * Alerts are returned in the order they were added. + * @param element the id of the element to lookup alerts against + */ + public List getAlerts(String element); + + /** + * Add an alert to this context. + * @param the element this alert is associated with + * @param alert the alert to add + */ + public void add(String element, Alert alert); + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/Alerts.java b/org.springframework.context/src/main/java/org/springframework/context/alert/Alerts.java new file mode 100644 index 00000000000..c96a50a076a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/Alerts.java @@ -0,0 +1,104 @@ +/* + * 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.context.alert; + +/** + * A static factory for conveniently constructing Alerts. + * Usage example: + *
+ *    import static org.springframework.ui.alert.Alerts.*;
+ *    
+ *    public void example() {
+ *        info("An info alert");
+ *        warning("A warning alert");
+ *        error("An error alert");
+ *        fatal("A fatal alert");
+ *    }
+ * 
+ * @author Keith Donald + * @since 3.0 + */ +public final class Alerts { + + /** + * Creates a new info alert. + * @param message the alert message + * @return the info alert + * @see Severity#INFO + */ + public static Alert info(String message) { + return new GenericAlert(Severity.INFO, message); + } + + /** + * Creates a new warning alert. + * @param message the alert message + * @return the info alert + * @see Severity#WARNING + */ + public static Alert warning(String message) { + return new GenericAlert(Severity.WARNING, message); + } + + /** + * Creates a new error alert. + * @param message the alert message + * @return the info alert + * @see Severity#ERROR + */ + public static Alert error(String message) { + return new GenericAlert(Severity.ERROR, message); + } + + /** + * Creates a new fatal alert. + * @param message the alert message + * @return the info alert + * @see Severity#FATAL + */ + public static Alert fatal(String message) { + return new GenericAlert(Severity.FATAL, message); + } + + private static class GenericAlert implements Alert { + + private Severity severity; + + private String message; + + public GenericAlert(Severity severity, String message) { + this.severity = severity; + this.message = message; + } + + public String getCode() { + return null; + } + + public Severity getSeverity() { + return severity; + } + + public String getMessage() { + return message; + } + + public String toString() { + return getSeverity() + ": " + getMessage(); + } + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/Severity.java b/org.springframework.context/src/main/java/org/springframework/context/alert/Severity.java new file mode 100644 index 00000000000..1db315a56b8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/Severity.java @@ -0,0 +1,51 @@ +/* + * 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.context.alert; + +/** + * The set of alert severities. + * @author Keith Donald + * @since 3.0 + * @see Alert + */ +public enum Severity { + + /** + * The "Informational" severity. + * Indicates a successful operation or result. + */ + INFO, + + /** + * The "Warning" severity. + * Indicates there is a minor problem, or to inform the user of possible misuse, + * or to indicate a problem may arise in the future. + */ + WARNING, + + /** + * The "Error" severity. + * Indicates a significant problem like a business rule violation. + */ + ERROR, + + /** + * The "Fatal" severity. + * Indicates a fatal problem like a system error or runtime exception. + */ + FATAL + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/package-info.java b/org.springframework.context/src/main/java/org/springframework/context/alert/package-info.java new file mode 100644 index 00000000000..d48d144481e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/package-info.java @@ -0,0 +1,5 @@ +/** + * A general-purpose Alerting API to communicate events of interest. + */ +package org.springframework.context.alert; + diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/support/DefaultAlertContext.java b/org.springframework.context/src/main/java/org/springframework/context/alert/support/DefaultAlertContext.java new file mode 100644 index 00000000000..4cb2e367e03 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/support/DefaultAlertContext.java @@ -0,0 +1,66 @@ +/* + * 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.context.alert.support; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.AlertContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.CachingMapDecorator; + +/** + * The default alert context implementation. + * @author Keith Donald + * @since 3.0 + */ +public class DefaultAlertContext implements AlertContext { + + @SuppressWarnings("serial") + private Map> alerts = new CachingMapDecorator>(new LinkedHashMap>()) { + protected List create(String element) { + return new ArrayList(); + } + }; + + // implementing AlertContext + + public Map> getAlerts() { + return Collections.unmodifiableMap(alerts); + } + + public List getAlerts(String element) { + List messages = alerts.get(element); + if (messages.isEmpty()) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(messages); + } + + public void add(String element, Alert alert) { + List alerts = this.alerts.get(element); + alerts.add(alert); + } + + public String toString() { + return new ToStringCreator(this).append("alerts", alerts).toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/alert/support/package-info.java b/org.springframework.context/src/main/java/org/springframework/context/alert/support/package-info.java new file mode 100644 index 00000000000..a05f69acff2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/alert/support/package-info.java @@ -0,0 +1,5 @@ +/** + * AlertContext implementation suitable for use in most environments. + */ +package org.springframework.context.alert.support; + diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageFactory.java b/org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageFactory.java new file mode 100644 index 00000000000..c64eda980e6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageFactory.java @@ -0,0 +1,31 @@ +/* + * 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.context.message; + +/** + * A factory for a default message to return if no message could be resolved. + * Allows the message String to be created lazily, only when it is needed. + * @author Keith Donald + * @since 3.0 + * @see MessageBuilder + */ +public interface DefaultMessageFactory { + + /** + * Create the default message. + */ + String createDefaultMessage(); +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageResolver.java b/org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageResolver.java new file mode 100644 index 00000000000..a3b6e39bcec --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/DefaultMessageResolver.java @@ -0,0 +1,153 @@ +/* + * 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.context.message; + +import java.util.Locale; +import java.util.Map; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.core.style.ToStringCreator; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +final class DefaultMessageResolver implements MessageResolver, MessageSourceResolvable { + + private String[] codes; + + private Map args; + + private DefaultMessageFactory defaultMessageFactory; + + private ExpressionParser expressionParser; + + public DefaultMessageResolver(String[] codes, Map args, + DefaultMessageFactory defaultMessageFactory, ExpressionParser expressionParser) { + this.codes = codes; + this.args = args; + this.defaultMessageFactory = defaultMessageFactory; + this.expressionParser = expressionParser; + } + + // implementing MessageResolver + + public String resolveMessage(MessageSource messageSource, Locale locale) { + if (messageSource == null) { + if (defaultMessageFactory != null) { + return defaultMessageFactory.createDefaultMessage(); + } 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, e); + } + Expression message; + try { + message = expressionParser.parseExpression(messageString, ParserContext.TEMPLATE_EXPRESSION); + } catch (ParseException e) { + throw new MessageResolutionException("Failed to parse message expression", e); + } + try { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(args); + context.addPropertyAccessor(new MessageArgumentAccessor(messageSource, locale)); + return (String) message.getValue(context); + } catch (EvaluationException e) { + throw new MessageResolutionException("Failed to evaluate message expression '" + + message.getExpressionString() + "' to generate final message text", e); + } + } + + // implementing MessageSourceResolver + + public String[] getCodes() { + return codes; + } + + public Object[] getArguments() { + return null; + } + + public String getDefaultMessage() { + return defaultMessageFactory.createDefaultMessage(); + } + + public String toString() { + return new ToStringCreator(this).append("codes", codes).append("args", args).append("defaultMessageFactory", + defaultMessageFactory).toString(); + } + + @SuppressWarnings("unchecked") + static class MessageArgumentAccessor implements PropertyAccessor { + + private MessageSource messageSource; + + private Locale locale; + + public MessageArgumentAccessor(MessageSource messageSource, Locale locale) { + this.messageSource = messageSource; + this.locale = locale; + } + + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + Map map = (Map) target; + Object o = map.get(name); + if (o == null) { + throw new AccessException("No message argument named '" + name + + "' is defined in the argument map; arguments available are " + map.keySet(), null); + } + if (o instanceof MessageSourceResolvable) { + String message = messageSource.getMessage((MessageSourceResolvable) o, locale); + return new TypedValue(message); + } else { + return new TypedValue(o); + } + } + + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + throw new UnsupportedOperationException("Should not be called"); + } + + public Class[] getSpecificTargetClasses() { + return new Class[] { Map.class }; + } + + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/MessageBuilder.java b/org.springframework.context/src/main/java/org/springframework/context/message/MessageBuilder.java new file mode 100644 index 00000000000..182be22b1a8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/MessageBuilder.java @@ -0,0 +1,148 @@ +/* + * 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.context.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}. + *

+ * Usage example: + *

+ * String message = new MessageBuilder(messageSource).
+ *     code("invalidFormat").
+ *     arg("label", new ResolvableArgument("mathForm.decimalField")).
+ *     arg("format", "#,###.##").
+ *     defaultMessage("The decimal field must be in format #,###.##").
+ *     build();
+ * 
+ * Example messages.properties loaded by the MessageSource: + *
+ * invalidFormat=The #{label} must be in format #{format}.
+ * mathForm.decimalField=Decimal Field
+ * 
+ * @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 messageResolverBuilder = 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) { + messageResolverBuilder.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) { + messageResolverBuilder.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) { + messageResolverBuilder.defaultMessage(message); + 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(DefaultMessageFactory defaultMessageFactory) { + messageResolverBuilder.defaultMessage(defaultMessageFactory); + 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 messageResolverBuilder.build().resolveMessage(messageSource, getLocale()); + } + + // internal helpers + + private Locale getLocale() { + if (locale != null) { + return locale; + } else { + return LocaleContextHolder.getLocale(); + } + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolutionException.java b/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolutionException.java new file mode 100644 index 00000000000..17beff23293 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolutionException.java @@ -0,0 +1,43 @@ +/* + * 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.context.message; + +/** + * Runtime exception thrown by a {@link MessageResolver} if a message resolution fails. + * @author Keith Donald + * @since 3.0 + */ +@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 + * @param cause the cause of the failure + */ + public MessageResolutionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolver.java b/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolver.java new file mode 100644 index 00000000000..c22414394a5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolver.java @@ -0,0 +1,38 @@ +/* + * 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.context.message; + +import java.util.Locale; + +import org.springframework.context.MessageSource; + +/** + * A factory for a localized message resolved from a MessageSource. + * @author Keith Donald + * @since 3.0 + * @see MessageSource + */ +public interface MessageResolver { + + /** + * Resolve the message from the message source for the locale. + * @param messageSource the message source, an abstraction for a resource bundle + * @param locale the locale of this request + * @return the resolved message + * @throws MessageResolutionException if a resolution failure occurs + */ + public String resolveMessage(MessageSource messageSource, Locale locale); +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolverBuilder.java b/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolverBuilder.java new file mode 100644 index 00000000000..3c63e30ef45 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/MessageResolverBuilder.java @@ -0,0 +1,130 @@ +/* + * 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.context.message; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Builds a {@link MessageResolver} that can resolve 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}. + *

+ * Usage example: + *

+ * MessageResolver resolver = new MessageResolverBuilder().
+ *     code("invalidFormat").
+ *     arg("label", new ResolvableArgument("mathForm.decimalField")).
+ *     arg("format", "#,###.##").
+ *     defaultMessage("The decimal field must be in format #,###.##").
+ *     build();
+ * String message = resolver.resolveMessage(messageSource, locale);
+ * 
+ * Example messages.properties loaded by the MessageSource: + *
+ * invalidFormat=The #{label} must be in format #{format}.
+ * mathForm.decimalField=Decimal Field
+ * 
+ * @author Keith Donald + * @since 3.0 + * @see #code(String) + * @see #arg(String, Object) + * @see #defaultMessage(String) + */ +public class MessageResolverBuilder { + + private Set codes = new LinkedHashSet(); + + private Map args = new LinkedHashMap(); + + private DefaultMessageFactory defaultMessageFactory; + + private ExpressionParser expressionParser = new SpelExpressionParser(); + + /** + * 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 MessageResolverBuilder code(String code) { + codes.add(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 passed to + * {@link MessageResolver#resolveMessage(org.springframework.context.MessageSource, java.util.Locale)}. + * @param name the argument name + * @param value the argument value + * @return this, for fluent API usage + * @see ResolvableArgument + */ + public MessageResolverBuilder arg(String name, Object value) { + args.put(name, value); + return this; + } + + /** + * Set the default message. + * If the MessageResolver has no codes to try, this will be used as the message. + * If the MessageResolver has 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 MessageResolverBuilder defaultMessage(String message) { + return defaultMessage(new StaticDefaultMessageFactory(message)); + } + + /** + * Set the default message. + * If the MessageResolver has no codes to try, this will be used as the message. + * If the MessageResolver has 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 MessageResolverBuilder defaultMessage(DefaultMessageFactory defaultMessageFactory) { + this.defaultMessageFactory = defaultMessageFactory; + 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 MessageResolver build() { + if (codes == null && defaultMessageFactory == null) { + throw new IllegalStateException( + "A message code or the message text is required to build this message resolver"); + } + String[] codesArray = (String[]) codes.toArray(new String[codes.size()]); + return new DefaultMessageResolver(codesArray, args, defaultMessageFactory, expressionParser); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/ResolvableArgument.java b/org.springframework.context/src/main/java/org/springframework/context/message/ResolvableArgument.java new file mode 100644 index 00000000000..52ede533982 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/ResolvableArgument.java @@ -0,0 +1,56 @@ +/* + * 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.context.message; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.core.style.ToStringCreator; + +/** + * A message argument value that is resolved from a MessageSource. + * Allows the value to be localized. + * @see MessageSource + * @author Keith Donald + */ +public class ResolvableArgument implements MessageSourceResolvable { + + private String code; + + /** + * Creates a resolvable argument. + * @param code the code that will be used to lookup the argument value from the message source + */ + public ResolvableArgument(String code) { + this.code = code; + } + + public String[] getCodes() { + return new String[] { code.toString() }; + } + + public Object[] getArguments() { + return null; + } + + public String getDefaultMessage() { + return String.valueOf(code); + } + + public String toString() { + return new ToStringCreator(this).append("code", code).toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/StaticDefaultMessageFactory.java b/org.springframework.context/src/main/java/org/springframework/context/message/StaticDefaultMessageFactory.java new file mode 100644 index 00000000000..338d4d11112 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/StaticDefaultMessageFactory.java @@ -0,0 +1,30 @@ +/* + * 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.context.message; + +class StaticDefaultMessageFactory implements DefaultMessageFactory { + + private String defaultMessage; + + public StaticDefaultMessageFactory(String defaultMessage) { + this.defaultMessage = defaultMessage; + } + + public String createDefaultMessage() { + return defaultMessage; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/message/package-info.java b/org.springframework.context/src/main/java/org/springframework/context/message/package-info.java new file mode 100644 index 00000000000..3400850824b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/message/package-info.java @@ -0,0 +1,5 @@ +/** + * An API for creating localized messages. + */ +package org.springframework.context.message; + diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/Binder.java b/org.springframework.context/src/main/java/org/springframework/model/binder/Binder.java new file mode 100644 index 00000000000..0c5b7a31ca2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/Binder.java @@ -0,0 +1,37 @@ +/* + * 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.model.binder; + +import java.util.Map; + +/** + * Bind to fields of a model object. + * @author Keith Donald + * @since 3.0 + * @param The type of model this binder binds to + */ +public interface Binder { + + /** + * Bind submitted field values. + * @param fieldValues the field values to bind + * @param model the model to bind to + * @return the results of the binding operation + * @throws MissingFieldException when the fieldValues Map is missing required fields + */ + BindingResults bind(Map fieldValues, M model); + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/BindingResult.java b/org.springframework.context/src/main/java/org/springframework/model/binder/BindingResult.java new file mode 100644 index 00000000000..95fe030e557 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/BindingResult.java @@ -0,0 +1,53 @@ +/* + * 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.model.binder; + +import org.springframework.context.alert.Alert; + +/** + * The result of a bind operation. + * @author Keith Donald + * @since 3.0 + * @see Binder#bind(java.util.Map, Object) + */ +public interface BindingResult { + + /** + * The name of the field this binding result is for. + * @see Binder#getNested(String) + */ + String getFieldName(); + + /** + * The raw submitted value for which binding was attempted. + * If not a failure, this value was successfully bound to the model. + * @see #isFailure() + */ + Object getSubmittedValue(); + + /** + * Indicates if the binding failed. + */ + boolean isFailure(); + + /** + * Gets the alert for this binding result, appropriate for rendering the result to the user. + * An alert describing a successful binding will have info severity. + * An alert describing a failed binding will have either warning, error, or fatal severity. + */ + Alert getAlert(); + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/BindingResults.java b/org.springframework.context/src/main/java/org/springframework/model/binder/BindingResults.java new file mode 100644 index 00000000000..99437e50bb0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/BindingResults.java @@ -0,0 +1,68 @@ +/* + * 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.model.binder; + +import java.util.List; + +import org.springframework.context.alert.Severity; + +/** + * The results of a bind operation. + * @author Keith Donald + * @since 3.0 + * @see Binder#bind(java.util.Map, Object) + */ +public interface BindingResults extends Iterable { + + /** + * The subset of BindingResults that were successful. + */ + List successes(); + + /** + * The subset of BindingResults that failed. + */ + List failures(); + + /** + * If there is at least one failure with a Severity equal to or greater than {@link Severity#ERROR}. + * @see BindingResults#failures() + */ + boolean hasErrors(); + + /** + * The subset of BindingResults that failed with {@link Severity#ERROR} or greater. + */ + List errors(); + + /** + * The total number of results. + */ + int size(); + + /** + * The BindingResult at the specified index. + * @throws IndexOutOfBoundsException if the index is out of bounds + */ + BindingResult get(int index); + + /** + * The BindingResult for the specified field. + * Returns null if no result exists for the fieldName specified. + */ + BindingResult get(String fieldName); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/MissingFieldException.java b/org.springframework.context/src/main/java/org/springframework/model/binder/MissingFieldException.java new file mode 100644 index 00000000000..89c99944e7f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/MissingFieldException.java @@ -0,0 +1,58 @@ +/* + * 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.model.binder; + +import java.util.List; +import java.util.Map; + +/** + * Exception thrown by a Binder when a required source value is missing unexpectedly from the sourceValues map. + * Indicates a client configuration error. + * @author Keith Donald + * @since 3.0 + * @see Binder#bind(Map, Object) + */ +@SuppressWarnings("serial") +public class MissingFieldException extends RuntimeException { + + private List missing; + + /** + * Creates a new missing field exceptions. + * @param missing + * @param fieldValues + */ + public MissingFieldException(List missing, Map fieldValues) { + super(getMessage(missing, fieldValues)); + this.missing = missing; + } + + /** + * The names of the fields that are missing. + */ + public List getMissing() { + return missing; + } + + private static String getMessage(List missingRequired, Map sourceValues) { + if (missingRequired.size() == 1) { + return "Missing a field [" + missingRequired.get(0) + "]; fieldValues map contained " + sourceValues.keySet(); + } else { + return "Missing fields " + missingRequired + "; fieldValues map contained " + sourceValues.keySet(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/package-info.java b/org.springframework.context/src/main/java/org/springframework/model/binder/package-info.java new file mode 100644 index 00000000000..58ac5a552e9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/package-info.java @@ -0,0 +1,5 @@ +/** + * API for binding submitted field values in a single batch operation. + */ +package org.springframework.model.binder; + diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/AbstractBinder.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/AbstractBinder.java new file mode 100644 index 00000000000..dca5489c8d2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/AbstractBinder.java @@ -0,0 +1,127 @@ +/* + * 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.model.binder.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.context.MessageSource; +import org.springframework.model.binder.Binder; +import org.springframework.model.binder.BindingResult; +import org.springframework.model.binder.BindingResults; +import org.springframework.model.binder.MissingFieldException; +import org.springframework.util.Assert; + +/** + * Base Binder implementation that defines common structural elements. + * Subclasses should be parameterized & implement {@link #bind(Map, Object)}. + * @author Keith Donald + * @since 3.0 + * @see #setRequiredFields(String[]) + * @see #setMessageSource(MessageSource) + * @see #createFieldBinder() + * @see #bind(Map, Object) + */ +public abstract class AbstractBinder implements Binder { + + private MessageSource messageSource; + + private String[] requiredFields; + + /** + * Configure the fields for which values must be present in each bind attempt. + * @param fieldNames the required field names + * @see MissingFieldException + */ + public void setRequiredFields(String[] fieldNames) { + this.requiredFields = fieldNames; + } + + /** + * Configure the MessageSource that resolves localized {@link BindingResult} alert messages. + * @param messageSource the message source + */ + public void setMessageSource(MessageSource messageSource) { + Assert.notNull(messageSource, "The MessageSource is required"); + this.messageSource = messageSource; + } + + /** + * The configured MessageSource that resolves binding result alert messages. + */ + protected MessageSource getMessageSource() { + return messageSource; + } + + // Binder implementation + + public final BindingResults bind(Map fieldValues, M model) { + fieldValues = filter(fieldValues, model); + checkRequired(fieldValues); + FieldBinder fieldBinder = createFieldBinder(model); + ArrayListBindingResults results = new ArrayListBindingResults(fieldValues.size()); + for (Map.Entry fieldValue : fieldValues.entrySet()) { + results.add(fieldBinder.bind(fieldValue.getKey(), fieldValue.getValue())); + } + return results; + } + + // subclass hooks + + /** + * Subclasses must implement this method to create the {@link FieldBinder} + * instance for the given model. + */ + protected abstract FieldBinder createFieldBinder(M model); + + /** + * Filter the fields to bind. + * Allows for pre-processing the fieldValues Map before any binding occurs. + * For example, you might insert empty or default values for fields that are not present. + * As another example, you might collapse multiple fields into a single field. + * Default implementation simply returns the fieldValues Map unchanged. + * @param fieldValues the original fieldValues Map provided by the caller + * @return the filtered fieldValues Map that will be used to bind + */ + protected Map filter(Map fieldValues, M model) { + return fieldValues; + } + + // internal helpers + + private void checkRequired(Map fieldValues) { + if (requiredFields == null) { + return; + } + List missingRequired = new ArrayList(); + for (String required : requiredFields) { + boolean found = false; + for (String property : fieldValues.keySet()) { + if (property.equals(required)) { + found = true; + } + } + if (!found) { + missingRequired.add(required); + } + } + if (!missingRequired.isEmpty()) { + throw new MissingFieldException(missingRequired, fieldValues); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/AlertBindingResult.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/AlertBindingResult.java new file mode 100644 index 00000000000..c4bb48ee186 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/AlertBindingResult.java @@ -0,0 +1,56 @@ +/* + * 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.model.binder.support; + +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.Severity; +import org.springframework.model.binder.BindingResult; + +public class AlertBindingResult implements BindingResult { + + private String fieldName; + + private Object submittedValue; + + private Alert alert; + + public AlertBindingResult(String fieldName, Object sourceValue, Alert alert) { + this.fieldName = fieldName; + this.submittedValue = sourceValue; + this.alert = alert; + } + + public String getFieldName() { + return fieldName; + } + + public Object getSubmittedValue() { + return submittedValue; + } + + public boolean isFailure() { + return alert.getSeverity().compareTo(Severity.INFO) > 1; + } + + public Alert getAlert() { + return alert; + } + + public String toString() { + return getAlert().toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/ArrayListBindingResults.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/ArrayListBindingResults.java new file mode 100644 index 00000000000..17bf526bd2c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/ArrayListBindingResults.java @@ -0,0 +1,104 @@ +/* + * 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.model.binder.support; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.context.alert.Severity; +import org.springframework.model.binder.BindingResult; +import org.springframework.model.binder.BindingResults; + +class ArrayListBindingResults implements BindingResults { + + private List results; + + public ArrayListBindingResults() { + results = new ArrayList(); + } + + public ArrayListBindingResults(int size) { + results = new ArrayList(size); + } + + public void add(BindingResult result) { + results.add(result); + } + + // implementing Iterable + + public Iterator iterator() { + return results.iterator(); + } + + // implementing BindingResults + + public List successes() { + List results = new ArrayList(); + for (BindingResult result : this) { + if (!result.isFailure()) { + results.add(result); + } + } + return results; + } + + public List failures() { + List results = new ArrayList(); + for (BindingResult result : this) { + if (result.isFailure()) { + results.add(result); + } + } + return results; + } + + public boolean hasErrors() { + return errors().size() > 0; + } + + public List errors() { + List results = new ArrayList(); + for (BindingResult result : this) { + if (result.isFailure() && result.getAlert().getSeverity().compareTo(Severity.ERROR) >= 0) { + results.add(result); + } + } + return results; + } + + public BindingResult get(int index) { + return results.get(index); + } + + public BindingResult get(String fieldName) { + for (BindingResult result : results) { + if (result.getFieldName().equals(fieldName)) { + return result; + } + } + return null; + } + + public int size() { + return results.size(); + } + + public String toString() { + return "[BindingResults = " + results.toString() + "]"; + } +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldBinder.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldBinder.java new file mode 100644 index 00000000000..bb3b2668c2c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldBinder.java @@ -0,0 +1,35 @@ +/* + * 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.model.binder.support; + +import org.springframework.model.binder.BindingResult; + +/** + * Binder callback interface for binding a single field value. + * @author Keith Donald + * @since 3.0 + * @see AbstractBinder#createFieldBinder(Object) + */ +public interface FieldBinder { + + /** + * Bind a single field. + * @param fieldName the field name + * @param value the field value + * @return the binding result + */ + BindingResult bind(String fieldName, Object value); +} diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotEditableResult.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotEditableResult.java new file mode 100644 index 00000000000..65d4fce8fce --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotEditableResult.java @@ -0,0 +1,86 @@ +/* + * 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.model.binder.support; + +import org.springframework.context.MessageSource; +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.Severity; +import org.springframework.context.message.DefaultMessageFactory; +import org.springframework.context.message.MessageBuilder; +import org.springframework.context.message.ResolvableArgument; +import org.springframework.model.binder.BindingResult; + +/** + * Indicates a field failed to bind because it was not editable/writeable. + * @author Keith Donald + * @since 3.0 + */ +public class FieldNotEditableResult implements BindingResult { + + private String fieldName; + + private Object submittedValue; + + private MessageSource messageSource; + + public FieldNotEditableResult(String fieldName, Object submittedValue, MessageSource messageSource) { + this.fieldName = fieldName; + this.submittedValue = submittedValue; + this.messageSource = messageSource; + } + + public String getFieldName() { + return fieldName; + } + + public Object getSubmittedValue() { + return submittedValue; + } + + public boolean isFailure() { + return true; + } + + public Alert getAlert() { + return new Alert() { + public String getCode() { + return "fieldNotEditable"; + } + + public Severity getSeverity() { + return Severity.WARNING; + } + + public String getMessage() { + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code(getCode()); + builder.arg("label", new ResolvableArgument(fieldName)); + builder.arg("value", submittedValue); + builder.defaultMessage(new DefaultMessageFactory() { + public String createDefaultMessage() { + return "Failed to bind submitted value " + submittedValue + "; field '" + fieldName + "' is not editable"; + } + }); + return builder.build(); + } + }; + } + + public String toString() { + return getAlert().toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotFoundResult.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotFoundResult.java new file mode 100644 index 00000000000..499c52b0066 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/FieldNotFoundResult.java @@ -0,0 +1,86 @@ +/* + * 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.model.binder.support; + +import org.springframework.context.MessageSource; +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.Severity; +import org.springframework.context.message.DefaultMessageFactory; +import org.springframework.context.message.MessageBuilder; +import org.springframework.context.message.ResolvableArgument; +import org.springframework.model.binder.BindingResult; + +/** + * Indicates field failed to bind because it was not found. + * @author Keith Donald + * @since 3.0 + */ +public class FieldNotFoundResult implements BindingResult { + + private String fieldName; + + private Object submittedValue; + + private MessageSource messageSource; + + public FieldNotFoundResult(String fieldName, Object submittedValue, MessageSource messageSource) { + this.fieldName = fieldName; + this.submittedValue = submittedValue; + this.messageSource = messageSource; + } + + public String getFieldName() { + return fieldName; + } + + public Object getSubmittedValue() { + return submittedValue; + } + + public boolean isFailure() { + return true; + } + + public Alert getAlert() { + return new Alert() { + public String getCode() { + return "fieldNotFound"; + } + + public Severity getSeverity() { + return Severity.WARNING; + } + + public String getMessage() { + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code(getCode()); + builder.arg("label", new ResolvableArgument(fieldName)); + builder.arg("value", submittedValue); + builder.defaultMessage(new DefaultMessageFactory() { + public String createDefaultMessage() { + return "Failed to bind submitted value " + submittedValue + "; no field '" + fieldName + "' found"; + } + }); + return builder.build(); + } + }; + } + + public String toString() { + return getAlert().toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/GenericBinder.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/GenericBinder.java new file mode 100644 index 00000000000..d9bc24a27dd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/GenericBinder.java @@ -0,0 +1,212 @@ +/* + * 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.model.binder.support; + +import org.springframework.context.MessageSource; +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.Severity; +import org.springframework.context.expression.MapAccessor; +import org.springframework.context.message.DefaultMessageFactory; +import org.springframework.context.message.MessageBuilder; +import org.springframework.context.message.ResolvableArgument; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParserConfiguration; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.model.binder.Binder; +import org.springframework.model.binder.BindingResult; + +/** + * A {@link Binder} implementation that accepts any target object and uses + * Spring's Expression Language (SpEL) to evaluate the keys in the field value Map. + * @author Mark Fisher + * @author Keith Donald + * @since 3.0 + */ +public class GenericBinder extends AbstractBinder { + + private final ExpressionParser expressionParser = new SpelExpressionParser( + SpelExpressionParserConfiguration.CreateObjectIfAttemptToReferenceNull + | SpelExpressionParserConfiguration.GrowListsOnIndexBeyondSize); + + @Override + protected FieldBinder createFieldBinder(Object model) { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.addPropertyAccessor(new MapAccessor()); + context.setRootObject(model); + return new ExpressionFieldBinder(getMessageSource(), expressionParser, context); + } + + private static class ExpressionFieldBinder implements FieldBinder { + + private final MessageSource messageSource; + + private final ExpressionParser expressionParser; + + private final EvaluationContext evaluationContext; + + private ExpressionFieldBinder(MessageSource messageSource, ExpressionParser expressionParser, + EvaluationContext evaluationContext) { + this.messageSource = messageSource; + this.expressionParser = expressionParser; + this.evaluationContext = evaluationContext; + } + + public BindingResult bind(String fieldName, Object value) { + Alert alert = null; + try { + Expression exp = expressionParser.parseExpression(fieldName); + if (!exp.isWritable(evaluationContext)) { + return new FieldNotEditableResult(fieldName, value, messageSource); + } + exp.setValue(evaluationContext, value); + alert = new BindSuccessAlert(fieldName, value, messageSource); + } catch (ParseException e) { + alert = new InternalErrorAlert(e); + } catch (EvaluationException e) { + SpelEvaluationException spelException = (SpelEvaluationException) e; + if (spelException.getMessageCode() == SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE) { + ConversionFailedException conversionFailure = findConversionFailureCause(spelException); + if (conversionFailure != null) { + alert = new TypeMismatchAlert(fieldName, value, conversionFailure, messageSource); + } + } + if (alert == null) { + alert = new InternalErrorAlert(e); + } + } + return new AlertBindingResult(fieldName, value, alert); + } + + private ConversionFailedException findConversionFailureCause(Exception e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof ConversionFailedException) { + return (ConversionFailedException) cause; + } + cause = cause.getCause(); + } + return null; + } + + } + + private static class BindSuccessAlert implements Alert { + + private final String fieldName; + + private final Object value; + + private MessageSource messageSource; + + public BindSuccessAlert(String fieldName, Object value, MessageSource messageSource) { + this.fieldName = fieldName; + this.value = value; + this.messageSource = messageSource; + } + + public String getCode() { + return "bindSuccess"; + } + + public String getMessage() { + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code(getCode()); + builder.arg("label", new ResolvableArgument(fieldName)); + builder.arg("value", value); + builder.defaultMessage(new DefaultMessageFactory() { + public String createDefaultMessage() { + return "Successfully bound submitted value " + value + " to field '" + fieldName + "'"; + } + }); + return builder.build(); + } + + public Severity getSeverity() { + return Severity.INFO; + } + } + + private static class TypeMismatchAlert implements Alert { + + private final String fieldName; + + private final Object value; + + private final ConversionFailedException cause; + + private MessageSource messageSource; + + public TypeMismatchAlert(String fieldName, Object value, ConversionFailedException cause, + MessageSource messageSource) { + this.fieldName = fieldName; + this.value = value; + this.cause = cause; + this.messageSource = messageSource; + } + + public String getCode() { + return "typeMismatch"; + } + + public String getMessage() { + MessageBuilder builder = new MessageBuilder(messageSource); + builder.code(getCode()); + builder.arg("label", new ResolvableArgument(fieldName)); + builder.arg("value", value); + builder.defaultMessage(new DefaultMessageFactory() { + public String createDefaultMessage() { + return "Failed to bind submitted value " + value + " to field '" + fieldName + + "'; value could not be converted to type [" + cause.getTargetType().getName() + "]"; + } + }); + return builder.build(); + } + + public Severity getSeverity() { + return Severity.ERROR; + } + } + + private static class InternalErrorAlert implements Alert { + + private final Exception cause; + + public InternalErrorAlert(Exception cause) { + this.cause = cause; + } + + public String getCode() { + return "internalError"; + } + + public String getMessage() { + return "An internal error occurred; message = [" + cause.getMessage() + "]"; + } + + public Severity getSeverity() { + return Severity.FATAL; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/model/binder/support/package-info.java b/org.springframework.context/src/main/java/org/springframework/model/binder/support/package-info.java new file mode 100644 index 00000000000..7e0cd0f68fd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/model/binder/support/package-info.java @@ -0,0 +1,5 @@ +/** + * Binder API implementation support. + */ +package org.springframework.model.binder.support; + diff --git a/org.springframework.context/src/test/java/org/springframework/context/alert/AlertsTests.java b/org.springframework.context/src/test/java/org/springframework/context/alert/AlertsTests.java new file mode 100644 index 00000000000..3a68564125f --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/alert/AlertsTests.java @@ -0,0 +1,34 @@ +package org.springframework.context.alert; + +import static org.junit.Assert.assertEquals; +import static org.springframework.context.alert.Alerts.error; +import static org.springframework.context.alert.Alerts.fatal; +import static org.springframework.context.alert.Alerts.info; +import static org.springframework.context.alert.Alerts.warning; + +import org.junit.Test; +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.Severity; + +public class AlertsTests { + + @Test + public void testFactoryMethods() { + Alert a1 = info("alert 1"); + assertEquals(Severity.INFO, a1.getSeverity()); + assertEquals("alert 1", a1.getMessage()); + + Alert a2 = warning("alert 2"); + assertEquals(Severity.WARNING, a2.getSeverity()); + assertEquals("alert 2", a2.getMessage()); + + Alert a3 = error("alert 3"); + assertEquals(Severity.ERROR, a3.getSeverity()); + assertEquals("alert 3", a3.getMessage()); + + Alert a4 = fatal("alert 4"); + assertEquals(Severity.FATAL, a4.getSeverity()); + assertEquals("alert 4", a4.getMessage()); + + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/alert/support/DefaultAlertContextTests.java b/org.springframework.context/src/test/java/org/springframework/context/alert/support/DefaultAlertContextTests.java new file mode 100644 index 00000000000..9b7eb7fc4d0 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/alert/support/DefaultAlertContextTests.java @@ -0,0 +1,39 @@ +package org.springframework.context.alert.support; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.alert.Alert; +import org.springframework.context.alert.Severity; +import org.springframework.context.alert.support.DefaultAlertContext; + +public class DefaultAlertContextTests { + + private DefaultAlertContext context; + + @Before + public void setUp() { + context = new DefaultAlertContext(); + } + + @Test + public void addAlert() { + Alert alert = new Alert() { + public String getCode() { + return "invalidFormat"; + } + + public String getMessage() { + return "Please enter a value in format yyyy-dd-mm"; + } + + public Severity getSeverity() { + return Severity.ERROR; + } + }; + context.add("form.property", alert); + assertEquals(1, context.getAlerts().size()); + assertEquals("invalidFormat", context.getAlerts("form.property").get(0).getCode()); + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/message/MessageBuilderTests.java b/org.springframework.context/src/test/java/org/springframework/context/message/MessageBuilderTests.java new file mode 100644 index 00000000000..0d1ce50289f --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/message/MessageBuilderTests.java @@ -0,0 +1,23 @@ +package org.springframework.context.message; + +import static org.junit.Assert.assertEquals; + +import java.util.Locale; + +import org.junit.Test; +import org.springframework.context.message.MessageBuilder; +import org.springframework.context.message.ResolvableArgument; + +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", "#,###.##").locale(Locale.US).defaultMessage("Field must be in format #,###.##").build(); + assertEquals("Decimal Field must be in format #,###.##", message); + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/message/MessageResolverBuilderTests.java b/org.springframework.context/src/test/java/org/springframework/context/message/MessageResolverBuilderTests.java new file mode 100644 index 00000000000..a8e9ab5762c --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/message/MessageResolverBuilderTests.java @@ -0,0 +1,26 @@ +package org.springframework.context.message; + +import static org.junit.Assert.assertEquals; + +import java.util.Locale; + +import org.junit.Test; +import org.springframework.context.message.MessageResolver; +import org.springframework.context.message.MessageResolverBuilder; +import org.springframework.context.message.ResolvableArgument; + +public class MessageResolverBuilderTests { + + private MessageResolverBuilder builder = new MessageResolverBuilder(); + + @Test + public void buildMessage() { + MessageResolver resolver = builder.code("invalidFormat").arg("label", new ResolvableArgument("mathForm.decimalField")) + .arg("format", "#,###.##").defaultMessage("Field must be in format #,###.##").build(); + MockMessageSource messageSource = new MockMessageSource(); + messageSource.addMessage("invalidFormat", Locale.US, "#{label} must be in format #{format}"); + messageSource.addMessage("mathForm.decimalField", Locale.US, "Decimal Field"); + String message = resolver.resolveMessage(messageSource, Locale.US); + assertEquals("Decimal Field must be in format #,###.##", message); + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/message/MockMessageSource.java b/org.springframework.context/src/test/java/org/springframework/context/message/MockMessageSource.java new file mode 100644 index 00000000000..cf58537ab2e --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/message/MockMessageSource.java @@ -0,0 +1,47 @@ +package org.springframework.context.message; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.springframework.context.support.AbstractMessageSource; +import org.springframework.util.Assert; + +public class MockMessageSource extends AbstractMessageSource { + + /** Map from 'code + locale' keys to message Strings */ + private final Map messages = new HashMap(); + + @Override + protected MessageFormat resolveCode(String code, Locale locale) { + throw new IllegalStateException("Should not be called"); + } + + @Override + protected String resolveCodeWithoutArguments(String code, Locale locale) { + return this.messages.get(code + "_" + locale.toString()); + } + + /** + * Associate the given message with the given code. + * @param code the lookup code + * * @param locale the locale that the message should be found within + * @param msg the message associated with this lookup code + */ + public void addMessage(String code, Locale locale, String msg) { + Assert.notNull(code, "Code must not be null"); + Assert.notNull(locale, "Locale must not be null"); + Assert.notNull(msg, "Message must not be null"); + this.messages.put(code + "_" + locale.toString(), msg); + if (logger.isDebugEnabled()) { + logger.debug("Added message [" + msg + "] for code [" + code + "] and Locale [" + locale + "]"); + } + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.messages; + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/model/binder/support/GenericBinderTests.java b/org.springframework.context/src/test/java/org/springframework/model/binder/support/GenericBinderTests.java new file mode 100644 index 00000000000..525a855d211 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/model/binder/support/GenericBinderTests.java @@ -0,0 +1,261 @@ +/* + * 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.model.binder.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +import org.junit.Test; +import org.springframework.context.alert.Severity; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.message.MockMessageSource; +import org.springframework.core.style.ToStringCreator; +import org.springframework.model.binder.Binder; +import org.springframework.model.binder.BindingResults; + +/** + * @author Mark Fisher + * @since 3.0 + */ +public class GenericBinderTests { + + @Test + public void simpleValues() { + Person person = new Person(); + Map map = new LinkedHashMap(); + map.put("name", "John Doe"); + map.put("age", 42); + map.put("male", true); + Binder binder = new GenericBinder(); + + BindingResults results = binder.bind(map, person); + assertEquals(3, results.size()); + assertEquals(3, results.successes().size()); + assertEquals(0, results.failures().size()); + assertEquals(0, results.errors().size()); + assertEquals("name", results.get(0).getFieldName()); + assertEquals("John Doe", results.get(0).getSubmittedValue()); + assertEquals(false, results.get(0).isFailure()); + assertEquals(Severity.INFO, results.get(0).getAlert().getSeverity()); + assertEquals("bindSuccess", results.get(0).getAlert().getCode()); + assertEquals("Successfully bound submitted value John Doe to field 'name'", results.get(0).getAlert().getMessage()); + assertEquals("name", results.get("name").getFieldName()); + assertEquals("John Doe", results.get("name").getSubmittedValue()); + + assertEquals("John Doe", person.name); + assertEquals(42, person.age); + assertTrue(person.male); + } + + @Test + public void nestedValues() { + Person person = new Person(); + Map map = new HashMap(); + map.put("pob.city", "Rome"); + map.put("pob.country", "Italy"); + Binder binder = new GenericBinder(); + binder.bind(map, person); + assertNotNull(person.pob); + assertEquals("Rome", person.pob.city); + assertEquals("Italy", person.pob.country); + } + + @Test + public void mapValues() { + Person person = new Person(); + Map map = new HashMap(); + map.put("jobHistory['0']", "Clerk"); + map.put("jobHistory['1']", "Plumber"); + Binder binder = new GenericBinder(); + binder.bind(map, person); + assertEquals("Clerk", person.jobHistory.get(0)); + assertEquals("Plumber", person.jobHistory.get(1)); + } + + @Test + public void typeMismatch() { + Person person = new Person(); + Map map = new HashMap(); + map.put("male", "bogus"); + Binder binder = new GenericBinder(); + BindingResults results = binder.bind(map, person); + assertEquals(1, results.size()); + assertEquals(0, results.successes().size()); + assertEquals(1, results.failures().size()); + assertEquals(1, results.errors().size()); + assertEquals("bogus", results.get(0).getSubmittedValue()); + assertEquals("typeMismatch", results.get(0).getAlert().getCode()); + assertEquals(Severity.ERROR, results.get(0).getAlert().getSeverity()); + assertEquals("Failed to bind submitted value bogus to field 'male'; value could not be converted to type [boolean]", results.get(0).getAlert().getMessage()); + } + + @Test + public void internalError() { + Person person = new Person(); + Map map = new HashMap(); + map.put("bogus", "bogus"); + Binder binder = new GenericBinder(); + BindingResults results = binder.bind(map, person); + assertEquals(1, results.size()); + assertEquals(0, results.successes().size()); + assertEquals(1, results.failures().size()); + assertEquals(1, results.errors().size()); + assertEquals("bogus", results.get(0).getSubmittedValue()); + assertEquals("internalError", results.get(0).getAlert().getCode()); + assertEquals(Severity.FATAL, results.get(0).getAlert().getSeverity()); + assertEquals("An internal error occurred; message = [EL1034E:(pos 0): A problem occurred whilst attempting to set the property 'bogus': Unable to access property 'bogus' through setter]", results.get(0).getAlert().getMessage()); + } + + @Test + public void fieldNotEditable() { + Person person = new Person(); + Map map = new HashMap(); + map.put("readOnly", "whatever"); + Binder binder = new GenericBinder(); + BindingResults results = binder.bind(map, person); + assertEquals(1, results.size()); + assertEquals(0, results.successes().size()); + assertEquals(1, results.failures().size()); + assertEquals(0, results.errors().size()); + assertEquals("whatever", results.get(0).getSubmittedValue()); + assertEquals("fieldNotEditable", results.get(0).getAlert().getCode()); + assertEquals(Severity.WARNING, results.get(0).getAlert().getSeverity()); + assertEquals("Failed to bind submitted value whatever; field 'readOnly' is not editable", results.get(0).getAlert().getMessage()); + } + + @Test + public void messageSource() { + Person person = new Person(); + Map map = new HashMap(); + map.put("male", "bogus"); + GenericBinder binder = new GenericBinder(); + MockMessageSource messageSource = new MockMessageSource(); + messageSource.addMessage("typeMismatch", Locale.US, "Please enter true or false for the value of the #{label} field; you entered #{value}"); + binder.setMessageSource(messageSource); + LocaleContextHolder.setLocale(Locale.US); + BindingResults results = binder.bind(map, person); + assertEquals("Please enter true or false for the value of the male field; you entered bogus", results.get(0).getAlert().getMessage()); + LocaleContextHolder.setLocale(null); + } + + public static class Person { + + private String name; + + private int age; + + private boolean male; + + private PlaceOfBirth pob; + + private Map jobHistory; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public boolean isMale() { + return male; + } + + public void setMale(boolean male) { + this.male = male; + } + + public PlaceOfBirth getPob() { + return pob; + } + + public void setPob(PlaceOfBirth pob) { + this.pob = pob; + } + + public Map getJobHistory() { + return jobHistory; + } + + public void setJobHistory(Map jobHistory) { + this.jobHistory = jobHistory; + } + + public void setBogus(String bogus) { + throw new RuntimeException("internal error"); + } + + public boolean isReadOnly() { + return true; + } + + public String toString() { + return new ToStringCreator(this) + .append("name", name) + .append("age", age) + .append("male", male) + .append("pob", pob) + .toString(); + } + } + + + public static class PlaceOfBirth { + + private String city; + + private String country; + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String toString() { + return new ToStringCreator(this) + .append("city", city) + .append("country", country) + .toString(); + } + } + +}