001: /* Copyright 2001, 2005, 2007 The JA-SIG Collaborative. All rights reserved.
002: * See license distributed with this file and
003: * available online at http://www.uportal.org/license.html
004: */
005:
006: package org.jasig.portal.channels;
007:
008: import java.io.IOException;
009: import java.io.InputStream;
010: import java.net.URI;
011: import java.net.URISyntaxException;
012: import java.net.URL;
013: import java.net.URLConnection;
014: import java.util.Enumeration;
015: import java.util.HashMap;
016: import java.util.Iterator;
017: import java.util.Map;
018:
019: import javax.xml.parsers.DocumentBuilder;
020: import javax.xml.parsers.DocumentBuilderFactory;
021:
022: import org.jasig.portal.ChannelCacheKey;
023: import org.jasig.portal.ChannelRuntimeData;
024: import org.jasig.portal.ChannelStaticData;
025: import org.jasig.portal.GeneralRenderingException;
026: import org.jasig.portal.ICacheable;
027: import org.jasig.portal.IChannel;
028: import org.jasig.portal.PortalException;
029: import org.jasig.portal.ResourceMissingException;
030: import org.jasig.portal.i18n.LocaleManager;
031: import org.jasig.portal.properties.PropertiesManager;
032: import org.jasig.portal.security.IPermission;
033: import org.jasig.portal.security.LocalConnectionContext;
034: import org.jasig.portal.tools.versioning.Version;
035: import org.jasig.portal.tools.versioning.VersionsManager;
036: import org.jasig.portal.utils.DTDResolver;
037: import org.jasig.portal.utils.ResourceLoader;
038: import org.jasig.portal.utils.XSLT;
039: import org.jasig.portal.utils.uri.BlockedUriException;
040: import org.jasig.portal.utils.uri.IUriScrutinizer;
041: import org.jasig.portal.utils.uri.PrefixUriScrutinizer;
042: import org.w3c.dom.Document;
043: import org.xml.sax.ContentHandler;
044:
045: /**
046: * <p>A channel which transforms XML for rendering in the portal.</p>
047: *
048: * <p>Static channel parameters to be supplied:
049: * <ol>
050: * <li> "xmlUri" - a URI representing the source XML document</li>
051: * <li> "sslUri" - a URI representing the corresponding .ssl (stylesheet list) file</li>
052: * <li> "xslTitle" - a title representing the stylesheet (optional)
053: * <i>If no title parameter is specified, a default
054: * stylesheet will be chosen according to the media</i>
055: * </li>
056: * <li> "xslUri" - a URI representing the stylesheet to use
057: * <i>If <code>xslUri</code> is supplied, <code>sslUri</code>
058: * and <code>xslTitle</code> will be ignored.</i>
059: * </li>
060: * <li> "cacheTimeout" - the amount of time (in seconds) that the contents of the
061: * channel should be cached (optional). If this parameter is left
062: * out, a default timeout value will be used.
063: * </li>
064: * <li> "cacheScope" - the desired scope for caching channel responses. Defaults to
065: * "instance" indicating that cache is per-channel-instance-per-user. Cache will
066: * not match across users. This is typically desirable to prevent user A from
067: * seeing user B's data. Alternately, this parameter may be set to "system"
068: * indicating that caching should occur across the entire portal
069: * such that two different users' instances of like-configured CGenericXSLT channels will share cache.
070: * </li>
071: * <li> "upc_localConnContext" - The class name of the ILocalConnectionContext
072: * implementation.
073: * <i>Use when local data needs to be sent with the
074: * request for the URL.</i>
075: * </li>
076: * <li> "upc_allow_xmlUri_prefixes" - Optional parameter specifying as a whitespace
077: * delimited String the allowable xmlUri prefixes.
078: * <i>Defaults to "http:// https://"</i>
079: * </li>
080: * <li> "upc_deny_xmlUri_prefixes" - Optional parameter specifying as a whitespace
081: * delimited String URI prefixes that should block a URI
082: * as xmlUri even if it matched one of the allow prefixes.
083: * <i>Defaults to ""</i>
084: * </li>
085: * <li> "restrict_xmlUri_inStaticData" - Optional parameter specifying whether
086: * the xmlUri should be restricted according to the allow and
087: * deny prefix rules above as presented in ChannelStaticData
088: * or just as presented in ChannelRuntimeData. "true" means
089: * both ChannelStaticData and ChannelRuntimeData will be restricted.
090: * Any other value or the parameter not being present means
091: * only ChannelRuntimeData will be restricted. It is important
092: * to set this value to true when using subscribe-time
093: * channel parameter configuration of the xmlUri.
094: * </p>
095: * <p>The xmlUri and xslTitle static parameters above can be overridden by including
096: * parameters of the same name (<code>xmlUri</code> and/or <code>xslTitle</code>)
097: * in the HttpRequest string. Prior to uPortal 2.5.1 sslUri and xslUri could also
098: * be overridden -- these features have been removed to improve the security of
099: * CGenericXSLT instances. </p>
100: * <p>
101: * Additionally, as of uPortal 2.5.1, the xmlUri must match an allowed URI prefix.
102: * By default http:// and https:// URIs are allowed. If you are using the
103: * empty document or another XML file from the classpath or from the filesystem,
104: * you will need to allow a prefix to or the full path of that resource.
105: * </p>
106: * <p>This channel can be used for all XML formats including RSS.
107: * Any other parameters passed to this channel via HttpRequest will get
108: * passed in turn to the XSLT stylesheet as stylesheet parameters. They can be
109: * read in the stylesheet as follows:
110: * <code><xsl:param name="yourParamName">aDefaultValue</xsl:param></code></p>
111: * <p>CGenericXSLT is also useful for channels that have no dynamic data. In these types
112: * of channels, all the markup comes from the XSLT stylesheets. An empty XML document
113: * can be used and is included with CGenericXSLT. Just set the xml parameter to this
114: * empty document and allow the path to the empty document.</p>
115: * @author Steve Toth, stoth@interactivebusiness.com
116: * @author Ken Weiner, kweiner@unicon.net
117: * @author Peter Kharchenko pkharchenko@unicon.net
118: * @version $Revision: 36839 $
119: */
120:
121: public class CGenericXSLT extends BaseChannel implements IChannel,
122: ICacheable {
123:
124: private String xmlUri;
125: private String sslUri;
126: private String xslTitle;
127: private String xslUri;
128: private final Map params = new HashMap();
129: private long cacheTimeout;
130: private LocalConnectionContext localConnContext = null;
131:
132: /**
133: * Either ChannelCacheKey.INSTANCE_KEY_SCOPE or ChannelCacheKey.SYSTEM_KEY_SCOPE.
134: * Indicates cache key scope that should be used in fulfilling the ICacheable API.
135: * Populated from 'cache-scope' channel parameter.
136: */
137: private int channelCacheScope = ChannelCacheKey.INSTANCE_KEY_SCOPE;
138:
139: private IUriScrutinizer uriScrutinizer;
140:
141: public CGenericXSLT() {
142:
143: }
144:
145: public void setStaticData(ChannelStaticData sd)
146: throws ResourceMissingException {
147:
148: String allowXmlUriPrefixesParam = sd
149: .getParameter("upc_allow_xmlUri_prefixes");
150: String denyXmlUriPrefixesParam = sd
151: .getParameter("upc_deny_xmlUri_prefixes");
152:
153: IUriScrutinizer temp = PrefixUriScrutinizer
154: .instanceFromParameters(allowXmlUriPrefixesParam,
155: denyXmlUriPrefixesParam);
156:
157: if (temp == null) {
158: throw new IllegalArgumentException(
159: "CGenericXSLT channel requires a non-null IUriScrutinizer");
160: }
161: uriScrutinizer = temp;
162:
163: xmlUri = sslUri = xslTitle = xslUri = null;
164: cacheTimeout = PropertiesManager
165: .getPropertyAsLong("org.jasig.portal.channels.CGenericXSLT.default_cache_timeout");
166:
167: // determine whether we should restrict what URIs we accept as the xmlUri from
168: // ChannelStaticData
169: String scrutinizeXmlUriAsStaticDataString = sd
170: .getParameter("restrict_xmlUri_inStaticData");
171: boolean scrutinizeXmlUriAsStaticData = "true"
172: .equals(scrutinizeXmlUriAsStaticDataString);
173:
174: String xmlUriParam = sd.getParameter("xmlUri");
175: if (scrutinizeXmlUriAsStaticData) {
176: // apply configured xmlUri restrictions
177: setXmlUri(xmlUriParam);
178: } else {
179: // set the field directly to avoid applying xmlUri restrictions
180: xmlUri = xmlUriParam;
181: }
182:
183: sslUri = sd.getParameter("sslUri");
184: xslTitle = sd.getParameter("xslTitle");
185: xslUri = sd.getParameter("xslUri");
186:
187: String cacheTimeoutText = sd.getParameter("cacheTimeout");
188:
189: if (cacheTimeoutText != null)
190: cacheTimeout = Long.parseLong(cacheTimeoutText);
191:
192: String cacheScopeText = sd
193: .getParameter(ICacheable.CHANNEL_CACHE_KEY_SCOPE_PARAM_NAME);
194:
195: if (cacheScopeText != null) {
196: String trimmedCacheScopeText = cacheScopeText.trim();
197: if (trimmedCacheScopeText.length() > 0) {
198: if (trimmedCacheScopeText
199: .equals(ICacheable.CHANNEL_CACHE_KEY_SYSTEM_SCOPE)) {
200: this .channelCacheScope = ChannelCacheKey.SYSTEM_KEY_SCOPE;
201: } else if (trimmedCacheScopeText
202: .equals(ICacheable.CHANNEL_CACHE_KEY_SYSTEM_SCOPE)) {
203: this .channelCacheScope = ChannelCacheKey.INSTANCE_KEY_SCOPE;
204: } else {
205: log
206: .warn("CGnericXSLT ChannelStaticData parameter ["
207: + ICacheable.CHANNEL_CACHE_KEY_SYSTEM_SCOPE
208: + "] had unrecognized value ["
209: + trimmedCacheScopeText
210: + "]; 'failing shut' with instance scoped caching.");
211: this .channelCacheScope = ChannelCacheKey.INSTANCE_KEY_SCOPE;
212: }
213: }
214: }
215:
216: String connContext = sd.getParameter("upc_localConnContext");
217: if (connContext != null && !connContext.trim().equals("")) {
218: try {
219: localConnContext = (LocalConnectionContext) Class
220: .forName(connContext).newInstance();
221: localConnContext.init(sd);
222: } catch (Exception e) {
223: log.error(
224: "CGenericXSLT: Cannot initialize ILocalConnectionContext: "
225: + connContext, e);
226: }
227: }
228: params.putAll(sd);
229: }
230:
231: public void setRuntimeData(ChannelRuntimeData rd) {
232: // because of the portal rendering model, there is no reason to synchronize on state
233: runtimeData = rd;
234: String xmlUri = rd.getParameter("xmlUri");
235:
236: if (xmlUri != null)
237: setXmlUri(xmlUri);
238:
239: // prior to uPortal 2.5.1 sslUri was configurable via ChannelRuntimeProperties
240: // this feature has been removed to improve security of CGenericXSLT instances.
241:
242: String s = rd.getParameter("xslTitle");
243:
244: if (s != null)
245: xslTitle = s;
246:
247: // grab the parameters and stuff them all into the state object
248: Enumeration enum1 = rd.getParameterNames();
249: while (enum1.hasMoreElements()) {
250: String n = (String) enum1.nextElement();
251: if (rd.getParameter(n) != null) {
252: params.put(n, rd.getParameter(n));
253: }
254: }
255: }
256:
257: public void renderXML(ContentHandler out) throws PortalException {
258: if (log.isDebugEnabled())
259: log.debug(this );
260:
261: // OK, pass everything we got cached in params...
262: if (params != null) {
263: Iterator it = params.keySet().iterator();
264: while (it.hasNext()) {
265: String n = (String) it.next();
266: if (params.get((Object) n) != null) {
267: runtimeData.put(n, params.get((Object) n));
268: }
269: }
270: }
271:
272: Document xmlDoc;
273: InputStream inputStream = null;
274:
275: try {
276: DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory
277: .newInstance();
278: docBuilderFactory.setNamespaceAware(true);
279: DocumentBuilder docBuilder = docBuilderFactory
280: .newDocumentBuilder();
281: DTDResolver dtdResolver = new DTDResolver();
282: docBuilder.setEntityResolver(dtdResolver);
283:
284: URL url;
285: if (localConnContext != null)
286: url = ResourceLoader.getResourceAsURL(this .getClass(),
287: localConnContext.getDescriptor(xmlUri,
288: runtimeData));
289: else
290: url = ResourceLoader.getResourceAsURL(this .getClass(),
291: xmlUri);
292:
293: URLConnection urlConnect = url.openConnection();
294:
295: if (localConnContext != null) {
296: try {
297: localConnContext.sendLocalData(urlConnect,
298: runtimeData);
299: } catch (Exception e) {
300: log
301: .error(
302: "CGenericXSLT: Unable to send data through "
303: + runtimeData
304: .getParameter("upc_localConnContext"),
305: e);
306: }
307: }
308: inputStream = urlConnect.getInputStream();
309: xmlDoc = docBuilder.parse(inputStream);
310: } catch (IOException ioe) {
311: throw new ResourceMissingException(xmlUri, "", ioe);
312: } catch (Exception e) {
313: throw new GeneralRenderingException("Problem parsing "
314: + xmlUri, e);
315: } finally {
316: try {
317: if (inputStream != null)
318: inputStream.close();
319: } catch (IOException ioe) {
320: throw new PortalException(
321: "CGenericXSLT:renderXML():: could not close InputStream",
322: ioe);
323: }
324: }
325:
326: runtimeData
327: .put("baseActionURL", runtimeData.getBaseActionURL());
328: runtimeData.put("isRenderingAsRoot", String.valueOf(runtimeData
329: .isRenderingAsRoot()));
330:
331: // add version parameters (used in footer channel)
332: VersionsManager versionsManager = VersionsManager.getInstance();
333: Version[] versions = versionsManager.getVersions();
334:
335: for (Version version : versions) {
336: String paramName = "version-" + version.getFname();
337: runtimeData.setParameter(paramName, version.dottedTriple());
338: }
339:
340: Version uPortalVersion = versionsManager
341: .getVersion(IPermission.PORTAL_FRAMEWORK);
342:
343: // The "uP_productAndVersion" parameter is deprecated
344: // instead use the "version-UP_FRAMEWORK" and other version parameters
345: // generated immediately previously.
346: runtimeData.put("uP_productAndVersion", "uPortal "
347: + uPortalVersion.dottedTriple());
348:
349: // OK, pass everything we got cached in params...
350: if (params != null) {
351: Iterator it = params.keySet().iterator();
352: while (it.hasNext()) {
353: String n = (String) it.next();
354: if (params.get((Object) n) != null) {
355: runtimeData.put(n, params.get((Object) n));
356: }
357: }
358: }
359:
360: XSLT xslt = XSLT.getTransformer(this );
361: xslt.setXML(xmlDoc);
362: if (xslUri != null)
363: xslt.setXSL(xslUri);
364: else
365: xslt.setXSL(sslUri, xslTitle, runtimeData.getBrowserInfo());
366: xslt.setTarget(out);
367: xslt.setStylesheetParameters(runtimeData);
368: xslt.transform();
369: }
370:
371: public ChannelCacheKey generateKey() {
372: ChannelCacheKey k = new ChannelCacheKey();
373: k.setKey(getKey());
374: k.setKeyScope(this .channelCacheScope);
375: k.setKeyValidity(new Long(System.currentTimeMillis()));
376: return k;
377: }
378:
379: public boolean isCacheValid(Object validity) {
380: if (!(validity instanceof Long))
381: return false;
382:
383: return (System.currentTimeMillis()
384: - ((Long) validity).longValue() < cacheTimeout * 1000);
385: }
386:
387: private String getKey() {
388: // Maybe not the best way to generate a key, but it seems to work.
389: // If you know a better way, please change it!
390: StringBuffer sbKey = new StringBuffer(1024);
391: sbKey.append("xmluri:").append(xmlUri).append(", ");
392: sbKey.append("sslUri:").append(sslUri).append(", ");
393:
394: // xslUri may either be specified as a parameter to this channel or we will
395: // get it by looking in the stylesheet list file
396: String xslUriForKey = xslUri;
397: try {
398: if (xslUriForKey == null) {
399: String s = ResourceLoader.getResourceAsURLString(
400: CGenericXSLT.class, sslUri);
401: xslUriForKey = XSLT.getStylesheetURI(s, runtimeData
402: .getBrowserInfo());
403: }
404: } catch (Exception e) {
405: log.error(e, e);
406: xslUriForKey = "Not attainable: " + e;
407: }
408:
409: sbKey.append("locales:").append(
410: LocaleManager.stringValueOf(runtimeData.getLocales()));
411: sbKey.append("xslUri:").append(xslUriForKey).append(", ");
412: sbKey.append("cacheTimeout:").append(cacheTimeout).append(", ");
413: sbKey.append("isRenderingAsRoot:").append(
414: runtimeData.isRenderingAsRoot()).append(", ");
415:
416: // If a local connection context is configured, include its descriptor in the key
417: if (localConnContext != null)
418: sbKey.append("descriptor:")
419: .append(
420: localConnContext.getDescriptor(xmlUri,
421: runtimeData)).append(", ");
422:
423: sbKey.append("params:").append(params.toString());
424: return sbKey.toString();
425: }
426:
427: /**
428: * Set the URI or resource-relative-path of the XML this CGenericXSLT should
429: * render.
430: * @param xmlUriArg URI or local resource path to the XML this channel should render.
431: * @throws IllegalArgumentException if xmlUriArg specifies a missing resource
432: * or if the URI has bad syntax
433: * @throws BlockedUriException if the xmlUriArg is blocked for policy reasons
434: */
435: private void setXmlUri(String xmlUriArg) {
436: URL url = null;
437: try {
438: url = ResourceLoader.getResourceAsURL(this .getClass(),
439: xmlUriArg);
440: } catch (ResourceMissingException e) {
441: IllegalArgumentException iae = new IllegalArgumentException(
442: "Resource [" + xmlUriArg + "] missing.");
443: iae.initCause(e);
444: throw iae;
445: }
446:
447: String urlString = url.toExternalForm();
448: try {
449: uriScrutinizer.scrutinize(new URI(urlString));
450: } catch (URISyntaxException e1) {
451: IllegalArgumentException iae2 = new IllegalArgumentException(
452: "xmlUri [" + xmlUriArg
453: + "] resolved to a URI with bad syntax.");
454: iae2.initCause(e1);
455: throw iae2;
456: }
457:
458: xmlUri = xmlUriArg;
459: }
460:
461: public String toString() {
462: StringBuffer str = new StringBuffer();
463: str.append("xmlUri = " + xmlUri + "\n");
464: str.append("xslUri = " + xslUri + "\n");
465: str.append("sslUri = " + sslUri + "\n");
466: str.append("xslTitle = " + xslTitle + "\n");
467: if (params != null) {
468: str.append("params = " + params.toString() + "\n");
469: }
470: return str.toString();
471: }
472: }
|