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
66 import com.opensymphony.xwork2.ActionContext;
67 import com.opensymphony.xwork2.ActionProxy;
68 import com.opensymphony.xwork2.ActionProxyFactory;
69 import com.opensymphony.xwork2.config.ConfigurationException;
70 import com.opensymphony.xwork2.inject.Container;
71 import com.opensymphony.xwork2.util.FileManager;
72 import com.opensymphony.xwork2.util.LocalizedTextUtil;
73 import com.opensymphony.xwork2.util.TextUtils;
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 (!TextUtils.stringSet(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 (!TextUtils.stringSet(namespace)) {
255 namespace = "";
256 }
257 modeMap.put(portletMode, namespace);
258 String defaultAction = portletConfig
259 .getInitParameter(defaultActionParam);
260 String method = null;
261 if (!TextUtils.stringSet(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 (TextUtils.stringSet(portletNamespace)) {
270 fullPath.append(portletNamespace);
271 }
272 if (TextUtils.stringSet(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, getActionMapping(request),
299 getRequestMap(request), getParameterMap(request),
300 getSessionMap(request), getApplicationMap(),
301 portletNamespace, EVENT_PHASE);
302 LOG.debug("Leaving processAction");
303 } finally {
304 ActionContext.setContext(null);
305 }
306 }
307
308 /***
309 * Service an action from the <tt>render</tt> phase.
310 *
311 * @see javax.portlet.Portlet#render(javax.portlet.RenderRequest,
312 * javax.portlet.RenderResponse)
313 */
314 public void render(RenderRequest request, RenderResponse response)
315 throws PortletException, IOException {
316
317 LOG.debug("Entering render");
318 resetActionContext();
319 response.setTitle(getTitle(request));
320 if(!request.getWindowState().equals(WindowState.MINIMIZED)) {
321 try {
322
323 serviceAction(request, response, getActionMapping(request),
324 getRequestMap(request), getParameterMap(request),
325 getSessionMap(request), getApplicationMap(),
326 portletNamespace, RENDER_PHASE);
327 LOG.debug("Leaving render");
328 } finally {
329 resetActionContext();
330 }
331 }
332 }
333
334 /***
335 * Reset the action context.
336 */
337 private void resetActionContext() {
338 ActionContext.setContext(null);
339 }
340
341 /***
342 * Merges all application and portlet attributes into a single
343 * <tt>HashMap</tt> to represent the entire <tt>Action</tt> context.
344 *
345 * @param requestMap a Map of all request attributes.
346 * @param parameterMap a Map of all request parameters.
347 * @param sessionMap a Map of all session attributes.
348 * @param applicationMap a Map of all servlet context attributes.
349 * @param request the PortletRequest object.
350 * @param response the PortletResponse object.
351 * @param portletConfig the PortletConfig object.
352 * @param phase The portlet phase (render or action, see
353 * {@link PortletActionConstants})
354 * @return a HashMap representing the <tt>Action</tt> context.
355 */
356 public HashMap<String, Object> createContextMap(Map<String, Object> requestMap, Map<String, String[]> parameterMap,
357 Map<String, Object> sessionMap, Map<String, Object> applicationMap, PortletRequest request,
358 PortletResponse response, PortletConfig portletConfig, Integer phase) throws IOException {
359
360
361 HttpServletResponse dummyResponse = new PortletServletResponse(response);
362 HttpServletRequest dummyRequest = new PortletServletRequest(request, getPortletContext());
363 container.inject(dummyRequest);
364 ServletContext dummyServletContext = new PortletServletContext(getPortletContext());
365 if(EVENT_PHASE.equals(phase)) {
366 dummyRequest = dispatcherUtils.wrapRequest(dummyRequest, dummyServletContext);
367 if(dummyRequest instanceof MultiPartRequestWrapper) {
368
369
370 parameterMap.putAll(dummyRequest.getParameterMap());
371 }
372 }
373
374 HashMap<String, Object> extraContext = new HashMap<String, Object>();
375
376 extraContext.put(StrutsStatics.HTTP_REQUEST, dummyRequest);
377 extraContext.put(StrutsStatics.HTTP_RESPONSE, dummyResponse);
378 extraContext.put(StrutsStatics.SERVLET_CONTEXT, dummyServletContext);
379
380 extraContext.put(ActionContext.PARAMETERS, parameterMap);
381 extraContext.put(ActionContext.SESSION, sessionMap);
382 extraContext.put(ActionContext.APPLICATION, applicationMap);
383
384 String defaultLocale = dispatcherUtils.getContainer().getInstance(String.class, StrutsConstants.STRUTS_LOCALE);
385 Locale locale = null;
386 if (defaultLocale != null) {
387 locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale());
388 } else {
389 locale = request.getLocale();
390 }
391 extraContext.put(ActionContext.LOCALE, locale);
392
393 extraContext.put(StrutsStatics.STRUTS_PORTLET_CONTEXT, getPortletContext());
394 extraContext.put(REQUEST, request);
395 extraContext.put(RESPONSE, response);
396 extraContext.put(PORTLET_CONFIG, portletConfig);
397 extraContext.put(PORTLET_NAMESPACE, portletNamespace);
398 extraContext.put(DEFAULT_ACTION_FOR_MODE, actionMap.get(request.getPortletMode()));
399
400 extraContext.put("request", requestMap);
401 extraContext.put("session", sessionMap);
402 extraContext.put("application", applicationMap);
403 extraContext.put("parameters", parameterMap);
404 extraContext.put(MODE_NAMESPACE_MAP, modeMap);
405
406 extraContext.put(PHASE, phase);
407
408 AttributeMap attrMap = new AttributeMap(extraContext);
409 extraContext.put("attr", attrMap);
410
411 return extraContext;
412 }
413
414 /***
415 * Loads the action and executes it. This method first creates the action
416 * context from the given parameters then loads an <tt>ActionProxy</tt>
417 * from the given action name and namespace. After that, the action is
418 * executed and output channels throught the response object.
419 *
420 * @param request the HttpServletRequest object.
421 * @param response the HttpServletResponse object.
422 * @param mapping the action mapping.
423 * @param requestMap a Map of request attributes.
424 * @param parameterMap a Map of request parameters.
425 * @param sessionMap a Map of all session attributes.
426 * @param applicationMap a Map of all application attributes.
427 * @param portletNamespace the namespace or context of the action.
428 * @param phase The portlet phase (render or action, see
429 * {@link PortletActionConstants})
430 */
431 public void serviceAction(PortletRequest request, PortletResponse response,
432 ActionMapping mapping, Map<String, Object> requestMap, Map<String, String[]> parameterMap,
433 Map<String, Object> sessionMap, Map<String, Object> applicationMap, String portletNamespace,
434 Integer phase) throws PortletException {
435 LOG.debug("serviceAction");
436 String actionName = mapping.getName();
437 String namespace = mapping.getNamespace();
438 Dispatcher.setInstance(dispatcherUtils);
439 try {
440 HashMap<String, Object> extraContext = createContextMap(requestMap, parameterMap,
441 sessionMap, applicationMap, request, response,
442 getPortletConfig(), phase);
443 extraContext.put(PortletActionConstants.ACTION_MAPPING, mapping);
444 LOG.debug("Creating action proxy for name = " + actionName
445 + ", namespace = " + namespace);
446 ActionProxy proxy = factory.createActionProxy(namespace,
447 actionName, mapping.getMethod(), extraContext);
448 request.setAttribute("struts.valueStack", proxy.getInvocation()
449 .getStack());
450 proxy.execute();
451 } catch (ConfigurationException e) {
452 LOG.error("Could not find action", e);
453 throw new PortletException("Could not find action " + actionName, e);
454 } catch (Exception e) {
455 LOG.error("Could not execute action", e);
456 throw new PortletException("Error executing action " + actionName,
457 e);
458 } finally {
459 Dispatcher.setInstance(null);
460 }
461 }
462
463 /***
464 * Returns a Map of all application attributes. Copies all attributes from
465 * the {@link PortletActionContext}into an {@link ApplicationMap}.
466 *
467 * @return a Map of all application attributes.
468 */
469 protected Map getApplicationMap() {
470 return new PortletApplicationMap(getPortletContext());
471 }
472
473 /***
474 * Gets the namespace of the action from the request. The namespace is the
475 * same as the portlet mode. E.g, view mode is mapped to namespace
476 * <code>view</code>, and edit mode is mapped to the namespace
477 * <code>edit</code>
478 *
479 * @param request the PortletRequest object.
480 * @return the namespace of the action.
481 */
482 protected ActionMapping getActionMapping(final PortletRequest request) {
483 ActionMapping mapping = null;
484 String actionPath = null;
485 if (resetAction(request)) {
486 mapping = (ActionMapping) actionMap.get(request.getPortletMode());
487 } else {
488 actionPath = request.getParameter(ACTION_PARAM);
489 if (!TextUtils.stringSet(actionPath)) {
490 mapping = (ActionMapping) actionMap.get(request
491 .getPortletMode());
492 } else {
493
494
495
496
497 PortletServletRequest httpRequest = new PortletServletRequest(request, getPortletContext());
498 container.inject(httpRequest);
499 mapping = actionMapper.getMapping(httpRequest, dispatcherUtils.getConfigurationManager());
500 }
501 }
502
503 if (mapping == null) {
504 throw new StrutsException("Unable to locate action mapping for request, probably due to " +
505 "an invalid action path: "+actionPath);
506 }
507 return mapping;
508 }
509
510 /***
511 * Get the namespace part of the action path.
512 * @param actionPath Full path to action
513 * @return The namespace part.
514 */
515 String getNamespace(String actionPath) {
516 int idx = actionPath.lastIndexOf('/');
517 String namespace = "";
518 if (idx >= 0) {
519 namespace = actionPath.substring(0, idx);
520 }
521 return namespace;
522 }
523
524 /***
525 * Get the action name part of the action path.
526 * @param actionPath Full path to action
527 * @return The action name.
528 */
529 String getActionName(String actionPath) {
530 int idx = actionPath.lastIndexOf('/');
531 String action = actionPath;
532 if (idx >= 0) {
533 action = actionPath.substring(idx + 1);
534 }
535 return action;
536 }
537
538 /***
539 * Returns a Map of all request parameters. This implementation just calls
540 * {@link PortletRequest#getParameterMap()}.
541 *
542 * @param request the PortletRequest object.
543 * @return a Map of all request parameters.
544 * @throws IOException if an exception occurs while retrieving the parameter
545 * map.
546 */
547 protected Map<String, String[]> getParameterMap(PortletRequest request) throws IOException {
548 return new HashMap<String, String[]>(request.getParameterMap());
549 }
550
551 /***
552 * Returns a Map of all request attributes. The default implementation is to
553 * wrap the request in a {@link RequestMap}. Override this method to
554 * customize how request attributes are mapped.
555 *
556 * @param request the PortletRequest object.
557 * @return a Map of all request attributes.
558 */
559 protected Map getRequestMap(PortletRequest request) {
560 return new PortletRequestMap(request);
561 }
562
563 /***
564 * Returns a Map of all session attributes. The default implementation is to
565 * wrap the reqeust in a {@link SessionMap}. Override this method to
566 * customize how session attributes are mapped.
567 *
568 * @param request the PortletRequest object.
569 * @return a Map of all session attributes.
570 */
571 protected Map getSessionMap(PortletRequest request) {
572 return new PortletSessionMap(request);
573 }
574
575 /***
576 * Convenience method to ease testing.
577 * @param factory
578 */
579 protected void setActionProxyFactory(ActionProxyFactory factory) {
580 this.factory = factory;
581 }
582
583 /***
584 * Check to see if the action parameter is valid for the current portlet mode. If the portlet
585 * mode has been changed with the portal widgets, the action name is invalid, since the
586 * action name belongs to the previous executing portlet mode. If this method evaluates to
587 * <code>true</code> the <code>default<Mode>Action</code> is used instead.
588 * @param request The portlet request.
589 * @return <code>true</code> if the action should be reset.
590 */
591 private boolean resetAction(PortletRequest request) {
592 boolean reset = false;
593 Map paramMap = request.getParameterMap();
594 String[] modeParam = (String[]) paramMap.get(MODE_PARAM);
595 if (modeParam != null && modeParam.length == 1) {
596 String originatingMode = modeParam[0];
597 String currentMode = request.getPortletMode().toString();
598 if (!currentMode.equals(originatingMode)) {
599 reset = true;
600 }
601 }
602 if(reset) {
603 request.setAttribute(ACTION_RESET, Boolean.TRUE);
604 }
605 else {
606 request.setAttribute(ACTION_RESET, Boolean.FALSE);
607 }
608 return reset;
609 }
610
611 public void destroy() {
612 if (dispatcherUtils == null) {
613 LOG.warn("something is seriously wrong, DispatcherUtil is not initialized (null) ");
614 } else {
615 dispatcherUtils.cleanup();
616 }
617 }
618
619 /***
620 * @param actionMapper the actionMapper to set
621 */
622 public void setActionMapper(ActionMapper actionMapper) {
623 this.actionMapper = actionMapper;
624 }
625
626 }