001: /*
002: * Copyright 2002-2006 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.struts;
018:
019: import java.lang.reflect.InvocationTargetException;
020: import java.util.Iterator;
021: import java.util.Locale;
022:
023: import javax.servlet.http.HttpServletRequest;
024:
025: import org.apache.commons.beanutils.BeanUtilsBean;
026: import org.apache.commons.beanutils.ConvertUtilsBean;
027: import org.apache.commons.beanutils.PropertyUtilsBean;
028: import org.apache.commons.logging.Log;
029: import org.apache.commons.logging.LogFactory;
030: import org.apache.struts.Globals;
031: import org.apache.struts.action.ActionForm;
032: import org.apache.struts.action.ActionMessage;
033: import org.apache.struts.action.ActionMessages;
034: import org.apache.struts.util.MessageResources;
035:
036: import org.springframework.context.MessageSourceResolvable;
037: import org.springframework.validation.Errors;
038: import org.springframework.validation.FieldError;
039: import org.springframework.validation.ObjectError;
040:
041: /**
042: * A thin Struts ActionForm adapter that delegates to Spring's more complete
043: * and advanced data binder and Errors object underneath the covers to bind
044: * to POJOs and manage rejected values.
045: *
046: * <p>Exposes Spring-managed errors to the standard Struts view tags, through
047: * exposing a corresponding Struts ActionMessages object as request attribute.
048: * Also exposes current field values in a Struts-compliant fashion, including
049: * rejected values (which Spring's binding keeps even for non-String fields).
050: *
051: * <p>Consequently, Struts views can be written in a completely traditional
052: * fashion (with standard <code>html:form</code>, <code>html:errors</code>, etc),
053: * seamlessly accessing a Spring-bound POJO form object underneath.
054: *
055: * <p>Note this ActionForm is designed explicitly for use in <i>request scope</i>.
056: * It expects to receive an <code>expose</code> call from the Action, passing
057: * in the Errors object to expose plus the current HttpServletRequest.
058: *
059: * <p>Example definition in <code>struts-config.xml</code>:
060: *
061: * <pre>
062: * <form-beans>
063: * <form-bean name="actionForm" type="org.springframework.web.struts.SpringBindingActionForm"/>
064: * </form-beans></pre>
065: *
066: * Example code in a custom Struts <code>Action</code>:
067: *
068: * <pre>
069: * public ActionForward execute(ActionMapping actionMapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
070: * SpringBindingActionForm form = (SpringBindingActionForm) actionForm;
071: * MyPojoBean bean = ...;
072: * ServletRequestDataBinder binder = new ServletRequestDataBinder(bean, "myPojo");
073: * binder.bind(request);
074: * form.expose(binder.getBindingResult(), request);
075: * return actionMapping.findForward("success");
076: * }</pre>
077: *
078: * This class is compatible with both Struts 1.2.x and Struts 1.1.
079: * On Struts 1.2, default messages registered with Spring binding errors
080: * are exposed when none of the error codes could be resolved.
081: * On Struts 1.1, this is not possible due to a limitation in the Struts
082: * message facility; hence, we expose the plain default error code there.
083: *
084: * @author Keith Donald
085: * @author Juergen Hoeller
086: * @since 1.2.2
087: * @see #expose(org.springframework.validation.Errors, javax.servlet.http.HttpServletRequest)
088: */
089: public class SpringBindingActionForm extends ActionForm {
090:
091: private static final Log logger = LogFactory
092: .getLog(SpringBindingActionForm.class);
093:
094: private static boolean defaultActionMessageAvailable = true;
095:
096: static {
097: // Register special PropertyUtilsBean subclass that knows how to
098: // extract field values from a SpringBindingActionForm.
099: // As a consequence of the static nature of Commons BeanUtils,
100: // we have to resort to this initialization hack here.
101: ConvertUtilsBean convUtils = new ConvertUtilsBean();
102: PropertyUtilsBean propUtils = new SpringBindingAwarePropertyUtilsBean();
103: BeanUtilsBean beanUtils = new BeanUtilsBean(convUtils,
104: propUtils);
105: BeanUtilsBean.setInstance(beanUtils);
106:
107: // Determine whether the Struts 1.2 support for default messages
108: // is available on ActionMessage: ActionMessage(String, boolean)
109: // with "false" to be passed into the boolean flag.
110: try {
111: ActionMessage.class.getConstructor(new Class[] {
112: String.class, boolean.class });
113: } catch (NoSuchMethodException ex) {
114: defaultActionMessageAvailable = false;
115: }
116: }
117:
118: private Errors errors;
119:
120: private Locale locale;
121:
122: private MessageResources messageResources;
123:
124: /**
125: * Set the Errors object that this SpringBindingActionForm is supposed
126: * to wrap. The contained field values and errors will be exposed
127: * to the view, accessible through Struts standard tags.
128: * @param errors the Spring Errors object to wrap, usually taken from
129: * a DataBinder that has been used for populating a POJO form object
130: * @param request the HttpServletRequest to retrieve the attributes from
131: * @see org.springframework.validation.DataBinder#getBindingResult()
132: */
133: public void expose(Errors errors, HttpServletRequest request) {
134: this .errors = errors;
135:
136: // Obtain the locale from Struts well-known location.
137: this .locale = (Locale) request.getSession().getAttribute(
138: Globals.LOCALE_KEY);
139:
140: // Obtain the MessageResources from Struts' well-known location.
141: this .messageResources = (MessageResources) request
142: .getAttribute(Globals.MESSAGES_KEY);
143:
144: if (errors != null && errors.hasErrors()) {
145: // Add global ActionError instances from the Spring Errors object.
146: ActionMessages actionMessages = (ActionMessages) request
147: .getAttribute(Globals.ERROR_KEY);
148: if (actionMessages == null) {
149: request.setAttribute(Globals.ERROR_KEY,
150: getActionMessages());
151: } else {
152: actionMessages.add(getActionMessages());
153: }
154: }
155: }
156:
157: /**
158: * Return an ActionMessages representation of this SpringBindingActionForm,
159: * exposing all errors contained in the underlying Spring Errors object.
160: * @see org.springframework.validation.Errors#getAllErrors()
161: */
162: private ActionMessages getActionMessages() {
163: ActionMessages actionMessages = new ActionMessages();
164: Iterator it = this .errors.getAllErrors().iterator();
165: while (it.hasNext()) {
166: ObjectError objectError = (ObjectError) it.next();
167: String effectiveMessageKey = findEffectiveMessageKey(objectError);
168: if (effectiveMessageKey == null
169: && !defaultActionMessageAvailable) {
170: // Need to specify default code despite it not being resolvable:
171: // Struts 1.1 ActionMessage doesn't support default messages.
172: effectiveMessageKey = objectError.getCode();
173: }
174: ActionMessage message = (effectiveMessageKey != null) ? new ActionMessage(
175: effectiveMessageKey, resolveArguments(objectError
176: .getArguments()))
177: : new ActionMessage(
178: objectError.getDefaultMessage(), false);
179: if (objectError instanceof FieldError) {
180: FieldError fieldError = (FieldError) objectError;
181: actionMessages.add(fieldError.getField(), message);
182: } else {
183: actionMessages.add(ActionMessages.GLOBAL_MESSAGE,
184: message);
185: }
186: }
187: if (logger.isDebugEnabled()) {
188: logger.debug("Final ActionMessages used for binding: "
189: + actionMessages);
190: }
191: return actionMessages;
192: }
193:
194: private Object[] resolveArguments(Object[] arguments) {
195: if (arguments == null || arguments.length == 0) {
196: return arguments;
197: }
198: for (int i = 0; i < arguments.length; i++) {
199: Object arg = arguments[i];
200: if (arg instanceof MessageSourceResolvable) {
201: MessageSourceResolvable resolvable = (MessageSourceResolvable) arg;
202: String[] codes = resolvable.getCodes();
203: boolean resolved = false;
204: if (this .messageResources != null) {
205: for (int j = 0; j < codes.length; j++) {
206: String code = codes[j];
207: if (this .messageResources.isPresent(
208: this .locale, code)) {
209: arguments[i] = this .messageResources
210: .getMessage(this .locale, code,
211: resolveArguments(resolvable
212: .getArguments()));
213: resolved = true;
214: break;
215: }
216: }
217: }
218: if (!resolved) {
219: arguments[i] = resolvable.getDefaultMessage();
220: }
221: }
222: }
223: return arguments;
224: }
225:
226: /**
227: * Find the most specific message key for the given error.
228: * @param error the ObjectError to find a message key for
229: * @return the most specific message key found
230: */
231: private String findEffectiveMessageKey(ObjectError error) {
232: if (this .messageResources != null) {
233: String[] possibleMatches = error.getCodes();
234: for (int i = 0; i < possibleMatches.length; i++) {
235: if (logger.isDebugEnabled()) {
236: logger.debug("Looking for error code '"
237: + possibleMatches[i] + "'");
238: }
239: if (this .messageResources.isPresent(this .locale,
240: possibleMatches[i])) {
241: if (logger.isDebugEnabled()) {
242: logger.debug("Found error code '"
243: + possibleMatches[i]
244: + "' in resource bundle");
245: }
246: return possibleMatches[i];
247: }
248: }
249: }
250: if (logger.isDebugEnabled()) {
251: logger
252: .debug("Could not find a suitable message error code, returning default message");
253: }
254: return null;
255: }
256:
257: /**
258: * Get the formatted value for the property at the provided path.
259: * The formatted value is a string value for display, converted
260: * via a registered property editor.
261: * @param propertyPath the property path
262: * @return the formatted property value
263: * @throws NoSuchMethodException if called during Struts binding
264: * (without Spring Errors object being exposed), to indicate no
265: * available property to Struts
266: */
267: private Object getFieldValue(String propertyPath)
268: throws NoSuchMethodException {
269: if (this .errors == null) {
270: throw new NoSuchMethodException(
271: "No bean properties exposed to Struts binding - performing Spring binding later on");
272: }
273: return this .errors.getFieldValue(propertyPath);
274: }
275:
276: /**
277: * Special subclass of PropertyUtilsBean that it is aware of SpringBindingActionForm
278: * and uses it for retrieving field values. The field values will be taken from
279: * the underlying POJO form object that the Spring Errors object was created for.
280: */
281: private static class SpringBindingAwarePropertyUtilsBean extends
282: PropertyUtilsBean {
283:
284: public Object getNestedProperty(Object bean, String propertyPath)
285: throws IllegalAccessException,
286: InvocationTargetException, NoSuchMethodException {
287:
288: // Extract Spring-managed field value in case of SpringBindingActionForm.
289: if (bean instanceof SpringBindingActionForm) {
290: SpringBindingActionForm form = (SpringBindingActionForm) bean;
291: return form.getFieldValue(propertyPath);
292: }
293:
294: // Else fall back to default PropertyUtils behavior.
295: return super.getNestedProperty(bean, propertyPath);
296: }
297: }
298:
299: }
|