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