001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.web.portlet;
018:
019: import java.io.IOException;
020: import java.security.Principal;
021: import java.util.Map;
022:
023: import javax.portlet.ActionRequest;
024: import javax.portlet.ActionResponse;
025: import javax.portlet.PortletException;
026: import javax.portlet.PortletRequest;
027: import javax.portlet.PortletResponse;
028: import javax.portlet.RenderRequest;
029: import javax.portlet.RenderResponse;
030:
031: import org.springframework.beans.BeanUtils;
032: import org.springframework.beans.BeansException;
033: import org.springframework.context.ApplicationContext;
034: import org.springframework.context.ApplicationContextException;
035: import org.springframework.context.ApplicationEvent;
036: import org.springframework.context.ApplicationListener;
037: import org.springframework.context.ConfigurableApplicationContext;
038: import org.springframework.context.event.ContextRefreshedEvent;
039: import org.springframework.context.event.SourceFilteringListener;
040: import org.springframework.util.StringUtils;
041: import org.springframework.web.portlet.context.ConfigurablePortletApplicationContext;
042: import org.springframework.web.portlet.context.PortletApplicationContextUtils;
043: import org.springframework.web.portlet.context.PortletRequestHandledEvent;
044: import org.springframework.web.portlet.context.XmlPortletApplicationContext;
045:
046: /**
047: * Base portlet for Spring's portlet framework. Provides integration with
048: * a Spring application context, in a JavaBean-based overall solution.
049: *
050: * <p>This class offers the following functionality:
051: * <ul>
052: * <li>Manages a Portlet {@link org.springframework.context.ApplicationContext}
053: * instance per portlet. The portlet's configuration is determined by beans
054: * in the portlet's namespace.
055: * <li>Publishes events on request processing, whether or not a request is
056: * successfully handled.
057: * </ul>
058: *
059: * <p>Subclasses must implement {@link #doActionService} and {@link #doRenderService}
060: * to handle action and render requests. Because this extends {@link GenericPortletBean}
061: * rather than Portlet directly, bean properties are mapped onto it. Subclasses can
062: * override {@link #initFrameworkPortlet()} for custom initialization.
063: *
064: * <p>Regards a "contextClass" parameter at the portlet init-param level,
065: * falling back to the default context class
066: * ({@link org.springframework.web.portlet.context.XmlPortletApplicationContext})
067: * if not found. Note that, with the default FrameworkPortlet,
068: * a context class needs to implement the
069: * {@link org.springframework.web.portlet.context.ConfigurablePortletApplicationContext} SPI.
070: *
071: * <p>Passes a "contextConfigLocation" portlet init-param to the context instance,
072: * parsing it into potentially multiple file paths which can be separated by any
073: * number of commas and spaces, like "test-portlet.xml, myPortlet.xml".
074: * If not explicitly specified, the context implementation is supposed to build a
075: * default location from the namespace of the portlet.
076: *
077: * <p>Note: In case of multiple config locations, later bean definitions will
078: * override ones defined in earlier loaded files, at least when using one of
079: * Spring's default ApplicationContext implementations. This can be leveraged
080: * to deliberately override certain bean definitions via an extra XML file.
081: *
082: * <p>The default namespace is "'portlet-name'-portlet", e.g. "test-portlet" for a
083: * portlet-name "test" (leading to a "/WEB-INF/test-portlet.xml" default location
084: * with XmlPortletApplicationContext). The namespace can also be set explicitly via
085: * the "namespace" portlet init-param.
086: *
087: * @author William G. Thompson, Jr.
088: * @author John A. Lewis
089: * @author Juergen Hoeller
090: * @since 2.0
091: * @see #doActionService
092: * @see #doRenderService
093: * @see #setContextClass
094: * @see #setContextConfigLocation
095: * @see #setNamespace
096: */
097: public abstract class FrameworkPortlet extends GenericPortletBean
098: implements ApplicationListener {
099:
100: /**
101: * Default context class for FrameworkPortlet.
102: * @see org.springframework.web.portlet.context.XmlPortletApplicationContext
103: */
104: public static final Class DEFAULT_CONTEXT_CLASS = XmlPortletApplicationContext.class;
105:
106: /**
107: * Suffix for Portlet ApplicationContext namespaces. If a portlet of this class is
108: * given the name "test" in a context, the namespace used by the portlet will
109: * resolve to "test-portlet".
110: */
111: public static final String DEFAULT_NAMESPACE_SUFFIX = "-portlet";
112:
113: /**
114: * Prefix for the PortletContext attribute for the Portlet ApplicationContext.
115: * The completion is the portlet name.
116: */
117: public static final String PORTLET_CONTEXT_PREFIX = FrameworkPortlet.class
118: .getName()
119: + ".CONTEXT.";
120:
121: /**
122: * Default USER_INFO attribute names to search for the current username:
123: * "user.login.id", "user.name".
124: */
125: public static final String[] DEFAULT_USERINFO_ATTRIBUTE_NAMES = {
126: "user.login.id", "user.name" };
127:
128: /** Portlet ApplicationContext implementation class to use */
129: private Class contextClass = DEFAULT_CONTEXT_CLASS;
130:
131: /** Namespace for this portlet */
132: private String namespace;
133:
134: /** Explicit context config location */
135: private String contextConfigLocation;
136:
137: /** Should we publish the context as a PortletContext attribute? */
138: private boolean publishContext = true;
139:
140: /** Should we publish a PortletRequestHandledEvent at the end of each request? */
141: private boolean publishEvents = true;
142:
143: /** USER_INFO attributes that may contain the username of the current user */
144: private String[] userinfoUsernameAttributes = DEFAULT_USERINFO_ATTRIBUTE_NAMES;
145:
146: /** ApplicationContext for this portlet */
147: private ApplicationContext portletApplicationContext;
148:
149: /** Flag used to detect whether onRefresh has already been called */
150: private boolean refreshEventReceived = false;
151:
152: /**
153: * Set a custom context class. This class must be of type ApplicationContext;
154: * when using the default FrameworkPortlet implementation, the context class
155: * must also implement ConfigurablePortletApplicationContext.
156: * @see #createPortletApplicationContext
157: */
158: public void setContextClass(Class contextClass) {
159: this .contextClass = contextClass;
160: }
161:
162: /**
163: * Return the custom context class.
164: */
165: public Class getContextClass() {
166: return this .contextClass;
167: }
168:
169: /**
170: * Set a custom namespace for this portlet,
171: * to be used for building a default context config location.
172: */
173: public void setNamespace(String namespace) {
174: this .namespace = namespace;
175: }
176:
177: /**
178: * Return the namespace for this portlet, falling back to default scheme if
179: * no custom namespace was set. (e.g. "test-portlet" for a portlet named "test")
180: */
181: public String getNamespace() {
182: return (this .namespace != null) ? this .namespace
183: : getPortletName() + DEFAULT_NAMESPACE_SUFFIX;
184: }
185:
186: /**
187: * Set the context config location explicitly, instead of relying on the default
188: * location built from the namespace. This location string can consist of
189: * multiple locations separated by any number of commas and spaces.
190: */
191: public void setContextConfigLocation(String contextConfigLocation) {
192: this .contextConfigLocation = contextConfigLocation;
193: }
194:
195: /**
196: * Return the explicit context config location, if any.
197: */
198: public String getContextConfigLocation() {
199: return this .contextConfigLocation;
200: }
201:
202: /**
203: * Set whether to publish this portlet's context as a PortletContext attribute,
204: * available to all objects in the web container. Default is true.
205: * <p>This is especially handy during testing, although it is debatable whether
206: * it's good practice to let other application objects access the context this way.
207: */
208: public void setPublishContext(boolean publishContext) {
209: this .publishContext = publishContext;
210: }
211:
212: /**
213: * Return whether to publish this portlet's context as a PortletContext attribute.
214: */
215: public boolean isPublishContext() {
216: return this .publishContext;
217: }
218:
219: /**
220: * Set whether this portlet should publish a PortletRequestHandledEvent at the end
221: * of each request. Default is true; can be turned off for a slight performance
222: * improvement, provided that no ApplicationListeners rely on such events.
223: * @see org.springframework.web.portlet.context.PortletRequestHandledEvent
224: */
225: public void setPublishEvents(boolean publishEvents) {
226: this .publishEvents = publishEvents;
227: }
228:
229: /**
230: * Return whether this portlet should publish a PortletRequestHandledEvent at the end
231: * of each request.
232: */
233: public boolean isPublishEvents() {
234: return this .publishEvents;
235: }
236:
237: /**
238: * Set the list of attributes to search in the USER_INFO map when trying
239: * to find the username of the current user.
240: * @see #getUsernameForRequest
241: */
242: public void setUserinfoUsernameAttributes(
243: String[] userinfoUsernameAttributes) {
244: this .userinfoUsernameAttributes = userinfoUsernameAttributes;
245: }
246:
247: /**
248: * Returns the list of attributes that will be searched in the USER_INFO map
249: * when trying to find the username of the current user
250: * @see #getUsernameForRequest
251: */
252: public String[] getUserinfoUsernameAttributes() {
253: return this .userinfoUsernameAttributes;
254: }
255:
256: /**
257: * Overridden method of GenericPortletBean, invoked after any bean properties
258: * have been set. Creates this portlet's ApplicationContext.
259: */
260: protected final void initPortletBean() throws PortletException,
261: BeansException {
262: getPortletContext().log(
263: "Initializing Spring FrameworkPortlet '"
264: + getPortletName() + "'");
265: if (logger.isInfoEnabled()) {
266: logger.info("FrameworkPortlet '" + getPortletName()
267: + "': initialization started");
268: }
269: long startTime = System.currentTimeMillis();
270:
271: try {
272: this .portletApplicationContext = initPortletApplicationContext();
273: initFrameworkPortlet();
274: } catch (PortletException ex) {
275: logger.error("Context initialization failed", ex);
276: throw ex;
277: } catch (BeansException ex) {
278: logger.error("Context initialization failed", ex);
279: throw ex;
280: }
281:
282: if (logger.isInfoEnabled()) {
283: long elapsedTime = System.currentTimeMillis() - startTime;
284: logger.info("FrameworkPortlet '" + getPortletName()
285: + "': initialization completed in " + elapsedTime
286: + " ms");
287: }
288: }
289:
290: /**
291: * Initialize and publish the Portlet ApplicationContext for this portlet.
292: * <p>Delegates to {@link #createPortletApplicationContext} for actual creation.
293: * Can be overridden in subclasses.
294: * @return the ApplicationContext for this portlet
295: * @throws BeansException if the context couldn't be initialized
296: */
297: protected ApplicationContext initPortletApplicationContext()
298: throws BeansException {
299: ApplicationContext parent = PortletApplicationContextUtils
300: .getWebApplicationContext(getPortletContext());
301: ApplicationContext pac = createPortletApplicationContext(parent);
302:
303: if (!this .refreshEventReceived) {
304: // Apparently not a ConfigurableApplicationContext with refresh support:
305: // triggering initial onRefresh manually here.
306: onRefresh(pac);
307: }
308:
309: if (isPublishContext()) {
310: // publish the context as a portlet context attribute
311: String attName = getPortletContextAttributeName();
312: getPortletContext().setAttribute(attName, pac);
313: if (logger.isDebugEnabled()) {
314: logger
315: .debug("Published ApplicationContext of portlet '"
316: + getPortletName()
317: + "' as PortletContext attribute with name ["
318: + attName + "]");
319: }
320: }
321: return pac;
322: }
323:
324: /**
325: * Instantiate the Portlet ApplicationContext for this portlet, either a default
326: * XmlPortletApplicationContext or a custom context class if set.
327: * <p>This implementation expects custom contexts to implement
328: * ConfigurablePortletApplicationContext. Can be overridden in subclasses.
329: * @param parent the parent ApplicationContext to use, or null if none
330: * @return the Portlet ApplicationContext for this portlet
331: * @throws BeansException if the context couldn't be initialized
332: * @see #setContextClass
333: * @see org.springframework.web.portlet.context.XmlPortletApplicationContext
334: */
335: protected ApplicationContext createPortletApplicationContext(
336: ApplicationContext parent) throws BeansException {
337:
338: if (logger.isDebugEnabled()) {
339: logger
340: .debug("Portlet with name '"
341: + getPortletName()
342: + "' will try to create custom ApplicationContext context of class '"
343: + getContextClass().getName() + "'"
344: + ", using parent context [" + parent + "]");
345: }
346: if (!ConfigurablePortletApplicationContext.class
347: .isAssignableFrom(getContextClass())) {
348: throw new ApplicationContextException(
349: "Fatal initialization error in portlet with name '"
350: + getPortletName()
351: + "': custom ApplicationContext class ["
352: + getContextClass().getName()
353: + "] is not of type ConfigurablePortletApplicationContext");
354: }
355:
356: ConfigurablePortletApplicationContext pac = (ConfigurablePortletApplicationContext) BeanUtils
357: .instantiateClass(getContextClass());
358: pac.setParent(parent);
359: pac.setPortletContext(getPortletContext());
360: pac.setPortletConfig(getPortletConfig());
361: pac.setNamespace(getNamespace());
362: if (getContextConfigLocation() != null) {
363: pac
364: .setConfigLocations(StringUtils
365: .tokenizeToStringArray(
366: getContextConfigLocation(),
367: ConfigurablePortletApplicationContext.CONFIG_LOCATION_DELIMITERS));
368: }
369: pac.addApplicationListener(new SourceFilteringListener(pac,
370: this ));
371:
372: postProcessPortletApplicationContext(pac);
373: pac.refresh();
374:
375: return pac;
376: }
377:
378: /**
379: * Post-process the given Portlet ApplicationContext before it is refreshed
380: * and activated as context for this portlet.
381: * <p>The default implementation is empty. <code>refresh()</code> will
382: * be called automatically after this method returns.
383: * @param pac the configured Portlet ApplicationContext (not refreshed yet)
384: * @see #createPortletApplicationContext
385: * @see ConfigurableApplicationContext#refresh()
386: */
387: protected void postProcessPortletApplicationContext(
388: ConfigurableApplicationContext pac) {
389: }
390:
391: /**
392: * Return the PortletContext attribute name for this portlets's ApplicationContext.
393: * <p>The default implementation returns PORTLET_CONTEXT_PREFIX + portlet name.
394: * @see #PORTLET_CONTEXT_PREFIX
395: * @see #getPortletName
396: */
397: public String getPortletContextAttributeName() {
398: return PORTLET_CONTEXT_PREFIX + getPortletName();
399: }
400:
401: /**
402: * Return this portlet's ApplicationContext.
403: */
404: public final ApplicationContext getPortletApplicationContext() {
405: return this .portletApplicationContext;
406: }
407:
408: /**
409: * This method will be invoked after any bean properties have been set and
410: * the ApplicationContext has been loaded.
411: * <p>The default implementation is empty; subclasses may override this method
412: * to perform any initialization they require.
413: * @throws PortletException in case of an initialization exception
414: * @throws BeansException if thrown by ApplicationContext methods
415: */
416: protected void initFrameworkPortlet() throws PortletException,
417: BeansException {
418: }
419:
420: /**
421: * Refresh this portlet's application context, as well as the
422: * dependent state of the portlet.
423: * @throws BeansException in case of errors
424: * @see #getPortletApplicationContext()
425: * @see org.springframework.context.ConfigurableApplicationContext#refresh()
426: */
427: public void refresh() throws BeansException {
428: ApplicationContext pac = getPortletApplicationContext();
429: if (!(pac instanceof ConfigurableApplicationContext)) {
430: throw new IllegalStateException(
431: "Portlet ApplicationContext does not support refresh: "
432: + pac);
433: }
434: ((ConfigurableApplicationContext) pac).refresh();
435: }
436:
437: /**
438: * ApplicationListener endpoint that receives events from this servlet's
439: * WebApplicationContext.
440: * <p>The default implementation calls {@link #onRefresh} in case of a
441: * {@link org.springframework.context.event.ContextRefreshedEvent},
442: * triggering a refresh of this servlet's context-dependent state.
443: * @param event the incoming ApplicationContext event
444: */
445: public void onApplicationEvent(ApplicationEvent event) {
446: if (event instanceof ContextRefreshedEvent) {
447: this .refreshEventReceived = true;
448: onRefresh(((ContextRefreshedEvent) event)
449: .getApplicationContext());
450: }
451: }
452:
453: /**
454: * Template method which can be overridden to add portlet-specific refresh work.
455: * Called after successful context refresh.
456: * <p>This implementation is empty.
457: * @param context the current Portlet ApplicationContext
458: * @throws BeansException in case of errors
459: * @see #refresh()
460: */
461: protected void onRefresh(ApplicationContext context)
462: throws BeansException {
463: // For subclasses: do nothing by default.
464: }
465:
466: /**
467: * Delegate render requests to processRequest/doRenderService.
468: */
469: protected final void doDispatch(RenderRequest request,
470: RenderResponse response) throws PortletException,
471: IOException {
472:
473: processRequest(request, response);
474: }
475:
476: /**
477: * Delegate action requests to processRequest/doActionService.
478: */
479: public final void processAction(ActionRequest request,
480: ActionResponse response) throws PortletException,
481: IOException {
482:
483: processRequest(request, response);
484: }
485:
486: /**
487: * Process this request, publishing an event regardless of the outcome.
488: * The actual event handling is performed by the abstract
489: * <code>doActionService()</code> and <code>doRenderService()</code> template methods.
490: * @see #doActionService
491: * @see #doRenderService
492: */
493: protected final void processRequest(PortletRequest request,
494: PortletResponse response) throws PortletException,
495: IOException {
496:
497: long startTime = System.currentTimeMillis();
498: Throwable failureCause = null;
499:
500: try {
501: if (request instanceof ActionRequest) {
502: doActionService((ActionRequest) request,
503: (ActionResponse) response);
504: } else {
505: doRenderService((RenderRequest) request,
506: (RenderResponse) response);
507: }
508: } catch (PortletException ex) {
509: failureCause = ex;
510: throw ex;
511: } catch (IOException ex) {
512: failureCause = ex;
513: throw ex;
514: } catch (Throwable ex) {
515: failureCause = ex;
516: throw new PortletException("Request processing failed", ex);
517: }
518:
519: finally {
520: if (failureCause != null) {
521: logger
522: .error("Could not complete request",
523: failureCause);
524: } else {
525: logger.debug("Successfully completed request");
526: }
527: if (isPublishEvents()) {
528: // Whether or not we succeeded, publish an event.
529: long processingTime = System.currentTimeMillis()
530: - startTime;
531: this .portletApplicationContext
532: .publishEvent(new PortletRequestHandledEvent(
533: this ,
534: getPortletConfig().getPortletName(),
535: request.getPortletMode().toString(),
536: (request instanceof ActionRequest ? "action"
537: : "render"), request
538: .getRequestedSessionId(),
539: getUsernameForRequest(request),
540: processingTime, failureCause));
541: }
542: }
543: }
544:
545: /**
546: * Determine the username for the given request.
547: * <p>The default implementation first tries the UserPrincipal.
548: * If that does not exist, then it checks the USER_INFO map.
549: * Can be overridden in subclasses.
550: * @param request current portlet request
551: * @return the username, or <code>null</code> if none found
552: * @see javax.portlet.PortletRequest#getUserPrincipal()
553: * @see javax.portlet.PortletRequest#getRemoteUser()
554: * @see javax.portlet.PortletRequest#USER_INFO
555: * @see #setUserinfoUsernameAttributes
556: */
557: protected String getUsernameForRequest(PortletRequest request) {
558: // Try the principal.
559: Principal userPrincipal = request.getUserPrincipal();
560: if (userPrincipal != null) {
561: return userPrincipal.getName();
562: }
563:
564: // Try the remote user name.
565: String userName = request.getRemoteUser();
566: if (userName != null) {
567: return userName;
568: }
569:
570: // Try the Portlet USER_INFO map.
571: Map userInfo = (Map) request
572: .getAttribute(PortletRequest.USER_INFO);
573: if (userInfo != null) {
574: for (int i = 0, n = this .userinfoUsernameAttributes.length; i < n; i++) {
575: userName = (String) userInfo
576: .get(this .userinfoUsernameAttributes[i]);
577: if (userName != null) {
578: return userName;
579: }
580: }
581: }
582:
583: // Nothing worked...
584: return null;
585: }
586:
587: /**
588: * Subclasses must implement this method to do the work of render request handling.
589: * <p>The contract is essentially the same as that for the <code>doDispatch</code>
590: * method of GenericPortlet.
591: * <p>This class intercepts calls to ensure that exception handling and
592: * event publication takes place.
593: * @param request current render request
594: * @param response current render response
595: * @throws Exception in case of any kind of processing failure
596: * @see javax.portlet.GenericPortlet#doDispatch
597: */
598: protected abstract void doRenderService(RenderRequest request,
599: RenderResponse response) throws Exception;
600:
601: /**
602: * Subclasses must implement this method to do the work of action request handling.
603: * <p>The contract is essentially the same as that for the <code>processAction</code>
604: * method of GenericPortlet.
605: * <p>This class intercepts calls to ensure that exception handling and
606: * event publication takes place.
607: * @param request current action request
608: * @param response current action response
609: * @throws Exception in case of any kind of processing failure
610: * @see javax.portlet.GenericPortlet#processAction
611: */
612: protected abstract void doActionService(ActionRequest request,
613: ActionResponse response) throws Exception;
614:
615: /**
616: * Close the ApplicationContext of this portlet.
617: * @see org.springframework.context.ConfigurableApplicationContext#close()
618: */
619: public void destroy() {
620: getPortletContext().log(
621: "Destroying Spring FrameworkPortlet '"
622: + getPortletName() + "'");
623: if (this .portletApplicationContext instanceof ConfigurableApplicationContext) {
624: ((ConfigurableApplicationContext) this.portletApplicationContext)
625: .close();
626: }
627: }
628:
629: }
|