001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.web.servlet.support;
018:
019: import java.beans.PropertyEditor;
020: import java.util.Arrays;
021: import java.util.List;
022:
023: import org.springframework.beans.BeanWrapperImpl;
024: import org.springframework.context.NoSuchMessageException;
025: import org.springframework.util.StringUtils;
026: import org.springframework.validation.AbstractPropertyBindingResult;
027: import org.springframework.validation.BindException;
028: import org.springframework.validation.Errors;
029: import org.springframework.validation.ObjectError;
030: import org.springframework.web.util.HtmlUtils;
031:
032: /**
033: * Simple adapter to expose the bind status of a field or object.
034: * Set as a variable both by the JSP bind tag and Velocity/FreeMarker macros.
035: *
036: * <p>Obviously, object status representations (i.e. errors at the object level
037: * rather than the field level) do not have an expression and a value but only
038: * error codes and messages. For simplicity's sake and to be able to use the same
039: * tags and macros, the same status class is used for both scenarios.
040: *
041: * @author Rod Johnson
042: * @author Juergen Hoeller
043: * @author Darren Davison
044: * @see RequestContext#getBindStatus
045: * @see org.springframework.web.servlet.tags.BindTag
046: * @see org.springframework.web.servlet.view.AbstractTemplateView#setExposeSpringMacroHelpers
047: */
048: public class BindStatus {
049:
050: private final RequestContext requestContext;
051:
052: private final String path;
053:
054: private final boolean htmlEscape;
055:
056: private final String expression;
057:
058: private final Errors errors;
059:
060: private Object value;
061:
062: private Class valueType;
063:
064: private PropertyEditor editor;
065:
066: private List objectErrors;
067:
068: private String[] errorCodes;
069:
070: private String[] errorMessages;
071:
072: /**
073: * Create a new BindStatus instance, representing a field or object status.
074: * @param requestContext the current RequestContext
075: * @param path the bean and property path for which values and errors
076: * will be resolved (e.g. "customer.address.street")
077: * @param htmlEscape whether to HTML-escape error messages and string values
078: * @throws IllegalStateException if no corresponding Errors object found
079: */
080: public BindStatus(RequestContext requestContext, String path,
081: boolean htmlEscape) throws IllegalStateException {
082:
083: this .requestContext = requestContext;
084: this .path = path;
085: this .htmlEscape = htmlEscape;
086:
087: // determine name of the object and property
088: String beanName = null;
089: int dotPos = path.indexOf('.');
090: if (dotPos == -1) {
091: // property not set, only the object itself
092: beanName = path;
093: this .expression = null;
094: } else {
095: beanName = path.substring(0, dotPos);
096: this .expression = path.substring(dotPos + 1);
097: }
098:
099: this .errors = requestContext.getErrors(beanName, false);
100:
101: if (this .errors != null) {
102: // Usual case: A BindingResult is available as request attribute.
103: // Can determine error codes and messages for the given expression.
104: // Can use a custom PropertyEditor, as registered by a form controller.
105:
106: if (this .expression != null) {
107: if ("*".equals(this .expression)) {
108: this .objectErrors = this .errors.getAllErrors();
109: } else if (this .expression.endsWith("*")) {
110: this .objectErrors = this .errors
111: .getFieldErrors(this .expression);
112: } else {
113: this .objectErrors = this .errors
114: .getFieldErrors(this .expression);
115: this .value = this .errors
116: .getFieldValue(this .expression);
117: this .valueType = this .errors
118: .getFieldType(this .expression);
119: this .editor = getCustomEditor(this .errors,
120: this .expression);
121: }
122: }
123:
124: else {
125: this .objectErrors = this .errors.getGlobalErrors();
126: }
127:
128: initErrorCodes();
129: }
130:
131: else {
132: // No BindingResult available as request attribute:
133: // Probably forwarded directly to a form view.
134: // Let's do the best we can: extract a plain target if appropriate.
135:
136: Object target = requestContext.getModelObject(beanName);
137: if (target == null) {
138: throw new IllegalStateException(
139: "Neither BindingResult nor plain target object for bean name '"
140: + beanName
141: + "' available as request attribute");
142: }
143:
144: if (this .expression != null && !"*".equals(this .expression)
145: && !this .expression.endsWith("*")) {
146: BeanWrapperImpl bw = new BeanWrapperImpl(target);
147: this .valueType = bw.getPropertyType(this .expression);
148: this .value = bw.getPropertyValue(this .expression);
149: }
150:
151: this .errorCodes = new String[0];
152: this .errorMessages = new String[0];
153: }
154:
155: if (htmlEscape && this .value instanceof String) {
156: this .value = HtmlUtils.htmlEscape((String) this .value);
157: }
158: }
159:
160: /**
161: * Find a custom editor for the given field, if any.
162: * @see org.springframework.validation.AbstractPropertyBindingResult#getCustomEditor(String)
163: */
164: private PropertyEditor getCustomEditor(Errors errors, String field) {
165: Errors bindingResult = errors;
166: // Unwrap BindException for backwards compatibility.
167: if (errors instanceof BindException) {
168: bindingResult = ((BindException) errors).getBindingResult();
169: }
170: if (bindingResult instanceof AbstractPropertyBindingResult) {
171: return ((AbstractPropertyBindingResult) bindingResult)
172: .getCustomEditor(field);
173: }
174: return null;
175: }
176:
177: /**
178: * Extract the error codes from the ObjectError list.
179: */
180: private void initErrorCodes() {
181: this .errorCodes = new String[this .objectErrors.size()];
182: for (int i = 0; i < this .objectErrors.size(); i++) {
183: ObjectError error = (ObjectError) this .objectErrors.get(i);
184: this .errorCodes[i] = error.getCode();
185: }
186: }
187:
188: /**
189: * Extract the error messages from the ObjectError list.
190: */
191: private void initErrorMessages() throws NoSuchMessageException {
192: if (this .errorMessages == null) {
193: this .errorMessages = new String[this .objectErrors.size()];
194: for (int i = 0; i < this .objectErrors.size(); i++) {
195: ObjectError error = (ObjectError) this .objectErrors
196: .get(i);
197: this .errorMessages[i] = this .requestContext.getMessage(
198: error, this .htmlEscape);
199: }
200: }
201: }
202:
203: /**
204: * Return the bean and property path for which values and errors
205: * will be resolved (e.g. "customer.address.street").
206: */
207: public String getPath() {
208: return this .path;
209: }
210:
211: /**
212: * Return a bind expression that can be used in HTML forms as input name
213: * for the respective field, or <code>null</code> if not field-specific.
214: * <p>Returns a bind path appropriate for resubmission, e.g. "address.street".
215: * Note that the complete bind path as required by the bind tag is
216: * "customer.address.street", if bound to a "customer" bean.
217: */
218: public String getExpression() {
219: return this .expression;
220: }
221:
222: /**
223: * Return the current value of the field, i.e. either the property value
224: * or a rejected update, or <code>null</code> if not field-specific.
225: * <p>This value will be an HTML-escaped String if the original value
226: * already was a String.
227: */
228: public Object getValue() {
229: return this .value;
230: }
231:
232: /**
233: * Gets the '<code>Class</code>' type of the field. Favour this instead of
234: * '<code>getValue().getClass()</code>' since '<code>getValue()</code>' may
235: * return '<code>null</code>'.
236: */
237: public Class getValueType() {
238: return this .valueType;
239: }
240:
241: /**
242: * Return a suitable display value for the field, i.e. the stringified
243: * value if not null, and an empty string in case of a null value.
244: * <p>This value will be an HTML-escaped String if the original value
245: * was non-null: the <code>toString</code> result of the original value
246: * will get HTML-escaped.
247: */
248: public String getDisplayValue() {
249: if (this .value instanceof String) {
250: return (String) this .value;
251: }
252: if (this .value != null) {
253: return (this .htmlEscape ? HtmlUtils.htmlEscape(this .value
254: .toString()) : this .value.toString());
255: }
256: return "";
257: }
258:
259: /**
260: * Return if this status represents a field or object error.
261: */
262: public boolean isError() {
263: return (this .errorCodes != null && this .errorCodes.length > 0);
264: }
265:
266: /**
267: * Return the error codes for the field or object, if any.
268: * Returns an empty array instead of null if none.
269: */
270: public String[] getErrorCodes() {
271: return this .errorCodes;
272: }
273:
274: /**
275: * Return the first error codes for the field or object, if any.
276: */
277: public String getErrorCode() {
278: return (this .errorCodes.length > 0 ? this .errorCodes[0] : "");
279: }
280:
281: /**
282: * Return the resolved error messages for the field or object,
283: * if any. Returns an empty array instead of null if none.
284: */
285: public String[] getErrorMessages() {
286: initErrorMessages();
287: return this .errorMessages;
288: }
289:
290: /**
291: * Return the first error message for the field or object, if any.
292: */
293: public String getErrorMessage() {
294: initErrorMessages();
295: return (this .errorMessages.length > 0 ? this .errorMessages[0]
296: : "");
297: }
298:
299: /**
300: * Return an error message string, concatenating all messages
301: * separated by the given delimiter.
302: * @param delimiter separator string, e.g. ", " or "<br>"
303: * @return the error message string
304: */
305: public String getErrorMessagesAsString(String delimiter) {
306: initErrorMessages();
307: return StringUtils.arrayToDelimitedString(this .errorMessages,
308: delimiter);
309: }
310:
311: /**
312: * Return the Errors instance (typically a BindingResult) that this
313: * bind status is currently associated with.
314: * @return the current Errors instance, or <code>null</code> if none
315: * @see org.springframework.validation.BindingResult
316: */
317: public Errors getErrors() {
318: return this .errors;
319: }
320:
321: /**
322: * Return the PropertyEditor for the property that this bind status
323: * is currently bound to.
324: * @return the current PropertyEditor, or <code>null</code> if none
325: */
326: public PropertyEditor getEditor() {
327: return this .editor;
328: }
329:
330: public String toString() {
331: StringBuffer sb = new StringBuffer("BindStatus: ");
332: sb.append("expression=[").append(this .expression).append("]; ");
333: sb.append("value=[").append(this .value).append("]");
334: if (isError()) {
335: sb.append("; errorCodes=" + Arrays.asList(this.errorCodes));
336: }
337: return sb.toString();
338: }
339:
340: }
|