1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.apache.struts2.portlet.dispatcher;
23
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.util.Enumeration;
27 import java.util.HashMap;
28 import java.util.Locale;
29 import java.util.Map;
30
31 import javax.portlet.ActionRequest;
32 import javax.portlet.ActionResponse;
33 import javax.portlet.GenericPortlet;
34 import javax.portlet.PortletConfig;
35 import javax.portlet.PortletException;
36 import javax.portlet.PortletMode;
37 import javax.portlet.PortletRequest;
38 import javax.portlet.PortletResponse;
39 import javax.portlet.RenderRequest;
40 import javax.portlet.RenderResponse;
41 import javax.portlet.WindowState;
42 import javax.servlet.ServletContext;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpServletResponse;
45
46 import org.apache.struts2.StrutsConstants;
47 import org.apache.struts2.StrutsException;
48 import org.apache.struts2.StrutsStatics;
49 import org.apache.struts2.dispatcher.ApplicationMap;
50 import org.apache.struts2.dispatcher.Dispatcher;
51 import org.apache.struts2.dispatcher.RequestMap;
52 import org.apache.struts2.dispatcher.SessionMap;
53 import org.apache.struts2.dispatcher.mapper.ActionMapper;
54 import org.apache.struts2.dispatcher.mapper.ActionMapping;
55 import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
56 import org.apache.struts2.portlet.PortletActionConstants;
57 import org.apache.struts2.portlet.PortletApplicationMap;
58 import org.apache.struts2.portlet.PortletRequestMap;
59 import org.apache.struts2.portlet.PortletSessionMap;
60 import org.apache.struts2.portlet.context.PortletActionContext;
61 import org.apache.struts2.portlet.servlet.PortletServletContext;
62 import org.apache.struts2.portlet.servlet.PortletServletRequest;
63 import org.apache.struts2.portlet.servlet.PortletServletResponse;
64 import org.apache.struts2.util.AttributeMap;
65 import org.apache.commons.lang.xwork.StringUtils;
66
67 import com.opensymphony.xwork2.ActionContext;
68 import com.opensymphony.xwork2.ActionProxy;
69 import com.opensymphony.xwork2.ActionProxyFactory;
70 import com.opensymphony.xwork2.config.ConfigurationException;
71 import com.opensymphony.xwork2.inject.Container;
72 import com.opensymphony.xwork2.util.FileManager;
73 import com.opensymphony.xwork2.util.LocalizedTextUtil;
74 import com.opensymphony.xwork2.util.logging.Logger;
75 import com.opensymphony.xwork2.util.logging.LoggerFactory;
76
77 /***
78 * <!-- START SNIPPET: javadoc -->
79 * <p>
80 * Struts JSR-168 portlet dispatcher. Similar to the WW2 Servlet dispatcher,
81 * but adjusted to a portal environment. The portlet is configured through the <tt>portlet.xml</tt>
82 * descriptor. Examples and descriptions follow below:
83 * </p>
84 * <!-- END SNIPPET: javadoc -->
85 *
86 * @author Nils-Helge Garli
87 * @author Rainer Hermanns
88 *
89 * <p><b>Init parameters</b></p>
90 * <!-- START SNIPPET: params -->
91 * <table class="confluenceTable">
92 * <tr>
93 * <th class="confluenceTh">Name</th>
94 * <th class="confluenceTh">Description</th>
95 * <th class="confluenceTh">Default value</th>
96 * </tr>
97 * <tr>
98 * <td class="confluenceTd">portletNamespace</td><td class="confluenceTd">The namespace for the portlet in the xwork configuration. This
99 * namespace is prepended to all action lookups, and makes it possible to host multiple
100 * portlets in the same portlet application. If this parameter is set, the complete namespace
101 * will be <tt>/portletNamespace/modeNamespace/actionName</tt></td><td class="confluenceTd">The default namespace</td>
102 * </tr>
103 * <tr>
104 * <td class="confluenceTd">viewNamespace</td><td class="confluenceTd">Base namespace in the xwork configuration for the <tt>view</tt> portlet
105 * mode</td><td class="confluenceTd">The default namespace</td>
106 * </tr>
107 * <tr>
108 * <td class="confluenceTd">editNamespace</td><td class="confluenceTd">Base namespace in the xwork configuration for the <tt>edit</tt> portlet
109 * mode</td><td class="confluenceTd">The default namespace</td>
110 * </tr>
111 * <tr>
112 * <td class="confluenceTd">helpNamespace</td><td class="confluenceTd">Base namespace in the xwork configuration for the <tt>help</tt> portlet
113 * mode</td><td class="confluenceTd">The default namespace</td>
114 * </tr>
115 * <tr>
116 * <td class="confluenceTd">defaultViewAction</td><td class="confluenceTd">Default action to invoke in the <tt>view</tt> portlet mode if no action is
117 * specified</td><td class="confluenceTd"><tt>default</tt></td>
118 * </tr>
119 * <tr>
120 * <td class="confluenceTd">defaultEditAction</td><td class="confluenceTd">Default action to invoke in the <tt>edit</tt> portlet mode if no action is
121 * specified</td><td class="confluenceTd"><tt>default</tt></td>
122 * </tr>
123 * <tr>
124 * <td class="confluenceTd">defaultHelpAction</td><td class="confluenceTd">Default action to invoke in the <tt>help</tt> portlet mode if no action is
125 * specified</td><td class="confluenceTd"><tt>default</tt></td>
126 * </tr>
127 * </table>
128 * <!-- END SNIPPET: params -->
129 * <p><b>Example:</b></p>
130 * <pre>
131 * <!-- START SNIPPET: example -->
132 *
133 * <init-param>
134 * <!-- The view mode namespace. Maps to a namespace in the xwork config file -->
135 * <name>viewNamespace</name>
136 * <value>/view</value>
137 * </init-param>
138 * <init-param>
139 * <!-- The default action to invoke in view mode -->
140 * <name>defaultViewAction</name>
141 * <value>index</value>
142 * </init-param>
143 * <init-param>
144 * <!-- The view mode namespace. Maps to a namespace in the xwork config file -->
145 * <name>editNamespace</name>
146 * <value>/edit</value>
147 * </init-param>
148 * <init-param>
149 * <!-- The default action to invoke in view mode -->
150 * <name>defaultEditAction</name>
151 * <value>index</value>
152 * </init-param>
153 * <init-param>
154 * <!-- The view mode namespace. Maps to a namespace in the xwork config file -->
155 * <name>helpNamespace</name>
156 * <value>/help</value>
157 * </init-param>
158 * <init-param>
159 * <!-- The default action to invoke in view mode -->
160 * <name>defaultHelpAction</name>
161 * <value>index</value>
162 * </init-param>
163 *
164 * <!-- END SNIPPET: example -->
165 * </pre>
166 */
167 public class Jsr168Dispatcher extends GenericPortlet implements StrutsStatics,
168 PortletActionConstants {
169
170 private static final Logger LOG = LoggerFactory.getLogger(Jsr168Dispatcher.class);
171
172 private ActionProxyFactory factory = null;
173
174 private Map<PortletMode,String> modeMap = new HashMap<PortletMode,String>(3);
175
176 private Map<PortletMode,ActionMapping> actionMap = new HashMap<PortletMode,ActionMapping>(3);
177
178 private String portletNamespace = null;
179
180 private Dispatcher dispatcherUtils;
181
182 private ActionMapper actionMapper;
183
184 private Container container;
185
186 /***
187 * Initialize the portlet with the init parameters from <tt>portlet.xml</tt>
188 */
189 public void init(PortletConfig cfg) throws PortletException {
190 super.init(cfg);
191 LOG.debug("Initializing portlet " + getPortletName());
192
193 Map<String,String> params = new HashMap<String,String>();
194 for (Enumeration e = cfg.getInitParameterNames(); e.hasMoreElements(); ) {
195 String name = (String) e.nextElement();
196 String value = cfg.getInitParameter(name);
197 params.put(name, value);
198 }
199
200 dispatcherUtils = new Dispatcher(new PortletServletContext(cfg.getPortletContext()), params);
201 dispatcherUtils.init();
202
203
204 if (factory == null) {
205 factory = dispatcherUtils.getConfigurationManager().getConfiguration().getContainer().getInstance(ActionProxyFactory.class);
206 }
207 portletNamespace = cfg.getInitParameter("portletNamespace");
208 LOG.debug("PortletNamespace: " + portletNamespace);
209 parseModeConfig(actionMap, cfg, PortletMode.VIEW, "viewNamespace",
210 "defaultViewAction");
211 parseModeConfig(actionMap, cfg, PortletMode.EDIT, "editNamespace",
212 "defaultEditAction");
213 parseModeConfig(actionMap, cfg, PortletMode.HELP, "helpNamespace",
214 "defaultHelpAction");
215 parseModeConfig(actionMap, cfg, new PortletMode("config"), "configNamespace",
216 "defaultConfigAction");
217 parseModeConfig(actionMap, cfg, new PortletMode("about"), "aboutNamespace",
218 "defaultAboutAction");
219 parseModeConfig(actionMap, cfg, new PortletMode("print"), "printNamespace",
220 "defaultPrintAction");
221 parseModeConfig(actionMap, cfg, new PortletMode("preview"), "previewNamespace",
222 "defaultPreviewAction");
223 parseModeConfig(actionMap, cfg, new PortletMode("edit_defaults"),
224 "editDefaultsNamespace", "defaultEditDefaultsAction");
225 if (StringUtils.isEmpty(portletNamespace)) {
226 portletNamespace = "";
227 }
228 LocalizedTextUtil
229 .addDefaultResourceBundle("org/apache/struts2/struts-messages");
230
231 container = dispatcherUtils.getContainer();
232
233 if ("true".equalsIgnoreCase(container.getInstance(String.class, StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD))) {
234 FileManager.setReloadingConfigs(true);
235 }
236
237 actionMapper = container.getInstance(ActionMapper.class);
238 }
239
240 /***
241 * Parse the mode to namespace mappings configured in portlet.xml
242 * @param actionMap The map with mode <-> default action mapping.
243 * @param portletConfig The PortletConfig.
244 * @param portletMode The PortletMode.
245 * @param nameSpaceParam Name of the init parameter where the namespace for the mode
246 * is configured.
247 * @param defaultActionParam Name of the init parameter where the default action to
248 * execute for the mode is configured.
249 */
250 void parseModeConfig(Map<PortletMode, ActionMapping> actionMap, PortletConfig portletConfig,
251 PortletMode portletMode, String nameSpaceParam,
252 String defaultActionParam) {
253 String namespace = portletConfig.getInitParameter(nameSpaceParam);
254 if (StringUtils.isEmpty(namespace)) {
255 namespace = "";
256 }
257 modeMap.put(portletMode, namespace);
258 String defaultAction = portletConfig
259 .getInitParameter(defaultActionParam);
260 String method = null;
261 if (StringUtils.isEmpty(defaultAction)) {
262 defaultAction = DEFAULT_ACTION_NAME;
263 }
264 if(defaultAction.indexOf('!') >= 0) {
265 method = defaultAction.substring(defaultAction.indexOf('!') + 1);
266 defaultAction = defaultAction.substring(0, defaultAction.indexOf('!'));
267 }
268 StringBuffer fullPath = new StringBuffer();
269 if (StringUtils.isNotEmpty(portletNamespace)) {
270 fullPath.append(portletNamespace);
271 }
272 if (StringUtils.isNotEmpty(namespace)) {
273 fullPath.append(namespace).append("/");
274 } else {
275 fullPath.append("/");
276 }
277 fullPath.append(defaultAction);
278 ActionMapping mapping = new ActionMapping();
279 mapping.setName(getActionName(fullPath.toString()));
280 mapping.setNamespace(getNamespace(fullPath.toString()));
281 if(method != null) {
282 mapping.setMethod(method);
283 }
284 actionMap.put(portletMode, mapping);
285 }
286
287 /***
288 * Service an action from the <tt>event</tt> phase.
289 *
290 * @see javax.portlet.Portlet#processAction(javax.portlet.ActionRequest,
291 * javax.portlet.ActionResponse)
292 */
293 public void processAction(ActionRequest request, ActionResponse response)
294 throws PortletException, IOException {
295 LOG.debug("Entering processAction");
296 resetActionContext();
297 try {
298 serviceAction(request, response, getRequestMap(request), getParameterMap(request),
299 getSessionMap(request), getApplicationMap(),
300 portletNamespace, EVENT_PHASE);
301 LOG.debug("Leaving processAction");
302 } finally {
303 ActionContext.setContext(null);
304 }
305 }
306
307 /***
308 * Service an action from the <tt>render</tt> phase.
309 *
310 * @see javax.portlet.Portlet#render(javax.portlet.RenderRequest,
311 * javax.portlet.RenderResponse)
312 */
313 public void render(RenderRequest request, RenderResponse response)
314 throws PortletException, IOException {
315
316 LOG.debug("Entering render");
317 resetActionContext();
318 response.setTitle(getTitle(request));
319 if(!request.getWindowState().equals(WindowState.MINIMIZED)) {
320 try {
321
322 serviceAction(request, response, getRequestMap(request), getParameterMap(request),
323 getSessionMap(request), getApplicationMap(),
324 portletNamespace, RENDER_PHASE);
325 LOG.debug("Leaving render");
326 } finally {
327 resetActionContext();
328 }
329 }
330 }
331
332 /***
333 * Reset the action context.
334 */
335 private void resetActionContext() {
336 ActionContext.setContext(null);
337 }
338
339 /***
340 * Merges all application and portlet attributes into a single
341 * <tt>HashMap</tt> to represent the entire <tt>Action</tt> context.
342 *
343 * @param requestMap a Map of all request attributes.
344 * @param parameterMap a Map of all request parameters.
345 * @param sessionMap a Map of all session attributes.
346 * @param applicationMap a Map of all servlet context attributes.
347 * @param request the PortletRequest object.
348 * @param response the PortletResponse object.
349 * @param portletConfig the PortletConfig object.
350 * @param phase The portlet phase (render or action, see
351 * {@link PortletActionConstants})
352 * @return a HashMap representing the <tt>Action</tt> context.
353 */
354 public HashMap<String, Object> createContextMap(Map<String, Object> requestMap, Map<String, String[]> parameterMap,
355 Map<String, Object> sessionMap, Map<String, Object> applicationMap, PortletRequest request,
356 PortletResponse response, HttpServletRequest servletRequest, HttpServletResponse servletResponse, ServletContext servletContext, PortletConfig portletConfig, Integer phase) throws IOException {
357
358
359 container.inject(servletRequest);
360
361
362 HashMap<String, Object> extraContext = new HashMap<String, Object>();
363
364 extraContext.put(StrutsStatics.HTTP_REQUEST, servletRequest);
365 extraContext.put(StrutsStatics.HTTP_RESPONSE, servletResponse);
366 extraContext.put(StrutsStatics.SERVLET_CONTEXT, servletContext);
367
368 extraContext.put(ActionContext.PARAMETERS, parameterMap);
369 extraContext.put(ActionContext.SESSION, sessionMap);
370 extraContext.put(ActionContext.APPLICATION, applicationMap);
371
372 String defaultLocale = dispatcherUtils.getContainer().getInstance(String.class, StrutsConstants.STRUTS_LOCALE);
373 Locale locale = null;
374 if (defaultLocale != null) {
375 locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale());
376 } else {
377 locale = request.getLocale();
378 }
379 extraContext.put(ActionContext.LOCALE, locale);
380
381 extraContext.put(StrutsStatics.STRUTS_PORTLET_CONTEXT, getPortletContext());
382 extraContext.put(REQUEST, request);
383 extraContext.put(RESPONSE, response);
384 extraContext.put(PORTLET_CONFIG, portletConfig);
385 extraContext.put(PORTLET_NAMESPACE, portletNamespace);
386 extraContext.put(DEFAULT_ACTION_FOR_MODE, actionMap.get(request.getPortletMode()));
387
388 extraContext.put("request", requestMap);
389 extraContext.put("session", sessionMap);
390 extraContext.put("application", applicationMap);
391 extraContext.put("parameters", parameterMap);
392 extraContext.put(MODE_NAMESPACE_MAP, modeMap);
393
394 extraContext.put(PHASE, phase);
395
396 AttributeMap attrMap = new AttributeMap(extraContext);
397 extraContext.put("attr", attrMap);
398
399 return extraContext;
400 }
401
402 /***
403 * Loads the action and executes it. This method first creates the action
404 * context from the given parameters then loads an <tt>ActionProxy</tt>
405 * from the given action name and namespace. After that, the action is
406 * executed and output channels throught the response object.
407 *
408 * @param request the HttpServletRequest object.
409 * @param response the HttpServletResponse object.
410 * @param requestMap a Map of request attributes.
411 * @param parameterMap a Map of request parameters.
412 * @param sessionMap a Map of all session attributes.
413 * @param applicationMap a Map of all application attributes.
414 * @param portletNamespace the namespace or context of the action.
415 * @param phase The portlet phase (render or action, see
416 * {@link PortletActionConstants})
417 */
418 public void serviceAction(PortletRequest request, PortletResponse response, Map<String, Object> requestMap, Map<String, String[]> parameterMap,
419 Map<String, Object> sessionMap, Map<String, Object> applicationMap, String portletNamespace,
420 Integer phase) throws PortletException {
421 LOG.debug("serviceAction");
422 Dispatcher.setInstance(dispatcherUtils);
423 String actionName = null;
424 String namespace = null;
425 try {
426 ServletContext servletContext = new PortletServletContext(getPortletContext());
427 HttpServletRequest servletRequest = new PortletServletRequest(request, getPortletContext());
428 HttpServletResponse servletResponse = new PortletServletResponse(response);
429 if(EVENT_PHASE.equals(phase)) {
430 servletRequest = dispatcherUtils.wrapRequest(servletRequest, servletContext);
431 if(servletRequest instanceof MultiPartRequestWrapper) {
432
433
434 parameterMap.putAll(servletRequest.getParameterMap());
435 }
436 }
437 container.inject(servletRequest);
438 ActionMapping mapping = getActionMapping(request, servletRequest);
439 actionName = mapping.getName();
440 namespace = mapping.getNamespace();
441 HashMap<String, Object> extraContext = createContextMap(requestMap, parameterMap,
442 sessionMap, applicationMap, request, response, servletRequest, servletResponse,
443 servletContext, getPortletConfig(), phase);
444 extraContext.put(PortletActionConstants.ACTION_MAPPING, mapping);
445 LOG.debug("Creating action proxy for name = " + actionName
446 + ", namespace = " + namespace);
447 ActionProxy proxy = factory.createActionProxy(namespace,
448 actionName, mapping.getMethod(), extraContext);
449 request.setAttribute("struts.valueStack", proxy.getInvocation()
450 .getStack());
451 proxy.execute();
452 } catch (ConfigurationException e) {
453 LOG.error("Could not find action", e);
454 throw new PortletException("Could not find action " + actionName, e);
455 } catch (Exception e) {
456 LOG.error("Could not execute action", e);
457 throw new PortletException("Error executing action " + actionName,
458 e);
459 } finally {
460 Dispatcher.setInstance(null);
461 }
462 }
463
464 /***
465 * Returns a Map of all application attributes. Copies all attributes from
466 * the {@link PortletActionContext}into an {@link ApplicationMap}.
467 *
468 * @return a Map of all application attributes.
469 */
470 protected Map getApplicationMap() {
471 return new PortletApplicationMap(getPortletContext());
472 }
473
474 /***
475 * Gets the namespace of the action from the request. The namespace is the
476 * same as the portlet mode. E.g, view mode is mapped to namespace
477 * <code>view</code>, and edit mode is mapped to the namespace
478 * <code>edit</code>
479 *
480 * @param request the PortletRequest object.
481 * @return the namespace of the action.
482 */
483 protected ActionMapping getActionMapping(final PortletRequest portletRequest, final HttpServletRequest servletRequest) {
484 ActionMapping mapping = null;
485 String actionPath = null;
486 if (resetAction(portletRequest)) {
487 mapping = (ActionMapping) actionMap.get(portletRequest.getPortletMode());
488 } else {
489 actionPath = servletRequest.getParameter(ACTION_PARAM);
490 if (StringUtils.isEmpty(actionPath)) {
491 mapping = (ActionMapping) actionMap.get(portletRequest
492 .getPortletMode());
493 } else {
494
495
496
497
498 mapping = actionMapper.getMapping(servletRequest, dispatcherUtils.getConfigurationManager());
499 }
500 }
501
502 if (mapping == null) {
503 throw new StrutsException("Unable to locate action mapping for request, probably due to " +
504 "an invalid action path: "+actionPath);
505 }
506 return mapping;
507 }
508
509 /***
510 * Get the namespace part of the action path.
511 * @param actionPath Full path to action
512 * @return The namespace part.
513 */
514 String getNamespace(String actionPath) {
515 int idx = actionPath.lastIndexOf('/');
516 String namespace = "";
517 if (idx >= 0) {
518 namespace = actionPath.substring(0, idx);
519 }
520 return namespace;
521 }
522
523 /***
524 * Get the action name part of the action path.
525 * @param actionPath Full path to action
526 * @return The action name.
527 */
528 String getActionName(String actionPath) {
529 int idx = actionPath.lastIndexOf('/');
530 String action = actionPath;
531 if (idx >= 0) {
532 action = actionPath.substring(idx + 1);
533 }
534 return action;
535 }
536
537 /***
538 * Returns a Map of all request parameters. This implementation just calls
539 * {@link PortletRequest#getParameterMap()}.
540 *
541 * @param request the PortletRequest object.
542 * @return a Map of all request parameters.
543 * @throws IOException if an exception occurs while retrieving the parameter
544 * map.
545 */
546 protected Map<String, String[]> getParameterMap(PortletRequest request) throws IOException {
547 return new HashMap<String, String[]>(request.getParameterMap());
548 }
549
550 /***
551 * Returns a Map of all request attributes. The default implementation is to
552 * wrap the request in a {@link RequestMap}. Override this method to
553 * customize how request attributes are mapped.
554 *
555 * @param request the PortletRequest object.
556 * @return a Map of all request attributes.
557 */
558 protected Map getRequestMap(PortletRequest request) {
559 return new PortletRequestMap(request);
560 }
561
562 /***
563 * Returns a Map of all session attributes. The default implementation is to
564 * wrap the reqeust in a {@link SessionMap}. Override this method to
565 * customize how session attributes are mapped.
566 *
567 * @param request the PortletRequest object.
568 * @return a Map of all session attributes.
569 */
570 protected Map getSessionMap(PortletRequest request) {
571 return new PortletSessionMap(request);
572 }
573
574 /***
575 * Convenience method to ease testing.
576 * @param factory
577 */
578 protected void setActionProxyFactory(ActionProxyFactory factory) {
579 this.factory = factory;
580 }
581
582 /***
583 * Check to see if the action parameter is valid for the current portlet mode. If the portlet
584 * mode has been changed with the portal widgets, the action name is invalid, since the
585 * action name belongs to the previous executing portlet mode. If this method evaluates to
586 * <code>true</code> the <code>default<Mode>Action</code> is used instead.
587 * @param request The portlet request.
588 * @return <code>true</code> if the action should be reset.
589 */
590 private boolean resetAction(PortletRequest request) {
591 boolean reset = false;
592 Map paramMap = request.getParameterMap();
593 String[] modeParam = (String[]) paramMap.get(MODE_PARAM);
594 if (modeParam != null && modeParam.length == 1) {
595 String originatingMode = modeParam[0];
596 String currentMode = request.getPortletMode().toString();
597 if (!currentMode.equals(originatingMode)) {
598 reset = true;
599 }
600 }
601 if(reset) {
602 request.setAttribute(ACTION_RESET, Boolean.TRUE);
603 }
604 else {
605 request.setAttribute(ACTION_RESET, Boolean.FALSE);
606 }
607 return reset;
608 }
609
610 public void destroy() {
611 if (dispatcherUtils == null) {
612 LOG.warn("something is seriously wrong, DispatcherUtil is not initialized (null) ");
613 } else {
614 dispatcherUtils.cleanup();
615 }
616 }
617
618 /***
619 * @param actionMapper the actionMapper to set
620 */
621 public void setActionMapper(ActionMapper actionMapper) {
622 this.actionMapper = actionMapper;
623 }
624
625 }