1 /*
2 * $Id$
3 *
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21 package org.apache.struts.chain;
22
23 import java.io.IOException;
24 import java.lang.reflect.Constructor;
25
26 import jakarta.servlet.ServletContext;
27 import jakarta.servlet.ServletException;
28 import jakarta.servlet.UnavailableException;
29 import jakarta.servlet.http.HttpServletRequest;
30 import jakarta.servlet.http.HttpServletResponse;
31
32 import org.apache.commons.beanutils.ConstructorUtils;
33 import org.apache.commons.chain.Catalog;
34 import org.apache.commons.chain.CatalogFactory;
35 import org.apache.commons.chain.Command;
36 import org.apache.struts.action.ActionServlet;
37 import org.apache.struts.action.RequestProcessor;
38 import org.apache.struts.chain.contexts.ActionContext;
39 import org.apache.struts.chain.contexts.ServletActionContext;
40 import org.apache.struts.config.ControllerConfig;
41 import org.apache.struts.config.ModuleConfig;
42 import org.apache.struts.upload.MultipartRequestWrapper;
43 import org.apache.struts.util.RequestUtils;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48 * ComposableRequestProcessor uses the Chain Of Responsibility design pattern
49 * (as implemented by the commons-chain package in Jakarta Commons) to support
50 * external configuration of command chains to be used. It is configured via
51 * the following context initialization parameters:
52 *
53 * <ul>
54 *
55 * <li>[org.apache.struts.chain.CATALOG_NAME] - Name of the Catalog in which we
56 * will look up the Command to be executed for each request. If not specified,
57 * the default value is struts.</li>
58 *
59 * <li> org.apache.struts.chain.COMMAND_NAME - Name of the Command which we
60 * will execute for each request, to be looked up in the specified Catalog. If
61 * not specified, the default value is servlet-standard.</li>
62 *
63 * </ul>
64 *
65 * @since Struts 1.1
66 */
67 public class ComposableRequestProcessor extends RequestProcessor {
68 private static final long serialVersionUID = -1205090974097129899L;
69
70 // ------------------------------------------------------ Instance Variables
71
72 /**
73 * Cache for constructor discovered by setActionContextClass method.
74 */
75 private static final Class<?>[] SERVLET_ACTION_CONTEXT_CTOR_SIGNATURE =
76 {
77 ServletContext.class, HttpServletRequest.class,
78 HttpServletResponse.class
79 };
80
81 /**
82 * Token for ActionContext class so that it can be stored in the
83 * ControllerConfig.
84 */
85 public static final String ACTION_CONTEXT_CLASS = "ACTION_CONTEXT_CLASS";
86
87 /**
88 * The {@code Log} instance for this class.
89 */
90 private transient final Logger log =
91 LoggerFactory.getLogger(ComposableRequestProcessor.class);
92
93 /**
94 * The {@link CatalogFactory} from which catalog containing the the
95 * base request-processing {@link Command} will be retrieved.
96 */
97 protected CatalogFactory<ActionContext> catalogFactory = null;
98
99 /**
100 * The {@link Catalog} containing all of the available command chains for
101 * this module.
102 */
103 protected Catalog<ActionContext> catalog = null;
104
105 /**
106 * The {@link Command} to be executed for each request.
107 */
108 protected Command<ActionContext> command = null;
109
110 /**
111 * ActionContext class as cached by createActionContextInstance method.
112 */
113 private Class<? extends ActionContext> actionContextClass;
114
115 /**
116 * ActionContext constructor as cached by createActionContextInstance
117 * method.
118 */
119 private Constructor<? extends ActionContext> servletActionContextConstructor = null;
120
121 // ---------------------------------------------------------- Public Methods
122
123 /**
124 * Clean up in preparation for a shutdown of this application.
125 */
126 public void destroy() {
127 super.destroy();
128 catalogFactory = null;
129 catalog = null;
130 command = null;
131 actionContextClass = null;
132 servletActionContextConstructor = null;
133 }
134
135 /**
136 * Initialize this request processor instance.
137 *
138 * @param servlet The ActionServlet we are associated with
139 * @param moduleConfig The ModuleConfig we are associated with.
140 *
141 * @throws ServletException If an error occurs during initialization
142 */
143 public void init(ActionServlet servlet, ModuleConfig moduleConfig)
144 throws ServletException {
145 log.info(
146 "Initializing composable request processor for module prefix '"
147 + moduleConfig.getPrefix() + "'");
148 super.init(servlet, moduleConfig);
149
150 initCatalogFactory(servlet, moduleConfig);
151
152 ControllerConfig controllerConfig = moduleConfig.getControllerConfig();
153
154 String catalogName = controllerConfig.getCatalog();
155
156 catalog = this.catalogFactory.getCatalog(catalogName);
157
158 if (catalog == null) {
159 throw new ServletException("Cannot find catalog '" + catalogName
160 + "'");
161 }
162
163 String commandName = controllerConfig.getCommand();
164
165 command = catalog.getCommand(commandName);
166
167 if (command == null) {
168 throw new ServletException("Cannot find command '" + commandName
169 + "'");
170 }
171
172 this.setActionContextClassName(controllerConfig.getProperty(
173 ACTION_CONTEXT_CLASS));
174 }
175
176 /**
177 * Set and cache ActionContext class.
178 *
179 * <p>If there is a custom class provided and if it uses our "preferred"
180 * constructor, cache a reference to that constructor rather than looking
181 * it up every time.</p>
182 *
183 * @param actionContextClass The ActionContext class to process
184 */
185 private void setActionContextClass(Class<? extends ActionContext> actionContextClass) {
186 this.actionContextClass = actionContextClass;
187
188 if (actionContextClass != null) {
189 this.servletActionContextConstructor =
190 ConstructorUtils.getAccessibleConstructor(actionContextClass,
191 SERVLET_ACTION_CONTEXT_CTOR_SIGNATURE);
192 } else {
193 this.servletActionContextConstructor = null;
194 }
195 }
196
197 /**
198 * Make sure that the specified {@code className} identifies a class which
199 * can be found and which implements the {@code ActionContext} interface.
200 *
201 * @param className Fully qualified name of
202 *
203 * @throws ServletException If an error occurs during initialization
204 * @throws UnavailableException if class does not implement ActionContext
205 * or is not found
206 */
207 private void setActionContextClassName(String className)
208 throws ServletException {
209 if ((className != null) && (className.trim().length() > 0)) {
210 if (log.isDebugEnabled()) {
211 log.debug(
212 "setActionContextClassName: requested context class: "
213 + className);
214 }
215
216 try {
217 Class<?> actionContextClass =
218 RequestUtils.applicationClass(className);
219
220 if (!ActionContext.class.isAssignableFrom(actionContextClass)) {
221 throw new UnavailableException("ActionContextClass " + "["
222 + className + "]"
223 + " must implement ActionContext interface.");
224 }
225
226 this.setActionContextClass(actionContextClass.asSubclass(ActionContext.class));
227 } catch (ClassNotFoundException e) {
228 throw new UnavailableException("ActionContextClass "
229 + className + " not found.");
230 }
231 } else {
232 if (log.isDebugEnabled()) {
233 log.debug("setActionContextClassName: no className specified");
234 }
235
236 this.setActionContextClass(null);
237 }
238 }
239
240 /**
241 * Establish the CatalogFactory which will be used to look up the catalog
242 * which has the request processing command.
243 *
244 * <p>The base implementation simply calls CatalogFactory.getInstance(),
245 * unless the catalogFactory property of this object has already been set,
246 * in which case it is not changed.</p>
247 *
248 * @param servlet The ActionServlet we are processing
249 * @param moduleConfig The ModuleConfig we are processing
250 */
251 protected void initCatalogFactory(ActionServlet servlet,
252 ModuleConfig moduleConfig) {
253 if (this.catalogFactory != null) {
254 return;
255 }
256
257 this.catalogFactory = CatalogFactory.getInstance();
258 }
259
260 /**
261 * Process an {@code HttpServletRequest} and create the corresponding
262 * {@code HttpServletResponse}.
263 *
264 * @param request The servlet request we are processing
265 * @param response The servlet response we are creating
266 *
267 * @throws IOException if an input/output error occurs
268 * @throws ServletException if a processing exception occurs
269 */
270 public void process(HttpServletRequest request, HttpServletResponse response)
271 throws IOException, ServletException {
272 // Wrap the request in the case of a multipart request
273 request = processMultipart(request);
274
275 // Create and populate a Context for this request
276 ActionContext context = contextInstance(request, response);
277
278 // Create and execute the command.
279 try {
280 if (log.isDebugEnabled()) {
281 log.debug("Using processing chain for this request");
282 }
283
284 command.execute(context);
285 } catch (Exception e) {
286 // Execute the exception processing chain??
287 throw new ServletException(e);
288 } finally {
289 // Release the context.
290 if (context != null) {
291 context.release();
292 }
293 }
294 }
295
296 /**
297 * Provide the initialized {@code ActionContext} instance which will be
298 * used by this request. Internally, this simply calls
299 * {@code createActionContextInstance} followed by
300 * {@code initializeActionContext}.
301 *
302 * @param request The servlet request we are processing
303 * @param response The servlet response we are creating
304 *
305 * @return Initiliazed ActionContext
306 *
307 * @throws ServletException if a processing exception occurs
308 */
309 protected ActionContext contextInstance(HttpServletRequest request,
310 HttpServletResponse response)
311 throws ServletException {
312 ActionContext context =
313 createActionContextInstance(getServletContext(), request, response);
314
315 initializeActionContext(context);
316
317 return context;
318 }
319
320 /**
321 * Create a new instance of {@code ActionContext} according to
322 * configuration. If no alternative was specified at initialization, a new
323 * instance {@code ServletActionContext} is returned. If an alternative was
324 * specified using the {@code ACTION_CONTEXT_CLASS} property, then that
325 * value is treated as a classname, and an instance of that class is
326 * created. If that class implements the same constructor that
327 * {@code ServletActionContext} does, then that constructor will be used:
328 * {@code ServletContext, HttpServletRequest, HttpServletResponse};
329 * otherwise, it is assumed that the class has a no-arguments constructor.
330 * If these constraints do not suit you, simply override this method in a subclass.
331 *
332 * @param servletContext The servlet context we are processing
333 * @param request The servlet request we are processing
334 * @param response The servlet response we are creating
335 *
336 * @return New instance of ActionContext
337 *
338 * @throws ServletException if a processing exception occurs
339 */
340 protected ActionContext createActionContextInstance(
341 ServletContext servletContext, HttpServletRequest request,
342 HttpServletResponse response)
343 throws ServletException {
344 if (this.actionContextClass == null) {
345 return new ServletActionContext(servletContext, request, response);
346 }
347
348 try {
349 if (this.servletActionContextConstructor == null) {
350 return this.actionContextClass.getDeclaredConstructor().newInstance();
351 }
352
353 return this.servletActionContextConstructor
354 .newInstance(servletContext, request, response);
355 } catch (Exception e) {
356 throw new ServletException(
357 "Error creating ActionContext instance of type "
358 + this.actionContextClass, e);
359 }
360 }
361
362 /**
363 * Set common properties on the given {@code ActionContext} instance so
364 * that commands in the chain can count on their presence. Note that
365 * while this method does not require that its argument be an instance of
366 * {@code ServletActionContext}, at this time many common Struts commands
367 * will be expecting to receive an {@code ActionContext} which is also a
368 * {@code ServletActionContext}.
369 *
370 * @param context The ActionContext we are processing
371 */
372 protected void initializeActionContext(ActionContext context) {
373 if (context instanceof ServletActionContext) {
374 ((ServletActionContext) context).setActionServlet(this.servlet);
375 }
376
377 context.setModuleConfig(this.moduleConfig);
378 }
379
380 /**
381 * If this is a multipart request, wrap it with a special wrapper.
382 * Otherwise, return the request unchanged.
383 *
384 * @param request The HttpServletRequest we are processing
385 *
386 * @return Original or wrapped request as appropriate
387 */
388 protected HttpServletRequest processMultipart(HttpServletRequest request) {
389 if (!"POST".equalsIgnoreCase(request.getMethod())) {
390 return (request);
391 }
392
393 String contentType = request.getContentType();
394
395 if ((contentType != null)
396 && contentType.startsWith("multipart/form-data")) {
397 return (new MultipartRequestWrapper(request));
398 } else {
399 return (request);
400 }
401 }
402
403 /**
404 * Set the {@code CatalogFactory} instance which should be used to find the
405 * request-processing command. In the base implementation, if this value is
406 * not already set, then it will be initialized when
407 * {@link #initCatalogFactory} is called.
408 *
409 * @param catalogFactory Our CatalogFactory instance
410 */
411 public void setCatalogFactory(CatalogFactory<ActionContext> catalogFactory) {
412 this.catalogFactory = catalogFactory;
413 }
414 }