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 /***
185 * Initialize the portlet with the init parameters from <tt>portlet.xml</tt>
186 */
187 public void init(PortletConfig cfg) throws PortletException {
188 super.init(cfg);
189 LOG.debug("Initializing portlet " + getPortletName());
190
191 Map<String,String> params = new HashMap<String,String>();
192 for (Enumeration e = cfg.getInitParameterNames(); e.hasMoreElements(); ) {
193 String name = (String) e.nextElement();
194 String value = cfg.getInitParameter(name);
195 params.put(name, value);
196 }
197
198 dispatcherUtils = new Dispatcher(new PortletServletContext(cfg.getPortletContext()), params);
199 dispatcherUtils.init();
200
201
202 if (factory == null) {
203 factory = dispatcherUtils.getConfigurationManager().getConfiguration().getContainer().getInstance(ActionProxyFactory.class);
204 }
205 portletNamespace = cfg.getInitParameter("portletNamespace");
206 LOG.debug("PortletNamespace: " + portletNamespace);
207 parseModeConfig(actionMap, cfg, PortletMode.VIEW, "viewNamespace",
208 "defaultViewAction");
209 parseModeConfig(actionMap, cfg, PortletMode.EDIT, "editNamespace",
210 "defaultEditAction");
211 parseModeConfig(actionMap, cfg, PortletMode.HELP, "helpNamespace",
212 "defaultHelpAction");
213 parseModeConfig(actionMap, cfg, new PortletMode("config"), "configNamespace",
214 "defaultConfigAction");
215 parseModeConfig(actionMap, cfg, new PortletMode("about"), "aboutNamespace",
216 "defaultAboutAction");
217 parseModeConfig(actionMap, cfg, new PortletMode("print"), "printNamespace",
218 "defaultPrintAction");
219 parseModeConfig(actionMap, cfg, new PortletMode("preview"), "previewNamespace",
220 "defaultPreviewAction");
221 parseModeConfig(actionMap, cfg, new PortletMode("edit_defaults"),
222 "editDefaultsNamespace", "defaultEditDefaultsAction");
223 if (!TextUtils.stringSet(portletNamespace)) {
224 portletNamespace = "";
225 }
226 LocalizedTextUtil
227 .addDefaultResourceBundle("org/apache/struts2/struts-messages");
228
229 Container container = dispatcherUtils.getContainer();
230
231 if ("true".equalsIgnoreCase(container.getInstance(String.class, StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD))) {
232 FileManager.setReloadingConfigs(true);
233 }
234
235 actionMapper = container.getInstance(ActionMapper.class);
236 }
237
238 /***
239 * Parse the mode to namespace mappings configured in portlet.xml
240 * @param actionMap The map with mode <-> default action mapping.
241 * @param portletConfig The PortletConfig.
242 * @param portletMode The PortletMode.
243 * @param nameSpaceParam Name of the init parameter where the namespace for the mode
244 * is configured.
245 * @param defaultActionParam Name of the init parameter where the default action to
246 * execute for the mode is configured.
247 */
248 void parseModeConfig(Map<PortletMode, ActionMapping> actionMap, PortletConfig portletConfig,
249 PortletMode portletMode, String nameSpaceParam,
250 String defaultActionParam) {
251 String namespace = portletConfig.getInitParameter(nameSpaceParam);
252 if (!TextUtils.stringSet(namespace)) {
253 namespace = "";
254 }
255 modeMap.put(portletMode, namespace);
256 String defaultAction = portletConfig
257 .getInitParameter(defaultActionParam);
258 String method = null;
259 if (!TextUtils.stringSet(defaultAction)) {
260 defaultAction = DEFAULT_ACTION_NAME;
261 }
262 if(defaultAction.indexOf('!') >= 0) {
263 method = defaultAction.substring(defaultAction.indexOf('!') + 1);
264 defaultAction = defaultAction.substring(0, defaultAction.indexOf('!'));
265 }
266 StringBuffer fullPath = new StringBuffer();
267 if (TextUtils.stringSet(portletNamespace)) {
268 fullPath.append(portletNamespace);
269 }
270 if (TextUtils.stringSet(namespace)) {
271 fullPath.append(namespace).append("/");
272 } else {
273 fullPath.append("/");
274 }
275 fullPath.append(defaultAction);
276 ActionMapping mapping = new ActionMapping();
277 mapping.setName(getActionName(fullPath.toString()));
278 mapping.setNamespace(getNamespace(fullPath.toString()));
279 if(method != null) {
280 mapping.setMethod(method);
281 }
282 actionMap.put(portletMode, mapping);
283 }
284
285 /***
286 * Service an action from the <tt>event</tt> phase.
287 *
288 * @see javax.portlet.Portlet#processAction(javax.portlet.ActionRequest,
289 * javax.portlet.ActionResponse)
290 */
291 public void processAction(ActionRequest request, ActionResponse response)
292 throws PortletException, IOException {
293 LOG.debug("Entering processAction");
294 resetActionContext();
295 try {
296 serviceAction(request, response, getActionMapping(request),
297 getRequestMap(request), getParameterMap(request),
298 getSessionMap(request), getApplicationMap(),
299 portletNamespace, EVENT_PHASE);
300 LOG.debug("Leaving processAction");
301 } finally {
302 ActionContext.setContext(null);
303 }
304 }
305
306 /***
307 * Service an action from the <tt>render</tt> phase.
308 *
309 * @see javax.portlet.Portlet#render(javax.portlet.RenderRequest,
310 * javax.portlet.RenderResponse)
311 */
312 public void render(RenderRequest request, RenderResponse response)
313 throws PortletException, IOException {
314
315 LOG.debug("Entering render");
316 resetActionContext();
317 response.setTitle(getTitle(request));
318 if(!request.getWindowState().equals(WindowState.MINIMIZED)) {
319 try {
320
321 serviceAction(request, response, getActionMapping(request),
322 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 createContextMap(Map requestMap, Map parameterMap,
355 Map sessionMap, Map applicationMap, PortletRequest request,
356 PortletResponse response, PortletConfig portletConfig, Integer phase) throws IOException {
357
358
359 HttpServletResponse dummyResponse = new PortletServletResponse(response);
360 HttpServletRequest dummyRequest = new PortletServletRequest(request, getPortletContext());
361 ServletContext dummyServletContext = new PortletServletContext(getPortletContext());
362 if(EVENT_PHASE.equals(phase)) {
363 dummyRequest = dispatcherUtils.wrapRequest(dummyRequest, dummyServletContext);
364 if(dummyRequest instanceof MultiPartRequestWrapper) {
365
366
367 parameterMap.putAll(dummyRequest.getParameterMap());
368 }
369 }
370
371 HashMap<String,Object> extraContext = new HashMap<String,Object>();
372
373 extraContext.put(StrutsStatics.HTTP_REQUEST, dummyRequest);
374 extraContext.put(StrutsStatics.HTTP_RESPONSE, dummyResponse);
375 extraContext.put(StrutsStatics.SERVLET_CONTEXT, dummyServletContext);
376
377 extraContext.put(ActionContext.PARAMETERS, parameterMap);
378 extraContext.put(ActionContext.SESSION, sessionMap);
379 extraContext.put(ActionContext.APPLICATION, applicationMap);
380
381 String defaultLocale = dispatcherUtils.getContainer().getInstance(String.class, StrutsConstants.STRUTS_LOCALE);
382 Locale locale = null;
383 if (defaultLocale != null) {
384 locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale());
385 } else {
386 locale = request.getLocale();
387 }
388 extraContext.put(ActionContext.LOCALE, locale);
389
390 extraContext.put(StrutsStatics.STRUTS_PORTLET_CONTEXT, getPortletContext());
391 extraContext.put(REQUEST, request);
392 extraContext.put(RESPONSE, response);
393 extraContext.put(PORTLET_CONFIG, portletConfig);
394 extraContext.put(PORTLET_NAMESPACE, portletNamespace);
395 extraContext.put(DEFAULT_ACTION_FOR_MODE, actionMap.get(request.getPortletMode()));
396
397 extraContext.put("request", requestMap);
398 extraContext.put("session", sessionMap);
399 extraContext.put("application", applicationMap);
400 extraContext.put("parameters", parameterMap);
401 extraContext.put(MODE_NAMESPACE_MAP, modeMap);
402
403 extraContext.put(PHASE, phase);
404
405 AttributeMap attrMap = new AttributeMap(extraContext);
406 extraContext.put("attr", attrMap);
407
408 return extraContext;
409 }
410
411 /***
412 * Loads the action and executes it. This method first creates the action
413 * context from the given parameters then loads an <tt>ActionProxy</tt>
414 * from the given action name and namespace. After that, the action is
415 * executed and output channels throught the response object.
416 *
417 * @param request the HttpServletRequest object.
418 * @param response the HttpServletResponse object.
419 * @param mapping the action mapping.
420 * @param requestMap a Map of request attributes.
421 * @param parameterMap a Map of request parameters.
422 * @param sessionMap a Map of all session attributes.
423 * @param applicationMap a Map of all application attributes.
424 * @param portletNamespace the namespace or context of the action.
425 * @param phase The portlet phase (render or action, see
426 * {@link PortletActionConstants})
427 */
428 public void serviceAction(PortletRequest request, PortletResponse response,
429 ActionMapping mapping, Map requestMap, Map parameterMap,
430 Map sessionMap, Map applicationMap, String portletNamespace,
431 Integer phase) throws PortletException {
432 LOG.debug("serviceAction");
433 String actionName = mapping.getName();
434 String namespace = mapping.getNamespace();
435 Dispatcher.setInstance(dispatcherUtils);
436 try {
437 HashMap extraContext = createContextMap(requestMap, parameterMap,
438 sessionMap, applicationMap, request, response,
439 getPortletConfig(), phase);
440 LOG.debug("Creating action proxy for name = " + actionName
441 + ", namespace = " + namespace);
442 ActionProxy proxy = factory.createActionProxy(namespace,
443 actionName, mapping.getMethod(), extraContext);
444 request.setAttribute("struts.valueStack", proxy.getInvocation()
445 .getStack());
446 proxy.execute();
447 } catch (ConfigurationException e) {
448 LOG.error("Could not find action", e);
449 throw new PortletException("Could not find action " + actionName, e);
450 } catch (Exception e) {
451 LOG.error("Could not execute action", e);
452 throw new PortletException("Error executing action " + actionName,
453 e);
454 } finally {
455 Dispatcher.setInstance(null);
456 }
457 }
458
459 /***
460 * Returns a Map of all application attributes. Copies all attributes from
461 * the {@link PortletActionContext}into an {@link ApplicationMap}.
462 *
463 * @return a Map of all application attributes.
464 */
465 protected Map getApplicationMap() {
466 return new PortletApplicationMap(getPortletContext());
467 }
468
469 /***
470 * Gets the namespace of the action from the request. The namespace is the
471 * same as the portlet mode. E.g, view mode is mapped to namespace
472 * <code>view</code>, and edit mode is mapped to the namespace
473 * <code>edit</code>
474 *
475 * @param request the PortletRequest object.
476 * @return the namespace of the action.
477 */
478 protected ActionMapping getActionMapping(final PortletRequest request) {
479 ActionMapping mapping = null;
480 String actionPath = null;
481 if (resetAction(request)) {
482 mapping = (ActionMapping) actionMap.get(request.getPortletMode());
483 } else {
484 actionPath = request.getParameter(ACTION_PARAM);
485 if (!TextUtils.stringSet(actionPath)) {
486 mapping = (ActionMapping) actionMap.get(request
487 .getPortletMode());
488 } else {
489
490
491
492
493 PortletServletRequest httpRequest = new PortletServletRequest(request, getPortletContext());
494 mapping = actionMapper.getMapping(httpRequest, dispatcherUtils.getConfigurationManager());
495 }
496 }
497
498 if (mapping == null) {
499 throw new StrutsException("Unable to locate action mapping for request, probably due to " +
500 "an invalid action path: "+actionPath);
501 }
502 return mapping;
503 }
504
505 /***
506 * Get the namespace part of the action path.
507 * @param actionPath Full path to action
508 * @return The namespace part.
509 */
510 String getNamespace(String actionPath) {
511 int idx = actionPath.lastIndexOf('/');
512 String namespace = "";
513 if (idx >= 0) {
514 namespace = actionPath.substring(0, idx);
515 }
516 return namespace;
517 }
518
519 /***
520 * Get the action name part of the action path.
521 * @param actionPath Full path to action
522 * @return The action name.
523 */
524 String getActionName(String actionPath) {
525 int idx = actionPath.lastIndexOf('/');
526 String action = actionPath;
527 if (idx >= 0) {
528 action = actionPath.substring(idx + 1);
529 }
530 return action;
531 }
532
533 /***
534 * Returns a Map of all request parameters. This implementation just calls
535 * {@link PortletRequest#getParameterMap()}.
536 *
537 * @param request the PortletRequest object.
538 * @return a Map of all request parameters.
539 * @throws IOException if an exception occurs while retrieving the parameter
540 * map.
541 */
542 protected Map getParameterMap(PortletRequest request) throws IOException {
543 return new HashMap(request.getParameterMap());
544 }
545
546 /***
547 * Returns a Map of all request attributes. The default implementation is to
548 * wrap the request in a {@link RequestMap}. Override this method to
549 * customize how request attributes are mapped.
550 *
551 * @param request the PortletRequest object.
552 * @return a Map of all request attributes.
553 */
554 protected Map getRequestMap(PortletRequest request) {
555 return new PortletRequestMap(request);
556 }
557
558 /***
559 * Returns a Map of all session attributes. The default implementation is to
560 * wrap the reqeust in a {@link SessionMap}. Override this method to
561 * customize how session attributes are mapped.
562 *
563 * @param request the PortletRequest object.
564 * @return a Map of all session attributes.
565 */
566 protected Map getSessionMap(PortletRequest request) {
567 return new PortletSessionMap(request);
568 }
569
570 /***
571 * Convenience method to ease testing.
572 * @param factory
573 */
574 protected void setActionProxyFactory(ActionProxyFactory factory) {
575 this.factory = factory;
576 }
577
578 /***
579 * Check to see if the action parameter is valid for the current portlet mode. If the portlet
580 * mode has been changed with the portal widgets, the action name is invalid, since the
581 * action name belongs to the previous executing portlet mode. If this method evaluates to
582 * <code>true</code> the <code>default<Mode>Action</code> is used instead.
583 * @param request The portlet request.
584 * @return <code>true</code> if the action should be reset.
585 */
586 private boolean resetAction(PortletRequest request) {
587 boolean reset = false;
588 Map paramMap = request.getParameterMap();
589 String[] modeParam = (String[]) paramMap.get(MODE_PARAM);
590 if (modeParam != null && modeParam.length == 1) {
591 String originatingMode = modeParam[0];
592 String currentMode = request.getPortletMode().toString();
593 if (!currentMode.equals(originatingMode)) {
594 reset = true;
595 }
596 }
597 if(reset) {
598 request.setAttribute(ACTION_RESET, Boolean.TRUE);
599 }
600 else {
601 request.setAttribute(ACTION_RESET, Boolean.FALSE);
602 }
603 return reset;
604 }
605
606 public void destroy() {
607 if (dispatcherUtils == null) {
608 LOG.warn("something is seriously wrong, DispatcherUtil is not initialized (null) ");
609 } else {
610 dispatcherUtils.cleanup();
611 }
612 }
613
614 /***
615 * @param actionMapper the actionMapper to set
616 */
617 public void setActionMapper(ActionMapper actionMapper) {
618 this.actionMapper = actionMapper;
619 }
620
621 }