1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.struts2.dispatcher;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27
28 import javax.servlet.ServletContext;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.HttpServletRequest;
31 import javax.servlet.http.HttpServletResponse;
32
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 import org.apache.struts2.ServletActionContext;
36 import org.apache.struts2.StrutsConstants;
37 import org.apache.struts2.StrutsStatics;
38 import org.apache.struts2.config.Settings;
39 import org.apache.struts2.config.StrutsXmlConfigurationProvider;
40 import org.apache.struts2.dispatcher.mapper.ActionMapping;
41 import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
42 import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
43 import org.apache.struts2.impl.StrutsActionProxyFactory;
44 import org.apache.struts2.impl.StrutsObjectFactory;
45 import org.apache.struts2.util.AttributeMap;
46 import org.apache.struts2.util.ObjectFactoryDestroyable;
47 import org.apache.struts2.util.ObjectFactoryInitializable;
48 import org.apache.struts2.views.freemarker.FreemarkerManager;
49
50 import com.opensymphony.xwork2.util.ClassLoaderUtil;
51 import com.opensymphony.xwork2.util.FileManager;
52 import com.opensymphony.xwork2.ActionContext;
53 import com.opensymphony.xwork2.ActionProxy;
54 import com.opensymphony.xwork2.ActionProxyFactory;
55 import com.opensymphony.xwork2.ObjectFactory;
56 import com.opensymphony.xwork2.Result;
57 import com.opensymphony.xwork2.config.ConfigurationException;
58 import com.opensymphony.xwork2.config.ConfigurationManager;
59 import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
60 import com.opensymphony.xwork2.util.LocalizedTextUtil;
61 import com.opensymphony.xwork2.util.ObjectTypeDeterminer;
62 import com.opensymphony.xwork2.util.ObjectTypeDeterminerFactory;
63 import com.opensymphony.xwork2.util.ValueStack;
64 import com.opensymphony.xwork2.util.ValueStackFactory;
65 import com.opensymphony.xwork2.util.XWorkContinuationConfig;
66 import com.opensymphony.xwork2.util.location.Location;
67 import com.opensymphony.xwork2.util.location.LocationUtils;
68 import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
69
70 import freemarker.template.Template;
71
72 /***
73 * A utility class the actual dispatcher delegates most of its tasks to. Each instance
74 * of the primary dispatcher holds an instance of this dispatcher to be shared for
75 * all requests.
76 *
77 * @see org.apache.struts2.dispatcher.FilterDispatcher
78 * @see org.apache.struts2.portlet.dispatcher.Jsr168Dispatcher
79 */
80 public class Dispatcher {
81
82
83 static {
84 ObjectFactory.setObjectFactory(new StrutsObjectFactory());
85 ActionProxyFactory.setFactory(new StrutsActionProxyFactory());
86 }
87
88 private static final Log LOG = LogFactory.getLog(Dispatcher.class);
89
90 private static ThreadLocal<Dispatcher> instance = new ThreadLocal<Dispatcher>();
91 private static List<DispatcherListener> dispatcherListeners =
92 new ArrayList<DispatcherListener>();
93
94 private ConfigurationManager configurationManager;
95 private static boolean portletSupportActive;
96 private boolean devMode = false;
97
98
99 private boolean paramsWorkaroundEnabled = false;
100
101 /***
102 * Gets the current instance for this thread
103 *
104 * @return The dispatcher instance
105 */
106 public static Dispatcher getInstance() {
107 return (Dispatcher) instance.get();
108 }
109
110 /***
111 * Sets the dispatcher instance for this thread
112 *
113 * @param instance The instance
114 */
115 public static void setInstance(Dispatcher instance) {
116 Dispatcher.instance.set(instance);
117 }
118
119 /***
120 * Adds a dispatcher lifecycle listener
121 *
122 * @param l The listener
123 */
124 public static synchronized void addDispatcherListener(DispatcherListener l) {
125 dispatcherListeners.add(l);
126 }
127
128 /***
129 * Removes a dispatcher lifecycle listener
130 *
131 * @param l The listener
132 */
133 public static synchronized void removeDispatcherListener(DispatcherListener l) {
134 dispatcherListeners.remove(l);
135 }
136
137 /***
138 * The constructor with its servlet context instance (optional)
139 *
140 * @param servletContext The servlet context
141 */
142 public Dispatcher(ServletContext servletContext) {
143 init(servletContext);
144 }
145
146 /***
147 * Cleans up thread local variables
148 */
149 public void cleanup() {
150 ObjectFactory objectFactory = ObjectFactory.getObjectFactory();
151 if (objectFactory == null) {
152 LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
153 }
154 if (objectFactory instanceof ObjectFactoryDestroyable) {
155 try {
156 ((ObjectFactoryDestroyable)objectFactory).destroy();
157 }
158 catch(Exception e) {
159
160 LOG.error("exception occurred while destroying ObjectFactory ["+objectFactory+"]", e);
161 }
162 }
163 instance.set(null);
164 synchronized(Dispatcher.class) {
165 if (dispatcherListeners.size() > 0) {
166 for (DispatcherListener l : dispatcherListeners) {
167 l.dispatcherDestroyed(this);
168 }
169 }
170 }
171 }
172
173 /***
174 * Initializes the instance
175 *
176 * @param servletContext The servlet context
177 */
178 private void init(ServletContext servletContext) {
179 boolean reloadi18n = Boolean.valueOf((String) Settings.get(StrutsConstants.STRUTS_I18N_RELOAD)).booleanValue();
180 LocalizedTextUtil.setReloadBundles(reloadi18n);
181
182 if (Settings.isSet(StrutsConstants.STRUTS_OBJECTFACTORY)) {
183 String className = (String) Settings.get(StrutsConstants.STRUTS_OBJECTFACTORY);
184 if (className.equals("spring")) {
185
186
187 className = "org.apache.struts2.spring.StrutsSpringObjectFactory";
188 } else if (className.equals("plexus")) {
189 className = "org.apache.struts2.plexus.PlexusObjectFactory";
190 LOG.warn("The 'plexus' shorthand for the Plexus ObjectFactory is deprecated. Please "
191 +"use the full class name: "+className);
192 }
193
194 try {
195 Class clazz = ClassLoaderUtil.loadClass(className, Dispatcher.class);
196 ObjectFactory objectFactory = (ObjectFactory) clazz.newInstance();
197 if (servletContext != null) {
198 if (objectFactory instanceof ObjectFactoryInitializable) {
199 ((ObjectFactoryInitializable) objectFactory).init(servletContext);
200 }
201 }
202 ObjectFactory.setObjectFactory(objectFactory);
203 } catch (Exception e) {
204 LOG.error("Could not load ObjectFactory named " + className + ". Using default ObjectFactory.", e);
205 }
206 }
207
208 if (Settings.isSet(StrutsConstants.STRUTS_OBJECTTYPEDETERMINER)) {
209 String className = (String) Settings.get(StrutsConstants.STRUTS_OBJECTTYPEDETERMINER);
210 if (className.equals("tiger")) {
211
212
213 className = "com.opensymphony.xwork2.util.GenericsObjectTypeDeterminer";
214 }
215 else if (className.equals("notiger")) {
216 className = "com.opensymphony.xwork2.util.DefaultObjectTypeDeterminer";
217 }
218
219 try {
220 Class clazz = ClassLoaderUtil.loadClass(className, Dispatcher.class);
221 ObjectTypeDeterminer objectTypeDeterminer = (ObjectTypeDeterminer) clazz.newInstance();
222 ObjectTypeDeterminerFactory.setInstance(objectTypeDeterminer);
223 } catch (Exception e) {
224 LOG.error("Could not load ObjectTypeDeterminer named " + className + ". Using default DefaultObjectTypeDeterminer.", e);
225 }
226 }
227
228 if ("true".equals(Settings.get(StrutsConstants.STRUTS_DEVMODE))) {
229 devMode = true;
230 Settings.set(StrutsConstants.STRUTS_I18N_RELOAD, "true");
231 Settings.set(StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD, "true");
232 }
233
234
235 if ("true".equalsIgnoreCase(Settings.get(StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD))) {
236 FileManager.setReloadingConfigs(true);
237 }
238
239 if (Settings.isSet(StrutsConstants.STRUTS_CONTINUATIONS_PACKAGE)) {
240 String pkg = Settings.get(StrutsConstants.STRUTS_CONTINUATIONS_PACKAGE);
241 ObjectFactory.setContinuationPackage(pkg);
242 }
243
244
245 if (servletContext != null && servletContext.getServerInfo() != null
246 && servletContext.getServerInfo().indexOf("WebLogic") >= 0) {
247 LOG.info("WebLogic server detected. Enabling Struts parameter access work-around.");
248 paramsWorkaroundEnabled = true;
249 } else if (Settings.isSet(StrutsConstants.STRUTS_DISPATCHER_PARAMETERSWORKAROUND)) {
250 paramsWorkaroundEnabled = "true".equals(Settings.get(StrutsConstants.STRUTS_DISPATCHER_PARAMETERSWORKAROUND));
251 } else {
252 LOG.debug("Parameter access work-around disabled.");
253 }
254
255 configurationManager = new ConfigurationManager();
256 String configFiles = null;
257 if (Settings.isSet(StrutsConstants.STRUTS_CONFIGURATION_FILES)) {
258 configFiles = Settings.get(StrutsConstants.STRUTS_CONFIGURATION_FILES);
259 }
260 if (configFiles != null) {
261 String[] files = configFiles.split("//s*[,]//s*");
262 for (String file : files) {
263 if ("xwork.xml".equals(file)) {
264 configurationManager.addConfigurationProvider(new XmlConfigurationProvider(file, false));
265 } else {
266 configurationManager.addConfigurationProvider(new StrutsXmlConfigurationProvider(file, false));
267 }
268 }
269 }
270
271 synchronized(Dispatcher.class) {
272 if (dispatcherListeners.size() > 0) {
273 for (DispatcherListener l : dispatcherListeners) {
274 l.dispatcherInitialized(this);
275 }
276 }
277 }
278 }
279
280 /***
281 * Loads the action and executes it. This method first creates the action context from the given
282 * parameters then loads an <tt>ActionProxy</tt> from the given action name and namespace. After that,
283 * the action is executed and output channels throught the response object. Actions not found are
284 * sent back to the user via the {@link Dispatcher#sendError} method, using the 404 return code.
285 * All other errors are reported by throwing a ServletException.
286 *
287 * @param request the HttpServletRequest object
288 * @param response the HttpServletResponse object
289 * @param mapping the action mapping object
290 * @throws ServletException when an unknown error occurs (not a 404, but typically something that
291 * would end up as a 5xx by the servlet container)
292 */
293 public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context, ActionMapping mapping) throws ServletException {
294 Map<String, Object> extraContext = createContextMap(request, response, mapping, context);
295
296
297 ValueStack stack = (ValueStack) request.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);
298 if (stack != null) {
299 extraContext.put(ActionContext.VALUE_STACK, ValueStackFactory.getFactory().createValueStack(stack));
300 }
301
302 String timerKey = "Handling request from Dispatcher";
303 try {
304 UtilTimerStack.push(timerKey);
305 String namespace = mapping.getNamespace();
306 String name = mapping.getName();
307 String method = mapping.getMethod();
308
309 String id = request.getParameter(XWorkContinuationConfig.CONTINUE_PARAM);
310 if (id != null) {
311
312
313 Map params = (Map) extraContext.get(ActionContext.PARAMETERS);
314 params.remove(XWorkContinuationConfig.CONTINUE_PARAM);
315
316
317 extraContext.put(XWorkContinuationConfig.CONTINUE_KEY, id);
318 }
319
320 ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(
321 configurationManager.getConfiguration(), namespace, name, extraContext, true, false);
322 proxy.setMethod(method);
323 request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());
324
325
326 if (mapping.getResult() != null) {
327 Result result = mapping.getResult();
328 result.execute(proxy.getInvocation());
329 } else {
330 proxy.execute();
331 }
332
333
334 if (stack != null) {
335 request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
336 }
337 } catch (ConfigurationException e) {
338 LOG.error("Could not find action or result", e);
339 sendError(request, response, context, HttpServletResponse.SC_NOT_FOUND, e);
340 } catch (Exception e) {
341 LOG.error("Could not execute action", e);
342 sendError(request, response, context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
343 } finally {
344 UtilTimerStack.pop(timerKey);
345 }
346 }
347
348 /***
349 * Creates a context map containing all the wrapped request objects
350 *
351 * @param request The servlet request
352 * @param response The servlet response
353 * @param mapping The action mapping
354 * @param context The servlet context
355 * @return A map of context objects
356 */
357 public Map<String,Object> createContextMap(HttpServletRequest request, HttpServletResponse response,
358 ActionMapping mapping, ServletContext context) {
359
360 Map requestMap = new RequestMap(request);
361
362
363 Map params = null;
364 if (mapping != null) {
365 params = mapping.getParams();
366 }
367 Map requestParams = new HashMap(request.getParameterMap());
368 if (params != null) {
369 params.putAll(requestParams);
370 } else {
371 params = requestParams;
372 }
373
374
375 Map session = new SessionMap(request);
376
377
378 Map application = new ApplicationMap(context);
379
380 Map<String,Object> extraContext = createContextMap(requestMap, params, session, application, request, response, context);
381 extraContext.put(ServletActionContext.ACTION_MAPPING, mapping);
382 return extraContext;
383 }
384
385 /***
386 * Merges all application and servlet attributes into a single <tt>HashMap</tt> to represent the entire
387 * <tt>Action</tt> context.
388 *
389 * @param requestMap a Map of all request attributes.
390 * @param parameterMap a Map of all request parameters.
391 * @param sessionMap a Map of all session attributes.
392 * @param applicationMap a Map of all servlet context attributes.
393 * @param request the HttpServletRequest object.
394 * @param response the HttpServletResponse object.
395 * @param servletContext the ServletContextmapping object.
396 * @return a HashMap representing the <tt>Action</tt> context.
397 */
398 public HashMap<String,Object> createContextMap(Map requestMap,
399 Map parameterMap,
400 Map sessionMap,
401 Map applicationMap,
402 HttpServletRequest request,
403 HttpServletResponse response,
404 ServletContext servletContext) {
405 HashMap<String,Object> extraContext = new HashMap<String,Object>();
406 extraContext.put(ActionContext.PARAMETERS, new HashMap(parameterMap));
407 extraContext.put(ActionContext.SESSION, sessionMap);
408 extraContext.put(ActionContext.APPLICATION, applicationMap);
409
410 Locale locale = null;
411 if (Settings.isSet(StrutsConstants.STRUTS_LOCALE)) {
412 locale = LocalizedTextUtil.localeFromString(Settings.get(StrutsConstants.STRUTS_LOCALE), request.getLocale());
413 } else {
414 locale = request.getLocale();
415 }
416
417 extraContext.put(ActionContext.LOCALE, locale);
418 extraContext.put(ActionContext.DEV_MODE, Boolean.valueOf(devMode));
419
420 extraContext.put(StrutsStatics.HTTP_REQUEST, request);
421 extraContext.put(StrutsStatics.HTTP_RESPONSE, response);
422 extraContext.put(StrutsStatics.SERVLET_CONTEXT, servletContext);
423
424
425 extraContext.put("request", requestMap);
426 extraContext.put("session", sessionMap);
427 extraContext.put("application", applicationMap);
428 extraContext.put("parameters", parameterMap);
429
430 AttributeMap attrMap = new AttributeMap(extraContext);
431 extraContext.put("attr", attrMap);
432
433 return extraContext;
434 }
435
436 /***
437 * Returns the maximum upload size allowed for multipart requests (this is configurable).
438 *
439 * @return the maximum upload size allowed for multipart requests
440 */
441 private static int getMaxSize() {
442 Integer maxSize = new Integer(Integer.MAX_VALUE);
443 try {
444 String maxSizeStr = Settings.get(StrutsConstants.STRUTS_MULTIPART_MAXSIZE);
445
446 if (maxSizeStr != null) {
447 try {
448 maxSize = new Integer(maxSizeStr);
449 } catch (NumberFormatException e) {
450 LOG.warn("Unable to format 'struts.multipart.maxSize' property setting. Defaulting to Integer.MAX_VALUE");
451 }
452 } else {
453 LOG.warn("Unable to format 'struts.multipart.maxSize' property setting. Defaulting to Integer.MAX_VALUE");
454 }
455 } catch (IllegalArgumentException e1) {
456 LOG.warn("Unable to format 'struts.multipart.maxSize' property setting. Defaulting to Integer.MAX_VALUE");
457 }
458
459 if (LOG.isDebugEnabled()) {
460 LOG.debug("maxSize=" + maxSize);
461 }
462
463 return maxSize.intValue();
464 }
465
466 /***
467 * Returns the path to save uploaded files to (this is configurable).
468 *
469 * @return the path to save uploaded files to
470 */
471 private String getSaveDir(ServletContext servletContext) {
472 String saveDir = Settings.get(StrutsConstants.STRUTS_MULTIPART_SAVEDIR).trim();
473
474 if (saveDir.equals("")) {
475 File tempdir = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
476 LOG.info("Unable to find 'struts.multipart.saveDir' property setting. Defaulting to javax.servlet.context.tempdir");
477
478 if (tempdir != null) {
479 saveDir = tempdir.toString();
480 }
481 } else {
482 File multipartSaveDir = new File(saveDir);
483
484 if (!multipartSaveDir.exists()) {
485 multipartSaveDir.mkdir();
486 }
487 }
488
489 if (LOG.isDebugEnabled()) {
490 LOG.debug("saveDir=" + saveDir);
491 }
492
493 return saveDir;
494 }
495
496 /***
497 * Prepares a request, including setting the encoding and locale
498 *
499 * @param request The request
500 * @param response The response
501 */
502 public void prepare(HttpServletRequest request, HttpServletResponse response) {
503 String encoding = null;
504 if (Settings.isSet(StrutsConstants.STRUTS_I18N_ENCODING)) {
505 encoding = Settings.get(StrutsConstants.STRUTS_I18N_ENCODING);
506 }
507
508 Locale locale = null;
509 if (Settings.isSet(StrutsConstants.STRUTS_LOCALE)) {
510 locale = LocalizedTextUtil.localeFromString(Settings.get(StrutsConstants.STRUTS_LOCALE), request.getLocale());
511 }
512
513 if (encoding != null) {
514 try {
515 request.setCharacterEncoding(encoding);
516 } catch (Exception e) {
517 LOG.error("Error setting character encoding to '" + encoding + "' - ignoring.", e);
518 }
519 }
520
521 if (locale != null) {
522 response.setLocale(locale);
523 }
524
525 if (paramsWorkaroundEnabled) {
526 request.getParameter("foo");
527 }
528 }
529
530 /***
531 * Wraps and returns the given response or returns the original response object. This is used to transparently
532 * handle multipart data as a wrapped class around the given request. Override this method to handle multipart
533 * requests in a special way or to handle other types of requests. Note, {@link org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper} is
534 * flexible - you should look to that first before overriding this method to handle multipart data.
535 *
536 * @param request the HttpServletRequest object.
537 * @return a wrapped request or original request.
538 * @see org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper
539 */
540 public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
541
542 if (request instanceof StrutsRequestWrapper) {
543 return request;
544 }
545
546 if (MultiPartRequest.isMultiPart(request)) {
547 request = new MultiPartRequestWrapper(request, getSaveDir(servletContext), getMaxSize());
548 } else {
549 request = new StrutsRequestWrapper(request);
550 }
551
552 return request;
553 }
554
555 /***
556 * Sends an HTTP error response code.
557 *
558 * @param request the HttpServletRequest object.
559 * @param response the HttpServletResponse object.
560 * @param code the HttpServletResponse error code (see {@link javax.servlet.http.HttpServletResponse} for possible error codes).
561 * @param e the Exception that is reported.
562 */
563 public void sendError(HttpServletRequest request, HttpServletResponse response,
564 ServletContext ctx, int code, Exception e) {
565 if (devMode) {
566 response.setContentType("text/html");
567
568 try {
569 freemarker.template.Configuration config = FreemarkerManager.getInstance().getConfiguration(ctx);
570 Template template = config.getTemplate("/org/apache/struts2/dispatcher/error.ftl");
571
572 List<Throwable> chain = new ArrayList<Throwable>();
573 Throwable cur = e;
574 chain.add(cur);
575 while ((cur = cur.getCause()) != null) {
576 chain.add(cur);
577 }
578
579 HashMap<String,Object> data = new HashMap<String,Object>();
580 data.put("exception", e);
581 data.put("unknown", Location.UNKNOWN);
582 data.put("chain", chain);
583 data.put("locator", new Locator());
584 template.process(data, response.getWriter());
585 response.getWriter().close();
586 } catch (Exception exp) {
587 try {
588 response.sendError(code, "Unable to show problem report: " + exp);
589 } catch (IOException ex) {
590
591 }
592 }
593 } else {
594 try {
595
596
597 request.setAttribute("javax.servlet.error.exception", e);
598
599
600 request.setAttribute("javax.servlet.jsp.jspException", e);
601
602
603 response.sendError(code, e.getMessage());
604 } catch (IOException e1) {
605
606 }
607 }
608 }
609
610 /***
611 * Returns <tt>true</tt>, if portlet support is active, <tt>false</tt> otherwise.
612 *
613 * @return <tt>true</tt>, if portlet support is active, <tt>false</tt> otherwise.
614 */
615 public boolean isPortletSupportActive() {
616 return portletSupportActive;
617 }
618
619 /***
620 * Set the flag that portlet support is active or not.
621 * @param portletSupportActive <tt>true</tt> or <tt>false</tt>
622 */
623 public static void setPortletSupportActive(boolean portletSupportActive) {
624 Dispatcher.portletSupportActive = portletSupportActive;
625 }
626
627 /*** Simple accessor for a static method */
628 public class Locator {
629 public Location getLocation(Object obj) {
630 Location loc = LocationUtils.getLocation(obj);
631 if (loc == null) {
632 return Location.UNKNOWN;
633 }
634 return loc;
635 }
636 }
637
638 /***
639 * Gets the current configuration manager instance
640 *
641 * @return The instance
642 */
643 public ConfigurationManager getConfigurationManager() {
644 return configurationManager;
645 }
646
647 /***
648 * Sets the current configuration manager instance
649 *
650 * @param mgr The configuration manager
651 */
652 public void setConfigurationManager(ConfigurationManager mgr) {
653 this.configurationManager = mgr;
654 }
655
656 /***
657 * @return the devMode
658 */
659 public boolean isDevMode() {
660 return devMode;
661 }
662
663 /***
664 * @param devMode the devMode to set
665 */
666 public void setDevMode(boolean devMode) {
667 this.devMode = devMode;
668 }
669
670 }