1 /* 2 * Copyright 2002-2006 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.web.struts; 18 19 import java.lang.reflect.InvocationTargetException; 20 import java.util.Iterator; 21 import java.util.Locale; 22 23 import javax.servlet.http.HttpServletRequest; 24 25 import org.apache.commons.beanutils.BeanUtilsBean; 26 import org.apache.commons.beanutils.ConvertUtilsBean; 27 import org.apache.commons.beanutils.PropertyUtilsBean; 28 import org.apache.commons.logging.Log; 29 import org.apache.commons.logging.LogFactory; 30 import org.apache.struts.Globals; 31 import org.apache.struts.action.ActionForm; 32 import org.apache.struts.action.ActionMessage; 33 import org.apache.struts.action.ActionMessages; 34 import org.apache.struts.util.MessageResources; 35 36 import org.springframework.context.MessageSourceResolvable; 37 import org.springframework.validation.Errors; 38 import org.springframework.validation.FieldError; 39 import org.springframework.validation.ObjectError; 40 41 /** 42 * A thin Struts ActionForm adapter that delegates to Spring's more complete 43 * and advanced data binder and Errors object underneath the covers to bind 44 * to POJOs and manage rejected values. 45 * 46 * <p>Exposes Spring-managed errors to the standard Struts view tags, through 47 * exposing a corresponding Struts ActionMessages object as request attribute. 48 * Also exposes current field values in a Struts-compliant fashion, including 49 * rejected values (which Spring's binding keeps even for non-String fields). 50 * 51 * <p>Consequently, Struts views can be written in a completely traditional 52 * fashion (with standard <code>html:form</code>, <code>html:errors</code>, etc), 53 * seamlessly accessing a Spring-bound POJO form object underneath. 54 * 55 * <p>Note this ActionForm is designed explicitly for use in <i>request scope</i>. 56 * It expects to receive an <code>expose</code> call from the Action, passing 57 * in the Errors object to expose plus the current HttpServletRequest. 58 * 59 * <p>Example definition in <code>struts-config.xml</code>: 60 * 61 * <pre> 62 * <form-beans> 63 * <form-bean name="actionForm" type="org.springframework.web.struts.SpringBindingActionForm"/> 64 * </form-beans></pre> 65 * 66 * Example code in a custom Struts <code>Action</code>: 67 * 68 * <pre> 69 * public ActionForward execute(ActionMapping actionMapping, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception { 70 * SpringBindingActionForm form = (SpringBindingActionForm) actionForm; 71 * MyPojoBean bean = ...; 72 * ServletRequestDataBinder binder = new ServletRequestDataBinder(bean, "myPojo"); 73 * binder.bind(request); 74 * form.expose(binder.getBindingResult(), request); 75 * return actionMapping.findForward("success"); 76 * }</pre> 77 * 78 * This class is compatible with both Struts 1.2.x and Struts 1.1. 79 * On Struts 1.2, default messages registered with Spring binding errors 80 * are exposed when none of the error codes could be resolved. 81 * On Struts 1.1, this is not possible due to a limitation in the Struts 82 * message facility; hence, we expose the plain default error code there. 83 * 84 * @author Keith Donald 85 * @author Juergen Hoeller 86 * @since 1.2.2 87 * @see #expose(org.springframework.validation.Errors, javax.servlet.http.HttpServletRequest) 88 * @deprecated as of Spring 3.0 89 */ 90 @Deprecated 91 public class SpringBindingActionForm extends ActionForm { 92 93 private static final Log logger = LogFactory.getLog(SpringBindingActionForm.class); 94 95 private static boolean defaultActionMessageAvailable = true; 96 97 98 static { 99 // Register special PropertyUtilsBean subclass that knows how to 100 // extract field values from a SpringBindingActionForm. 101 // As a consequence of the static nature of Commons BeanUtils, 102 // we have to resort to this initialization hack here. 103 ConvertUtilsBean convUtils = new ConvertUtilsBean(); 104 PropertyUtilsBean propUtils = new SpringBindingAwarePropertyUtilsBean(); 105 BeanUtilsBean beanUtils = new BeanUtilsBean(convUtils, propUtils); 106 BeanUtilsBean.setInstance(beanUtils); 107 108 // Determine whether the Struts 1.2 support for default messages 109 // is available on ActionMessage: ActionMessage(String, boolean) 110 // with "false" to be passed into the boolean flag. 111 try { ActionMessage.class.getConstructor(new Class[] {String.class, boolean.class})112 ActionMessage.class.getConstructor(new Class[] {String.class, boolean.class}); 113 } 114 catch (NoSuchMethodException ex) { 115 defaultActionMessageAvailable = false; 116 } 117 } 118 119 120 private Errors errors; 121 122 private Locale locale; 123 124 private MessageResources messageResources; 125 126 127 /** 128 * Set the Errors object that this SpringBindingActionForm is supposed 129 * to wrap. The contained field values and errors will be exposed 130 * to the view, accessible through Struts standard tags. 131 * @param errors the Spring Errors object to wrap, usually taken from 132 * a DataBinder that has been used for populating a POJO form object 133 * @param request the HttpServletRequest to retrieve the attributes from 134 * @see org.springframework.validation.DataBinder#getBindingResult() 135 */ expose(Errors errors, HttpServletRequest request)136 public void expose(Errors errors, HttpServletRequest request) { 137 this.errors = errors; 138 139 // Obtain the locale from Struts well-known location. 140 this.locale = (Locale) request.getSession().getAttribute(Globals.LOCALE_KEY); 141 142 // Obtain the MessageResources from Struts' well-known location. 143 this.messageResources = (MessageResources) request.getAttribute(Globals.MESSAGES_KEY); 144 145 if (errors != null && errors.hasErrors()) { 146 // Add global ActionError instances from the Spring Errors object. 147 ActionMessages actionMessages = (ActionMessages) request.getAttribute(Globals.ERROR_KEY); 148 if (actionMessages == null) { 149 request.setAttribute(Globals.ERROR_KEY, getActionMessages()); 150 } 151 else { 152 actionMessages.add(getActionMessages()); 153 } 154 } 155 } 156 157 158 /** 159 * Return an ActionMessages representation of this SpringBindingActionForm, 160 * exposing all errors contained in the underlying Spring Errors object. 161 * @see org.springframework.validation.Errors#getAllErrors() 162 */ getActionMessages()163 private ActionMessages getActionMessages() { 164 ActionMessages actionMessages = new ActionMessages(); 165 Iterator it = this.errors.getAllErrors().iterator(); 166 while (it.hasNext()) { 167 ObjectError objectError = (ObjectError) it.next(); 168 String effectiveMessageKey = findEffectiveMessageKey(objectError); 169 if (effectiveMessageKey == null && !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) ? 175 new ActionMessage(effectiveMessageKey, resolveArguments(objectError.getArguments())) : 176 new ActionMessage(objectError.getDefaultMessage(), false); 177 if (objectError instanceof FieldError) { 178 FieldError fieldError = (FieldError) objectError; 179 actionMessages.add(fieldError.getField(), message); 180 } 181 else { 182 actionMessages.add(ActionMessages.GLOBAL_MESSAGE, message); 183 } 184 } 185 if (logger.isDebugEnabled()) { 186 logger.debug("Final ActionMessages used for binding: " + actionMessages); 187 } 188 return actionMessages; 189 } 190 resolveArguments(Object[] arguments)191 private Object[] resolveArguments(Object[] arguments) { 192 if (arguments == null || arguments.length == 0) { 193 return arguments; 194 } 195 for (int i = 0; i < arguments.length; i++) { 196 Object arg = arguments[i]; 197 if (arg instanceof MessageSourceResolvable) { 198 MessageSourceResolvable resolvable = (MessageSourceResolvable)arg; 199 String[] codes = resolvable.getCodes(); 200 boolean resolved = false; 201 if (this.messageResources != null) { 202 for (int j = 0; j < codes.length; j++) { 203 String code = codes[j]; 204 if (this.messageResources.isPresent(this.locale, code)) { 205 arguments[i] = this.messageResources.getMessage( 206 this.locale, code, resolveArguments(resolvable.getArguments())); 207 resolved = true; 208 break; 209 } 210 } 211 } 212 if (!resolved) { 213 arguments[i] = resolvable.getDefaultMessage(); 214 } 215 } 216 } 217 return arguments; 218 } 219 220 /** 221 * Find the most specific message key for the given error. 222 * @param error the ObjectError to find a message key for 223 * @return the most specific message key found 224 */ findEffectiveMessageKey(ObjectError error)225 private String findEffectiveMessageKey(ObjectError error) { 226 if (this.messageResources != null) { 227 String[] possibleMatches = error.getCodes(); 228 for (int i = 0; i < possibleMatches.length; i++) { 229 if (logger.isDebugEnabled()) { 230 logger.debug("Looking for error code '" + possibleMatches[i] + "'"); 231 } 232 if (this.messageResources.isPresent(this.locale, possibleMatches[i])) { 233 if (logger.isDebugEnabled()) { 234 logger.debug("Found error code '" + possibleMatches[i] + "' in resource bundle"); 235 } 236 return possibleMatches[i]; 237 } 238 } 239 } 240 if (logger.isDebugEnabled()) { 241 logger.debug("Could not find a suitable message error code, returning default message"); 242 } 243 return null; 244 } 245 246 247 /** 248 * Get the formatted value for the property at the provided path. 249 * The formatted value is a string value for display, converted 250 * via a registered property editor. 251 * @param propertyPath the property path 252 * @return the formatted property value 253 * @throws NoSuchMethodException if called during Struts binding 254 * (without Spring Errors object being exposed), to indicate no 255 * available property to Struts 256 */ getFieldValue(String propertyPath)257 private Object getFieldValue(String propertyPath) throws NoSuchMethodException { 258 if (this.errors == null) { 259 throw new NoSuchMethodException( 260 "No bean properties exposed to Struts binding - performing Spring binding later on"); 261 } 262 return this.errors.getFieldValue(propertyPath); 263 } 264 265 266 /** 267 * Special subclass of PropertyUtilsBean that it is aware of SpringBindingActionForm 268 * and uses it for retrieving field values. The field values will be taken from 269 * the underlying POJO form object that the Spring Errors object was created for. 270 */ 271 private static class SpringBindingAwarePropertyUtilsBean extends PropertyUtilsBean { 272 273 @Override getNestedProperty(Object bean, String propertyPath)274 public Object getNestedProperty(Object bean, String propertyPath) 275 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 276 277 // Extract Spring-managed field value in case of SpringBindingActionForm. 278 if (bean instanceof SpringBindingActionForm) { 279 SpringBindingActionForm form = (SpringBindingActionForm) bean; 280 return form.getFieldValue(propertyPath); 281 } 282 283 // Else fall back to default PropertyUtils behavior. 284 return super.getNestedProperty(bean, propertyPath); 285 } 286 } 287 288 } 289