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