View Javadoc
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 }