001: /*
002: * $Id: Jsr168Dispatcher.java 569305 2007-08-24 09:13:29Z nilsga $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.portlet.dispatcher;
022:
023: import com.opensymphony.xwork2.ActionContext;
024: import com.opensymphony.xwork2.ActionProxy;
025: import com.opensymphony.xwork2.ActionProxyFactory;
026: import com.opensymphony.xwork2.config.ConfigurationException;
027: import com.opensymphony.xwork2.inject.Container;
028: import com.opensymphony.xwork2.util.FileManager;
029: import com.opensymphony.xwork2.util.LocalizedTextUtil;
030: import com.opensymphony.xwork2.util.TextUtils;
031: import org.apache.commons.logging.Log;
032: import org.apache.commons.logging.LogFactory;
033: import org.apache.struts2.StrutsConstants;
034: import org.apache.struts2.StrutsException;
035: import org.apache.struts2.StrutsStatics;
036: import org.apache.struts2.dispatcher.ApplicationMap;
037: import org.apache.struts2.dispatcher.Dispatcher;
038: import org.apache.struts2.dispatcher.RequestMap;
039: import org.apache.struts2.dispatcher.SessionMap;
040: import org.apache.struts2.dispatcher.mapper.ActionMapper;
041: import org.apache.struts2.dispatcher.mapper.ActionMapping;
042: import org.apache.struts2.portlet.PortletActionConstants;
043: import org.apache.struts2.portlet.PortletApplicationMap;
044: import org.apache.struts2.portlet.PortletRequestMap;
045: import org.apache.struts2.portlet.PortletSessionMap;
046: import org.apache.struts2.portlet.context.PortletActionContext;
047: import org.apache.struts2.portlet.servlet.PortletServletContext;
048: import org.apache.struts2.portlet.servlet.PortletServletRequest;
049: import org.apache.struts2.portlet.servlet.PortletServletResponse;
050: import org.apache.struts2.util.AttributeMap;
051:
052: import javax.portlet.*;
053: import javax.servlet.ServletContext;
054: import javax.servlet.http.HttpServletRequest;
055: import javax.servlet.http.HttpServletResponse;
056: import java.io.IOException;
057: import java.util.Enumeration;
058: import java.util.HashMap;
059: import java.util.Locale;
060: import java.util.Map;
061:
062: /**
063: * <!-- START SNIPPET: javadoc -->
064: * <p>
065: * Struts JSR-168 portlet dispatcher. Similar to the WW2 Servlet dispatcher,
066: * but adjusted to a portal environment. The portlet is configured through the <tt>portlet.xml</tt>
067: * descriptor. Examples and descriptions follow below:
068: * </p>
069: * <!-- END SNIPPET: javadoc -->
070: *
071: * @author Nils-Helge Garli
072: * @author Rainer Hermanns
073: *
074: * <p><b>Init parameters</b></p>
075: * <!-- START SNIPPET: params -->
076: * <table class="confluenceTable">
077: * <tr>
078: * <th class="confluenceTh">Name</th>
079: * <th class="confluenceTh">Description</th>
080: * <th class="confluenceTh">Default value</th>
081: * </tr>
082: * <tr>
083: * <td class="confluenceTd">portletNamespace</td><td class="confluenceTd">The namespace for the portlet in the xwork configuration. This
084: * namespace is prepended to all action lookups, and makes it possible to host multiple
085: * portlets in the same portlet application. If this parameter is set, the complete namespace
086: * will be <tt>/portletNamespace/modeNamespace/actionName</tt></td><td class="confluenceTd">The default namespace</td>
087: * </tr>
088: * <tr>
089: * <td class="confluenceTd">viewNamespace</td><td class="confluenceTd">Base namespace in the xwork configuration for the <tt>view</tt> portlet
090: * mode</td><td class="confluenceTd">The default namespace</td>
091: * </tr>
092: * <tr>
093: * <td class="confluenceTd">editNamespace</td><td class="confluenceTd">Base namespace in the xwork configuration for the <tt>edit</tt> portlet
094: * mode</td><td class="confluenceTd">The default namespace</td>
095: * </tr>
096: * <tr>
097: * <td class="confluenceTd">helpNamespace</td><td class="confluenceTd">Base namespace in the xwork configuration for the <tt>help</tt> portlet
098: * mode</td><td class="confluenceTd">The default namespace</td>
099: * </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
153: StrutsStatics, PortletActionConstants {
154:
155: private static final Log LOG = LogFactory
156: .getLog(Jsr168Dispatcher.class);
157:
158: private ActionProxyFactory factory = null;
159:
160: private Map<PortletMode, String> modeMap = new HashMap<PortletMode, String>(
161: 3);
162:
163: private Map<PortletMode, ActionMapping> actionMap = new HashMap<PortletMode, ActionMapping>(
164: 3);
165:
166: private String portletNamespace = null;
167:
168: private Dispatcher dispatcherUtils;
169:
170: private ActionMapper actionMapper;
171:
172: /**
173: * Initialize the portlet with the init parameters from <tt>portlet.xml</tt>
174: */
175: public void init(PortletConfig cfg) throws PortletException {
176: super .init(cfg);
177: LOG.debug("Initializing portlet " + getPortletName());
178:
179: Map<String, String> params = new HashMap<String, String>();
180: for (Enumeration e = cfg.getInitParameterNames(); e
181: .hasMoreElements();) {
182: String name = (String) e.nextElement();
183: String value = cfg.getInitParameter(name);
184: params.put(name, value);
185: }
186:
187: // TODO: CHECK IF NEEDED (RG Portlet Refactoring Backports)
188: Dispatcher.setPortletSupportActive(true);
189: dispatcherUtils = new Dispatcher(new PortletServletContext(cfg
190: .getPortletContext()), params);
191: dispatcherUtils.init();
192:
193: // For testability
194: if (factory == null) {
195: factory = dispatcherUtils.getConfigurationManager()
196: .getConfiguration().getContainer().getInstance(
197: 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"),
208: "configNamespace", "defaultConfigAction");
209: parseModeConfig(cfg, new PortletMode("about"),
210: "aboutNamespace", "defaultAboutAction");
211: parseModeConfig(cfg, new PortletMode("print"),
212: "printNamespace", "defaultPrintAction");
213: parseModeConfig(cfg, new PortletMode("preview"),
214: "previewNamespace", "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: //check for configuration reloading
225: if ("true".equalsIgnoreCase(container.getInstance(String.class,
226: StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD))) {
227: FileManager.setReloadingConfigs(true);
228: }
229:
230: actionMapper = container.getInstance(ActionMapper.class);
231: }
232:
233: /**
234: * Parse the mode to namespace mappings configured in portlet.xml
235: * @param portletConfig The PortletConfig
236: * @param portletMode The PortletMode
237: * @param nameSpaceParam Name of the init parameter where the namespace for the mode
238: * is configured.
239: * @param defaultActionParam Name of the init parameter where the default action to
240: * execute for the mode is configured.
241: */
242: private void parseModeConfig(PortletConfig portletConfig,
243: PortletMode portletMode, String nameSpaceParam,
244: String defaultActionParam) {
245: String namespace = portletConfig
246: .getInitParameter(nameSpaceParam);
247: if (!TextUtils.stringSet(namespace)) {
248: namespace = "";
249: }
250: modeMap.put(portletMode, namespace);
251: String defaultAction = portletConfig
252: .getInitParameter(defaultActionParam);
253: if (!TextUtils.stringSet(defaultAction)) {
254: defaultAction = DEFAULT_ACTION_NAME;
255: }
256: StringBuffer fullPath = new StringBuffer();
257: if (TextUtils.stringSet(portletNamespace)) {
258: fullPath.append(portletNamespace);
259: }
260: if (TextUtils.stringSet(namespace)) {
261: fullPath.append(namespace).append("/");
262: } else {
263: fullPath.append("/");
264: }
265: fullPath.append(defaultAction);
266: ActionMapping mapping = new ActionMapping();
267: mapping.setName(getActionName(fullPath.toString()));
268: mapping.setNamespace(getNamespace(fullPath.toString()));
269: actionMap.put(portletMode, mapping);
270: }
271:
272: /**
273: * Service an action from the <tt>event</tt> phase.
274: *
275: * @see javax.portlet.Portlet#processAction(javax.portlet.ActionRequest,
276: * javax.portlet.ActionResponse)
277: */
278: public void processAction(ActionRequest request,
279: ActionResponse response) throws PortletException,
280: IOException {
281: LOG.debug("Entering processAction");
282: resetActionContext();
283: try {
284: serviceAction(request, response, getActionMapping(request),
285: getRequestMap(request), getParameterMap(request),
286: getSessionMap(request), getApplicationMap(),
287: portletNamespace, EVENT_PHASE);
288: LOG.debug("Leaving processAction");
289: } finally {
290: ActionContext.setContext(null);
291: }
292: }
293:
294: /**
295: * Service an action from the <tt>render</tt> phase.
296: *
297: * @see javax.portlet.Portlet#render(javax.portlet.RenderRequest,
298: * javax.portlet.RenderResponse)
299: */
300: public void render(RenderRequest request, RenderResponse response)
301: throws PortletException, IOException {
302:
303: LOG.debug("Entering render");
304: resetActionContext();
305: response.setTitle(getTitle(request));
306: if (!request.getWindowState().equals(WindowState.MINIMIZED)) {
307: try {
308: // Check to see if an event set the render to be included directly
309: serviceAction(request, response,
310: getActionMapping(request),
311: getRequestMap(request),
312: getParameterMap(request),
313: getSessionMap(request), getApplicationMap(),
314: portletNamespace, RENDER_PHASE);
315: LOG.debug("Leaving render");
316: } finally {
317: resetActionContext();
318: }
319: }
320: }
321:
322: /**
323: * Reset the action context.
324: */
325: private void resetActionContext() {
326: ActionContext.setContext(null);
327: }
328:
329: /**
330: * Merges all application and portlet attributes into a single
331: * <tt>HashMap</tt> to represent the entire <tt>Action</tt> context.
332: *
333: * @param requestMap a Map of all request attributes.
334: * @param parameterMap a Map of all request parameters.
335: * @param sessionMap a Map of all session attributes.
336: * @param applicationMap a Map of all servlet context attributes.
337: * @param request the PortletRequest object.
338: * @param response the PortletResponse object.
339: * @param portletConfig the PortletConfig object.
340: * @param phase The portlet phase (render or action, see
341: * {@link PortletActionConstants})
342: * @return a HashMap representing the <tt>Action</tt> context.
343: */
344: public HashMap createContextMap(Map requestMap, Map parameterMap,
345: Map sessionMap, Map applicationMap, PortletRequest request,
346: PortletResponse response, PortletConfig portletConfig,
347: Integer phase) throws IOException {
348:
349: // TODO Must put http request/response objects into map for use with
350: // ServletActionContext
351: HttpServletResponse dummyResponse = new PortletServletResponse(
352: response);
353: HttpServletRequest dummyRequest = new PortletServletRequest(
354: request, getPortletContext());
355: ServletContext dummyServletContext = new PortletServletContext(
356: getPortletContext());
357: if (EVENT_PHASE.equals(phase)) {
358: dummyRequest = dispatcherUtils.wrapRequest(dummyRequest,
359: dummyServletContext);
360: }
361: HashMap<String, Object> extraContext = new HashMap<String, Object>();
362: // The dummy servlet objects. Eases reuse of existing interceptors that uses the servlet objects.
363: extraContext.put(StrutsStatics.HTTP_REQUEST, dummyRequest);
364: extraContext.put(StrutsStatics.HTTP_RESPONSE, dummyResponse);
365: extraContext.put(StrutsStatics.SERVLET_CONTEXT,
366: dummyServletContext);
367: // End dummy servlet objects
368: extraContext.put(ActionContext.PARAMETERS, parameterMap);
369: extraContext.put(ActionContext.SESSION, sessionMap);
370: extraContext.put(ActionContext.APPLICATION, applicationMap);
371:
372: String defaultLocale = dispatcherUtils.getContainer()
373: .getInstance(String.class,
374: StrutsConstants.STRUTS_LOCALE);
375: Locale locale = null;
376: if (defaultLocale != null) {
377: locale = LocalizedTextUtil.localeFromString(defaultLocale,
378: request.getLocale());
379: } else {
380: locale = request.getLocale();
381: }
382: extraContext.put(ActionContext.LOCALE, locale);
383:
384: extraContext.put(StrutsStatics.STRUTS_PORTLET_CONTEXT,
385: getPortletContext());
386: extraContext.put(REQUEST, request);
387: extraContext.put(RESPONSE, response);
388: extraContext.put(PORTLET_CONFIG, portletConfig);
389: extraContext.put(PORTLET_NAMESPACE, portletNamespace);
390: extraContext.put(DEFAULT_ACTION_FOR_MODE, actionMap.get(request
391: .getPortletMode()));
392: // helpers to get access to request/session/application scope
393: extraContext.put("request", requestMap);
394: extraContext.put("session", sessionMap);
395: extraContext.put("application", applicationMap);
396: extraContext.put("parameters", parameterMap);
397: extraContext.put(MODE_NAMESPACE_MAP, modeMap);
398:
399: extraContext.put(PHASE, phase);
400:
401: AttributeMap attrMap = new AttributeMap(extraContext);
402: extraContext.put("attr", attrMap);
403:
404: return extraContext;
405: }
406:
407: /**
408: * Loads the action and executes it. This method first creates the action
409: * context from the given parameters then loads an <tt>ActionProxy</tt>
410: * from the given action name and namespace. After that, the action is
411: * executed and output channels throught the response object.
412: *
413: * @param request the HttpServletRequest object.
414: * @param response the HttpServletResponse object.
415: * @param mapping the action mapping.
416: * @param requestMap a Map of request attributes.
417: * @param parameterMap a Map of request parameters.
418: * @param sessionMap a Map of all session attributes.
419: * @param applicationMap a Map of all application attributes.
420: * @param portletNamespace the namespace or context of the action.
421: * @param phase The portlet phase (render or action, see
422: * {@link PortletActionConstants})
423: */
424: public void serviceAction(PortletRequest request,
425: PortletResponse response, ActionMapping mapping,
426: Map requestMap, Map parameterMap, Map sessionMap,
427: Map applicationMap, String portletNamespace, Integer phase)
428: throws PortletException {
429: LOG.debug("serviceAction");
430: String actionName = mapping.getName();
431: String namespace = mapping.getNamespace();
432: Dispatcher.setInstance(dispatcherUtils);
433: try {
434: HashMap extraContext = createContextMap(requestMap,
435: parameterMap, sessionMap, applicationMap, request,
436: response, getPortletConfig(), phase);
437: LOG.debug("Creating action proxy for name = " + actionName
438: + ", namespace = " + namespace);
439: ActionProxy proxy = factory.createActionProxy(namespace,
440: actionName, extraContext);
441: proxy.setMethod(mapping.getMethod());
442: request.setAttribute("struts.valueStack", proxy
443: .getInvocation().getStack());
444: proxy.execute();
445: } catch (ConfigurationException e) {
446: LOG.error("Could not find action", e);
447: throw new PortletException("Could not find action "
448: + actionName, e);
449: } catch (Exception e) {
450: LOG.error("Could not execute action", e);
451: throw new PortletException("Error executing action "
452: + actionName, e);
453: } finally {
454: Dispatcher.setInstance(null);
455: }
456: }
457:
458: /**
459: * Returns a Map of all application attributes. Copies all attributes from
460: * the {@link PortletActionContext}into an {@link ApplicationMap}.
461: *
462: * @return a Map of all application attributes.
463: */
464: protected Map getApplicationMap() {
465: return new PortletApplicationMap(getPortletContext());
466: }
467:
468: /**
469: * Gets the namespace of the action from the request. The namespace is the
470: * same as the portlet mode. E.g, view mode is mapped to namespace
471: * <code>view</code>, and edit mode is mapped to the namespace
472: * <code>edit</code>
473: *
474: * @param request the PortletRequest object.
475: * @return the namespace of the action.
476: */
477: protected ActionMapping getActionMapping(
478: final PortletRequest request) {
479: ActionMapping mapping = null;
480: String actionPath = null;
481: if (resetAction(request)) {
482: mapping = actionMap.get(request.getPortletMode());
483: } else {
484: actionPath = request.getParameter(ACTION_PARAM);
485: if (!TextUtils.stringSet(actionPath)) {
486: mapping = actionMap.get(request.getPortletMode());
487: } else {
488:
489: // Use the usual action mapper, but it is expecting an action extension
490: // on the uri, so we add the default one, which should be ok as the
491: // portlet is a portlet first, a servlet second
492: PortletServletRequest httpRequest = new PortletServletRequest(
493: request, getPortletContext());
494: mapping = actionMapper.getMapping(httpRequest,
495: dispatcherUtils.getConfigurationManager());
496: }
497: }
498:
499: if (mapping == null) {
500: throw new StrutsException(
501: "Unable to locate action mapping for request, probably due to "
502: + "an invalid action path: " + actionPath);
503: }
504: return mapping;
505: }
506:
507: /**
508: * Get the namespace part of the action path.
509: * @param actionPath Full path to action
510: * @return The namespace part.
511: */
512: String getNamespace(String actionPath) {
513: int idx = actionPath.lastIndexOf('/');
514: String namespace = "";
515: if (idx >= 0) {
516: namespace = actionPath.substring(0, idx);
517: }
518: return namespace;
519: }
520:
521: /**
522: * Get the action name part of the action path.
523: * @param actionPath Full path to action
524: * @return The action name.
525: */
526: String getActionName(String actionPath) {
527: int idx = actionPath.lastIndexOf('/');
528: String action = actionPath;
529: if (idx >= 0) {
530: action = actionPath.substring(idx + 1);
531: }
532: return action;
533: }
534:
535: /**
536: * Returns a Map of all request parameters. This implementation just calls
537: * {@link PortletRequest#getParameterMap()}.
538: *
539: * @param request the PortletRequest object.
540: * @return a Map of all request parameters.
541: * @throws IOException if an exception occurs while retrieving the parameter
542: * map.
543: */
544: protected Map getParameterMap(PortletRequest request)
545: throws IOException {
546: return new HashMap(request.getParameterMap());
547: }
548:
549: /**
550: * Returns a Map of all request attributes. The default implementation is to
551: * wrap the request in a {@link RequestMap}. Override this method to
552: * customize how request attributes are mapped.
553: *
554: * @param request the PortletRequest object.
555: * @return a Map of all request attributes.
556: */
557: protected Map getRequestMap(PortletRequest request) {
558: return new PortletRequestMap(request);
559: }
560:
561: /**
562: * Returns a Map of all session attributes. The default implementation is to
563: * wrap the reqeust in a {@link SessionMap}. Override this method to
564: * customize how session attributes are mapped.
565: *
566: * @param request the PortletRequest object.
567: * @return a Map of all session attributes.
568: */
569: protected Map getSessionMap(PortletRequest request) {
570: return new PortletSessionMap(request);
571: }
572:
573: /**
574: * Convenience method to ease testing.
575: * @param factory
576: */
577: protected void setActionProxyFactory(ActionProxyFactory factory) {
578: this .factory = factory;
579: }
580:
581: /**
582: * Check to see if the action parameter is valid for the current portlet mode. If the portlet
583: * mode has been changed with the portal widgets, the action name is invalid, since the
584: * action name belongs to the previous executing portlet mode. If this method evaluates to
585: * <code>true</code> the <code>default<Mode>Action</code> is used instead.
586: * @param request The portlet request.
587: * @return <code>true</code> if the action should be reset.
588: */
589: private boolean resetAction(PortletRequest request) {
590: boolean reset = false;
591: Map paramMap = request.getParameterMap();
592: String[] modeParam = (String[]) paramMap.get(MODE_PARAM);
593: if (modeParam != null && modeParam.length == 1) {
594: String originatingMode = modeParam[0];
595: String currentMode = request.getPortletMode().toString();
596: if (!currentMode.equals(originatingMode)) {
597: reset = true;
598: }
599: }
600: if (reset) {
601: request.setAttribute(ACTION_RESET, Boolean.TRUE);
602: } else {
603: request.setAttribute(ACTION_RESET, Boolean.FALSE);
604: }
605: return reset;
606: }
607:
608: public void destroy() {
609: if (dispatcherUtils == null) {
610: LOG
611: .warn("something is seriously wrong, DispatcherUtil is not initialized (null) ");
612: } else {
613: dispatcherUtils.cleanup();
614: }
615: }
616:
617: /**
618: * @param actionMapper the actionMapper to set
619: */
620: public void setActionMapper(ActionMapper actionMapper) {
621: this.actionMapper = actionMapper;
622: }
623:
624: }
|