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  * &lt;form-beans&gt;
63  *   &lt;form-bean name="actionForm" type="org.springframework.web.struts.SpringBindingActionForm"/&gt;
64  * &lt;/form-beans&gt;</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