001: /*
002: * Created on Aug 25, 2004
003: *
004: * To change the template for this generated file go to
005: * Window>Preferences>Java>Code Generation>Code and Comments
006: */
007: package org.jasig.portal.channels.jsp;
008:
009: import java.io.File;
010: import java.io.PrintWriter;
011: import java.util.Enumeration;
012: import java.util.HashMap;
013: import java.util.HashSet;
014: import java.util.Iterator;
015: import java.util.Map;
016: import java.util.Properties;
017: import java.util.Set;
018: import java.util.Map.Entry;
019:
020: import javax.servlet.RequestDispatcher;
021: import javax.servlet.ServletException;
022: import javax.servlet.http.HttpServletRequest;
023: import javax.servlet.http.HttpServletResponse;
024: import javax.servlet.http.HttpSession;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028: import org.jasig.portal.ChannelCacheKey;
029: import org.jasig.portal.ChannelRuntimeData;
030: import org.jasig.portal.ChannelStaticData;
031: import org.jasig.portal.ICacheable;
032: import org.jasig.portal.ICharacterChannel;
033: import org.jasig.portal.IDirectResponse;
034: import org.jasig.portal.IPrivileged;
035: import org.jasig.portal.IPrivilegedChannel;
036: import org.jasig.portal.PortalControlStructures;
037: import org.jasig.portal.PortalEvent;
038: import org.jasig.portal.PortalException;
039: import org.jasig.portal.car.CarResources;
040: import org.jasig.portal.channels.BaseChannel;
041: import org.jasig.portal.i18n.LocaleManager;
042: import org.jasig.portal.properties.PropertiesManager;
043:
044: /**
045: * @author Mark Boyd
046: *
047: **/
048: public class Channel extends BaseChannel implements ICacheable,
049: ICharacterChannel, IPrivilegedChannel, IDirectResponse {
050: private static final Log LOG = LogFactory.getLog(Channel.class);
051:
052: /**
053: * The property to look for to determine if we should serialize rendering
054: * of jsps for a single http request.
055: */
056: private static final String SINGLE_THREAD_CFG_PROPERTY = Channel.class
057: .getName()
058: + ".serializeJspRendering";
059:
060: /**
061: * Indicates whether serialization of rendering is configured.
062: */
063: private static final boolean cSerializeJspRendering = loadRenderingCfg();
064:
065: private static final String PREFS_PREFIX = "JSP.";
066: protected String mControllerClassname = null;
067: private Map mObjects;
068: private Properties mJspMap = new Properties();
069: private HttpServletResponse mResponse;
070: private HttpServletRequest mRequest;
071: private static Set mLoaded = new HashSet();
072: private static Map mDeploymentApproach = new HashMap();
073: protected IController mController;
074: private HttpSession mSession = null;
075:
076: private static String CONTROLLER_KEY = "controllerClass";
077: public static final Object CAR_DEPLOYMENT = new Object();
078: public static final Object TRADITIONAL_DEPLOYMENT = new Object();
079: private String cJspContextPath = null;
080:
081: /**
082: * Default constructor used by Channel Manager to instiate this channel.
083: * This approach delegates to ChannelStaticData to find a property key of
084: * "controllerClass" with a value of a class name the implements the
085: * IContoller interface.
086: *
087: */
088: public Channel() {
089: }
090:
091: /**
092: * Reads configuration to determine if we are serializing rendering of
093: * jsps in a single http request for a user.
094: * @return
095: */
096: private static boolean loadRenderingCfg() {
097: boolean serialize = PropertiesManager.getPropertyAsBoolean(
098: SINGLE_THREAD_CFG_PROPERTY, false);
099: if (LOG.isDebugEnabled()) {
100: LOG
101: .debug("Serializing JSP Request Rendering: "
102: + serialize);
103: }
104: return serialize;
105: }
106:
107: /**
108: * Return a cache key for indicating if cached output is stale. This
109: * call is delegated to the contained IController which extends ICacheable.
110: *
111: * @see org.jasig.portal.ICacheable#generateKey()
112: */
113: public ChannelCacheKey generateKey() {
114: if (mController != null) {
115: return mController.generateKey();
116: }
117: return null;
118: }
119:
120: /**
121: * Indicate whether cached output is still valid for the passed in
122: * validity object. The call is delegated to the contained IController
123: * which extends ICacheable.
124: *
125: * @see org.jasig.portal.ICacheable#isCacheValid(java.lang.Object)
126: */
127: public boolean isCacheValid(Object validity) {
128: if (mController != null) {
129: return mController.isCacheValid(validity);
130: }
131: return false;
132: }
133:
134: /* (non-Javadoc)
135: * @see org.jasig.portal.ICharacterChannel#renderCharacters(java.io.PrintWriter)
136: */
137: public void renderCharacters(PrintWriter pw) throws PortalException {
138: if (mController == null) {
139: return;
140: }
141:
142: // 1) determine what JSP to forward to
143: String jspId = mController.getJspToRender();
144: if (jspId == null) {
145: throw new PortalException(
146: "No JSP id returned by controller.");
147: }
148:
149: String jsp = mJspMap.getProperty(jspId);
150: boolean relativeToController = false;
151:
152: if (jsp == null) {
153: throw new PortalException(
154: "No mapping available for JSP id '" + jspId + "'.");
155: }
156:
157: if (!jsp.startsWith("/")) {
158: relativeToController = true;
159: jsp = mController.getClass().getPackage().getName()
160: .replace('.', '/')
161: + "/" + jsp;
162: }
163:
164: String prefix = "";
165: // jsps deployed from a car are deployed in configurable location.
166: if (relativeToController
167: || mDeploymentApproach.get(mControllerClassname) == CAR_DEPLOYMENT) {
168: prefix = getJspContextPath();
169: }
170:
171: // 2) create req/res wrappers and push in model objects returned
172: // from controller.
173:
174: HttpRequestFacade reqF = new HttpRequestFacade(mRequest);
175: HttpResponseFacade respF = new HttpResponseFacade(mResponse);
176:
177: if (mObjects != null) {
178: for (Iterator itr = mObjects.entrySet().iterator(); itr
179: .hasNext();) {
180: Map.Entry entry = (Entry) itr.next();
181: reqF.setAttribute((String) entry.getKey(), entry
182: .getValue());
183: }
184: }
185:
186: // 3) Add baseActionUrl and baseMediaUrl to the request object.
187: // Since classes and JSPs get deployed in some context relative
188: // classpath accessible area as determined by the Deployer's
189: // getRealPath() the classes will always be loaded by the non-car class
190: // loader. Therefore, we can't use traditional ways of determining
191: // if the channel was deployed via CAR and the images are still
192: // embedded therein. So we use this map which is updated during
193: // deployment checks to tell in which way the channel is deployed.
194:
195: if (mControllerClassname != null) {
196: reqF.setAttribute("baseMediaUrl", runtimeData
197: .getBaseMediaURL(mControllerClassname));
198: reqF.setAttribute("baseActionUrl", runtimeData
199: .getBaseActionURL(true));
200: reqF
201: .setAttribute("userLocale", runtimeData
202: .getLocales()[0]);
203: }
204:
205: // 4 render the JSP for the channel
206: String jspPath = prefix + jsp;
207: /*
208: * When we converted to tomcat we started seeing jsp channel content
209: * being swapped and some jsp's not getting their expected models
210: * resulting in exceptions. The problem was traced to dispatching
211: * somehow getting crossed. Therefore, we synchronize on the request
212: * to serialized all jsp rendering for a single user request if
213: * configured to do so.
214: */
215: if (cSerializeJspRendering) {
216: synchronized (mRequest) {
217: renderJsp(jspPath, reqF, respF);
218: }
219: } else {
220: renderJsp(jspPath, reqF, respF);
221: }
222:
223: // 5) if rendering successful, extract characters and use for our UI.
224: if (respF.isSuccessful()) {
225: pw.print(respF.getCharacters());
226: } else {
227: throw new PortalException(
228: "A problem occurred rendering JSP '"
229: + jspPath
230: + "'. Response Error Code: "
231: + respF.getErrorCode()
232: + (respF.getErrorMessage() != null ? ", Response Error Message '"
233: + respF.getErrorMessage() + "'"
234: : ""));
235: }
236: }
237:
238: /**
239: * Obtains dispatcher to JSP and forwards request to it to be rendered.
240: * Upon returning, respF should contain the output of the JSP.
241: *
242: * @param jspPath
243: * @param reqF
244: * @param respF
245: */
246: private void renderJsp(String jspPath, HttpRequestFacade reqF,
247: HttpResponseFacade respF) {
248: // 4.a) get the request dispatcher for the JSP
249: RequestDispatcher dispatch = mRequest
250: .getRequestDispatcher(jspPath);
251:
252: if (LOG.isDebugEnabled()) {
253: if (dispatch == null) {
254: LOG.debug("\n\n Jsp Channel Type with:\n"
255: + "- controller: '"
256: + mController.getClass().getName() + "'\n"
257: + "- called: getRequestDispatcher()'\n"
258: + "- on:"
259: + mRequest.getClass().getName() + ".'\n"
260: + "- hashcode: " + mRequest.hashCode() + "\n"
261: + "- passing: " + jspPath + "'\n"
262: + "- received: NULL");
263: LOG.debug("\n" + "- FROM:\n", new Throwable(
264: "STACK"));
265: } else {
266: LOG.debug("\n\n Jsp Channel Type with:\n"
267: + "- controller: '"
268: + mController.getClass().getName() + "'\n"
269: + "- called: getRequestDispatcher()'\n"
270: + "- on:"
271: + mRequest.getClass().getName() + ".'\n"
272: + "- hashcode: " + mRequest.hashCode() + "\n"
273: + "- passing: " + jspPath + "'\n"
274: + "- received: "
275: + dispatch.getClass().getName() + ".");
276: LOG.debug("\n" + "- FROM:\n", new Throwable(
277: "STACK"));
278: }
279: }
280: if (dispatch == null) {
281: throw new PortalException("Unable to delegate to JSP '"
282: + jspPath + "'. " + mRequest.getClass().getName()
283: + ".getRequestDispatch('" + jspPath
284: + "') returned NULL.");
285: }
286:
287: // 4.b) now render the JSP view
288: try {
289: dispatch.forward(reqF, respF);
290: } catch (IllegalStateException e) {
291: throw new PortalException(
292: "A problem occurred rendering JSP '" + jspPath
293: + "'", e);
294: } catch (ServletException e) {
295: throw new PortalException(
296: "A problem occurred rendering JSP '" + jspPath
297: + "'", e);
298: } catch (Exception e) {
299: throw new PortalException(
300: "A problem occurred rendering JSP '" + jspPath
301: + "'", e);
302: }
303: }
304:
305: private String getJspContextPath() {
306: if (cJspContextPath == null) {
307: String ctxRelativePath = PropertiesManager.getProperty(
308: Deployer.JSP_DEPLOY_DIR_PROP, "/WEB-INF/classes");
309: if (!ctxRelativePath.endsWith("/")
310: && !ctxRelativePath.endsWith("\\")) {
311: ctxRelativePath = ctxRelativePath + File.separatorChar;
312: }
313: cJspContextPath = ctxRelativePath;
314: }
315: return cJspContextPath;
316: }
317:
318: /* (non-Javadoc)
319: * @see org.jasig.portal.IChannel#receiveEvent(org.jasig.portal.PortalEvent)
320: */
321: public void receiveEvent(PortalEvent ev) {
322: if (mController != null) {
323: mController.receiveEvent(ev);
324: }
325: }
326:
327: /* (non-Javadoc)
328: * @see org.jasig.portal.IChannel#setRuntimeData(org.jasig.portal.ChannelRuntimeData)
329: */
330: public void setRuntimeData(ChannelRuntimeData rd)
331: throws PortalException {
332:
333: if (log.isTraceEnabled()) {
334: log.trace("JSP Channel received setRuntimeData [" + rd
335: + "]");
336: }
337: if (mController != null) {
338: super .setRuntimeData(new MediaResolver(rd));
339: mObjects = mController.processRuntimeData(runtimeData,
340: mSession);
341: } else
342: super .setRuntimeData(rd);
343:
344: // Now indicate to the JSPs via the HttpRequest which locale is in use.
345: java.util.Locale locale = runtimeData.getLocales()[0];
346:
347: // To avoid the compile-time dependency, use the published
348: // strings instead of linking directly to the JSTL Config
349: // class.
350: mRequest.setAttribute("javax.servlet.jsp.jstl.fmt.locale",
351: locale);
352:
353: // To avoid the compile-time dependency, use the published
354: // strings instead of linking directly to the JSTL Config
355: // class.
356: mRequest.setAttribute("javax.servlet.jsp.jstl.fmt.locale"
357: + ".request", locale);
358:
359: }
360:
361: /* (non-Javadoc)
362: * @see org.jasig.portal.IChannel#setStaticData(org.jasig.portal.ChannelStaticData)
363: */
364: public void setStaticData(ChannelStaticData sd)
365: throws PortalException {
366: super .setStaticData(sd);
367: getController();
368:
369: if (mController != null) {
370: // strip channel-type specific prefix from those parameters that
371: // are specific to the JSPs.
372: // Take all parameters whose names start with "JSP." and remove them
373: // from the channel static data, then reinsert them w/out the prefix.
374: Enumeration allKeys = sd.keys();
375: while (allKeys.hasMoreElements()) {
376: String p = (String) allKeys.nextElement();
377: if (p.startsWith(PREFS_PREFIX)) {
378: String name = p.substring(PREFS_PREFIX.length());
379: String value = sd.getParameter(p);
380: sd.setParameter(name, value);
381: // remove old parameter
382: sd.remove(p);
383: }
384: }
385:
386: mController.setStaticData(sd);
387: loadJspMap();
388: }
389: }
390:
391: /**
392: *
393: */
394: private void loadJspMap() {
395: Map jsps = mController.getJspMap();
396: if (null == jsps) {
397: return;
398: }
399:
400: for (Iterator itr = jsps.entrySet().iterator(); itr.hasNext();) {
401: Map.Entry entry = (Entry) itr.next();
402: String key = (String) entry.getKey();
403: mJspMap.put(key, entry.getValue());
404: }
405: }
406:
407: /**
408: *
409: */
410: private void getController() throws PortalException {
411: if (mControllerClassname == null) {
412: mControllerClassname = this .staticData
413: .getParameter(CONTROLLER_KEY);
414: }
415:
416: if (mControllerClassname == null) {
417: throw new PortalException("No implementation of "
418: + "org.jasig.portal.channels.jsp.IController "
419: + "specified on ChannelStaticData.");
420: }
421:
422: syncDeploymentOfResources(mControllerClassname);
423:
424: if (mControllerClassname == null) {
425: throw new PortalException("No '" + CONTROLLER_KEY
426: + "' specified.");
427: }
428:
429: // now lets load and instantiate the handler
430: Class c = null;
431: Object obj = null;
432:
433: try {
434: CarResources cRes = CarResources.getInstance();
435: ClassLoader cl = cRes.getClassLoader();
436: c = cl.loadClass(mControllerClassname);
437: } catch (Exception e) {
438: throw new PortalException("Class '" + mControllerClassname
439: + "' specified in parameter '" + CONTROLLER_KEY
440: + "' could not be loaded.", e);
441: } catch (NoClassDefFoundError e) {
442: throw new PortalException("Class '" + mControllerClassname
443: + "' specified in parameter '" + CONTROLLER_KEY
444: + "' could not be loaded.", e);
445: }
446:
447: try {
448: obj = c.newInstance();
449: } catch (Exception e) {
450: throw new PortalException("Unable to instantiate class '"
451: + mControllerClassname
452: + "' specified in parameter '" + CONTROLLER_KEY
453: + "'.", e);
454: }
455: try {
456: mController = (IController) obj;
457: } catch (ClassCastException cce) {
458: throw new PortalException("Class '" + mControllerClassname
459: + "' specified in parameter '" + CONTROLLER_KEY
460: + "' does not implement "
461: + IController.class.getName());
462: }
463: }
464:
465: /**
466: * The purpose of this method is to see if resources for this controller are
467: * currently being deployed and wait until they are done. The first thread
468: * in here for a specific controller class will take care of deploying while
469: * all others will wait until it is done. This is done to keep that class
470: * from being loaded by the classloader until it is sitting in a proper
471: * non-car class loader dependant location so that it and any other classes
472: * potentially in a CAR can be used by the JSPs. If left in and loaded from
473: * the CARs the classloader used by the JSPs will not be able to find them.
474: *
475: * @param classname
476: */
477: private void syncDeploymentOfResources(String classname) {
478: String resource = classname.replace('.', '/') + ".class";
479: CarResources cRes = CarResources.getInstance();
480: String car = cRes.getContainingCarPath(resource);
481:
482: if (car == null) // class not found in car, no deployment necessary
483: {
484: mDeploymentApproach.put(classname,
485: Channel.TRADITIONAL_DEPLOYMENT);
486: return;
487: }
488: if (!mLoaded.contains(car)) {
489: synchronized (Channel.class) {
490: if (!mLoaded.contains(car)) {
491: Deployer deployer = new Deployer();
492: deployer.deployResources(classname);
493: mDeploymentApproach.put(classname, (deployer
494: .isDeployedInCar() ? Channel.CAR_DEPLOYMENT
495: : Channel.TRADITIONAL_DEPLOYMENT));
496: mLoaded.add(car);
497: }
498: }
499: }
500: }
501:
502: //////////////// Implementation of IPrivileged ///////////////////
503:
504: /**
505: * Extracts the HttpServletRequest, HttpServletResponse, and
506: * HttpSession for use in delegated JSPs.
507: *
508: * @see org.jasig.portal.IPrivileged#setPortalControlStructures(org.jasig.portal.PortalControlStructures)
509: */
510: public void setPortalControlStructures(PortalControlStructures pcs)
511: throws PortalException {
512: mSession = pcs.getHttpSession();
513: mRequest = pcs.getHttpServletRequest();
514: mResponse = pcs.getHttpServletResponse();
515:
516: if (mController != null && mController instanceof IPrivileged) {
517: ((IPrivileged) mController).setPortalControlStructures(pcs);
518: }
519: }
520:
521: //////////////// Implementation of IDirectResponse ///////////////////
522:
523: /**
524: * Serves up any type of file to the browser as dictated by the controller
525: * if the controller supports IDirectResponse.
526: *
527: * @see org.jasig.portal.IDirectResponse#setResponse(javax.servlet.http.HttpServletResponse)
528: */
529: public void setResponse(HttpServletResponse response) {
530: if (mController != null
531: && mController instanceof IDirectResponse) {
532: ((IDirectResponse) mController).setResponse(response);
533: } else {
534: throw new UnsupportedOperationException("JSP Controller "
535: + mControllerClassname + " does not implement "
536: + IDirectResponse.class.getName() + ".");
537: }
538: }
539: }
|