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