001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.ext.jsp;
054:
055: import java.beans.IntrospectionException;
056: import java.io.ByteArrayInputStream;
057: import java.io.FilterInputStream;
058: import java.io.IOException;
059: import java.io.InputStream;
060: import java.net.MalformedURLException;
061: import java.util.ArrayList;
062: import java.util.HashMap;
063: import java.util.Iterator;
064: import java.util.List;
065: import java.util.Map;
066: import java.util.Set;
067: import java.util.zip.ZipEntry;
068: import java.util.zip.ZipInputStream;
069:
070: import javax.servlet.ServletContext;
071: import javax.servlet.http.HttpServletRequest;
072: import javax.xml.parsers.ParserConfigurationException;
073: import javax.xml.parsers.SAXParserFactory;
074:
075: import org.xml.sax.Attributes;
076: import org.xml.sax.EntityResolver;
077: import org.xml.sax.InputSource;
078: import org.xml.sax.Locator;
079: import org.xml.sax.SAXException;
080: import org.xml.sax.SAXParseException;
081: import org.xml.sax.XMLReader;
082: import org.xml.sax.helpers.DefaultHandler;
083:
084: import freemarker.core.Environment;
085: import freemarker.ext.servlet.FreemarkerServlet;
086: import freemarker.ext.servlet.HttpRequestHashModel;
087: import freemarker.log.Logger;
088: import freemarker.template.TemplateHashModel;
089: import freemarker.template.TemplateModel;
090: import freemarker.template.TemplateModelException;
091: import freemarker.template.utility.ClassUtil;
092:
093: /**
094: * A hash model associated with a servlet context that can load JSP tag
095: * libraries associated with that servlet context. An instance of this class is
096: * made available in the root data model of templates executed by
097: * {@link freemarker.ext.servlet.FreemarkerServlet} under key
098: * <tt>JspTaglibs</tt>. It can be added to custom servlets as well to enable JSP
099: * taglib integration in them as well.
100: * @version $Id: TaglibFactory.java,v 1.26 2005/06/13 20:52:55 szegedia Exp $
101: * @author Attila Szegedi
102: */
103: public class TaglibFactory implements TemplateHashModel {
104: private static final Logger logger = Logger
105: .getLogger("freemarker.jsp");
106:
107: // No TLDs have been looked up yet
108: private static final int LOOKUP_NONE = 0;
109: // Only explicit TLDs in web.xml have been looked up
110: private static final int LOOKUP_WEB_XML = 1;
111: // Both explicit TLDs and those in JARs have been looked up
112: private static final int LOOKUP_JARS = 2;
113:
114: private final ServletContext ctx;
115: private final Map taglibs = new HashMap();
116: private final Map locations = new HashMap();
117: private int lookupPhase = LOOKUP_NONE;
118:
119: /**
120: * Creates a new JSP taglib factory that will be used to load JSP taglibs
121: * for the web application represented by the passed servlet context.
122: * @param ctx the servlet context whose JSP tag libraries will this factory
123: * load.
124: */
125: public TaglibFactory(ServletContext ctx) {
126: this .ctx = ctx;
127: }
128:
129: /**
130: * Retrieves a JSP tag library identified by an URI. The matching of the URI
131: * to a JSP taglib is done as described in the JSP 1.2 FCS specification.
132: * @param uri the URI that describes the JSP taglib. It can be any of the
133: * three forms allowed by the JSP specification: absolute URI, root relative
134: * URI and non-root relative URI. Note that if a non-root relative URI is
135: * used it is resolved relative to the URL of the current request. In this
136: * case, the current request is obtained by looking up a
137: * {@link HttpRequestHashModel} object named <tt>Request</tt> in the root
138: * data model. FreemarkerServlet provides this object under the expected
139: * name, and custom servlets that want to integrate JSP taglib support
140: * should do the same.
141: * @return a hash model representing the JSP taglib. Each element of this
142: * hash model represents a single custom tag from the library, implemented
143: * as a {@link freemarker.template.TemplateTransformModel}.
144: */
145: public TemplateModel get(String uri) throws TemplateModelException {
146: uri = resolveRelativeUri(uri);
147: synchronized (taglibs) {
148: Taglib taglib = null;
149: taglib = (Taglib) taglibs.get(uri);
150: if (taglib != null) {
151: return taglib;
152: }
153:
154: taglib = new Taglib();
155: try {
156: do {
157: if (taglib.load(uri, ctx, locations)) {
158: taglibs.put(uri, taglib);
159: return taglib;
160: }
161: } while (getMoreTaglibLocations());
162: } catch (TemplateModelException e) {
163: throw e;
164: } catch (Exception e) {
165: throw new TemplateModelException(
166: "Could not load taglib information", e);
167: }
168: return null;
169: }
170: }
171:
172: /**
173: * Returns false.
174: */
175: public boolean isEmpty() {
176: return false;
177: }
178:
179: private boolean getMoreTaglibLocations()
180: throws MalformedURLException, ParserConfigurationException,
181: IOException, SAXException {
182: switch (lookupPhase) {
183: case LOOKUP_NONE: {
184: getLocationsFromWebXml();
185: lookupPhase = LOOKUP_WEB_XML;
186: return true;
187: }
188: case LOOKUP_WEB_XML: {
189: getLocationsFromLibJars();
190: lookupPhase = LOOKUP_JARS;
191: return true;
192: }
193: default: {
194: return false;
195: }
196: }
197: }
198:
199: private void getLocationsFromWebXml() throws MalformedURLException,
200: ParserConfigurationException, IOException, SAXException {
201: WebXmlParser webXmlParser = new WebXmlParser(locations);
202: InputStream in = ctx.getResourceAsStream("/WEB-INF/web.xml");
203: if (in == null) {
204: // No /WEB-INF/web.xml - do nothing
205: return;
206: }
207: try {
208: parseXml(in, ctx.getResource("/WEB-INF/web.xml")
209: .toExternalForm(), webXmlParser);
210: } finally {
211: in.close();
212: }
213: }
214:
215: private static class WebXmlParser extends DefaultHandler {
216: private final Map locations;
217:
218: private StringBuffer buf;
219: private String uri;
220: private String location;
221:
222: WebXmlParser(Map locations) {
223: this .locations = locations;
224: }
225:
226: public void startElement(String nsuri, String localName,
227: String qName, Attributes atts) {
228: if ("taglib-uri".equals(qName)
229: || "taglib-location".equals(qName)) {
230: buf = new StringBuffer();
231: }
232: }
233:
234: public void characters(char[] chars, int off, int len) {
235: if (buf != null) {
236: buf.append(chars, off, len);
237: }
238: }
239:
240: public void endElement(String nsuri, String localName,
241: String qName) {
242: if ("taglib-uri".equals(qName)) {
243: uri = buf.toString().trim();
244: buf = null;
245: } else if ("taglib-location".equals(qName)) {
246: location = buf.toString().trim();
247: if (location.indexOf("://") == -1
248: && !location.startsWith("/")) {
249: location = "/WEB-INF/" + location;
250: }
251: buf = null;
252: } else if ("taglib".equals(qName)) {
253: String[] loc = new String[2];
254: loc[0] = location;
255: if (location.endsWith(".jar")
256: || location.endsWith(".zip")) {
257: loc[1] = "META-INF/taglib.tld";
258: }
259: locations.put(uri, loc);
260: if (logger.isDebugEnabled()) {
261: logger.debug("web.xml assigned URI " + uri
262: + " to location " + loc[0]
263: + (loc[1] != null ? "!" + loc[1] : ""));
264: }
265: }
266: }
267: }
268:
269: private void getLocationsFromLibJars()
270: throws ParserConfigurationException, IOException,
271: SAXException {
272: Set libs = ctx.getResourcePaths("/WEB-INF/lib");
273: for (Iterator iter = libs.iterator(); iter.hasNext();) {
274: String path = (String) iter.next();
275: if (path.endsWith(".jar") || path.endsWith(".zip")) {
276: ZipInputStream zin = new ZipInputStream(ctx
277: .getResourceAsStream(path));
278: // Make stream uncloseable by XML parsers
279: InputStream uin = new FilterInputStream(zin) {
280: public void close() {
281: }
282: };
283: try {
284: for (;;) {
285: ZipEntry ze = zin.getNextEntry();
286: if (ze == null) {
287: break;
288: }
289: String zname = ze.getName();
290: if (zname.startsWith("META-INF/")
291: && zname.endsWith(".tld")) {
292: String url = "jar:"
293: + ctx.getResource(path)
294: .toExternalForm() + "!"
295: + zname;
296: String loc = getTldUri(uin, url);
297: if (loc != null) {
298: locations.put(loc, new String[] { path,
299: zname });
300: if (logger.isDebugEnabled()) {
301: logger.debug("libjar assigned URI "
302: + loc + " to location "
303: + path + "!" + zname);
304: }
305: }
306: }
307: }
308: } finally {
309: zin.close();
310: }
311: }
312: }
313: }
314:
315: private String getTldUri(InputStream in, String url)
316: throws ParserConfigurationException, IOException,
317: SAXException {
318: TldUriReader tur = new TldUriReader();
319: parseXml(in, url, tur);
320: return tur.getUri();
321: }
322:
323: private static class TldUriReader extends DefaultHandler {
324:
325: private StringBuffer buf;
326: private String uri;
327:
328: TldUriReader() {
329: }
330:
331: String getUri() {
332: return uri;
333: }
334:
335: public void startElement(String nsuri, String localName,
336: String qName, Attributes atts) {
337: if ("uri".equals(qName)) {
338: buf = new StringBuffer();
339: }
340: }
341:
342: public void characters(char[] chars, int off, int len) {
343: if (buf != null) {
344: buf.append(chars, off, len);
345: }
346: }
347:
348: public void endElement(String nsuri, String localName,
349: String qName) {
350: if ("uri".equals(qName)) {
351: uri = buf.toString().trim();
352: buf = null;
353: }
354: }
355: }
356:
357: private static void parseXml(InputStream in, String url,
358: DefaultHandler handler)
359: throws ParserConfigurationException, IOException,
360: SAXException {
361: InputSource is = new InputSource();
362: is.setByteStream(in);
363: is.setSystemId(url);
364: SAXParserFactory factory = SAXParserFactory.newInstance();
365: factory.setNamespaceAware(false);
366: factory.setValidating(true);
367: XMLReader reader = factory.newSAXParser().getXMLReader();
368: reader.setEntityResolver(new LocalTaglibDtds());
369: reader.setContentHandler(handler);
370: reader.parse(is);
371: }
372:
373: private static final class Taglib implements TemplateHashModel {
374: private Map tags;
375:
376: Taglib() {
377: }
378:
379: public TemplateModel get(String key) {
380: return (TagTransformModel) tags.get(key);
381: }
382:
383: public boolean isEmpty() {
384: return false;
385: }
386:
387: boolean load(String uri, ServletContext ctx, Map locations)
388: throws ParserConfigurationException, IOException,
389: SAXException, TemplateModelException {
390: String[] tldPath = getTldPath(uri, locations);
391: if (logger.isDebugEnabled()) {
392: if (tldPath == null) {
393: logger.debug("Loading taglib " + uri
394: + " from location null");
395: } else {
396: logger.debug("Loading taglib "
397: + uri
398: + " from location "
399: + tldPath[0]
400: + (tldPath[1] != null ? "!" + tldPath[1]
401: : ""));
402: }
403: }
404: tags = loadTaglib(tldPath, ctx);
405: if (tags != null) {
406: locations.remove(uri);
407: return true;
408: } else {
409: return false;
410: }
411: }
412: }
413:
414: private static final Map loadTaglib(String[] tldPath,
415: ServletContext ctx) throws ParserConfigurationException,
416: IOException, SAXException, TemplateModelException {
417: if (tldPath == null) {
418: return null;
419: }
420: String filePath = tldPath[0];
421: TldParser tldParser = new TldParser();
422: InputStream in = ctx.getResourceAsStream(filePath);
423: if (in == null) {
424: throw new TemplateModelException(
425: "Could not find webapp resource " + filePath);
426: }
427: String url = ctx.getResource(filePath).toExternalForm();
428: try {
429: String jarPath = tldPath[1];
430: if (jarPath != null) {
431: ZipInputStream zin = new ZipInputStream(in);
432: for (;;) {
433: ZipEntry ze = zin.getNextEntry();
434: if (ze == null) {
435: throw new TemplateModelException(
436: "Could not find JAR entry " + jarPath
437: + " inside webapp resource "
438: + filePath);
439: }
440: String zname = ze.getName();
441: if (zname.equals(jarPath)) {
442: parseXml(zin, "jar:" + url + "!" + zname,
443: tldParser);
444: break;
445: }
446: }
447: } else {
448: parseXml(in, url, tldParser);
449: }
450: } finally {
451: in.close();
452: }
453: EventForwarding eventForwarding = EventForwarding
454: .getInstance(ctx);
455: if (eventForwarding != null) {
456: eventForwarding.addListeners(tldParser.getListeners());
457: } else if (tldParser.getListeners().size() > 0) {
458: throw new TemplateModelException(
459: "Event listeners specified in the TLD could not be "
460: + " registered since the web application doesn't have a"
461: + " listener of class "
462: + EventForwarding.class.getName()
463: + ". To remedy this, add this element to web.xml:\n"
464: + "| <listener>\n" + "| <listener-class>"
465: + EventForwarding.class.getName()
466: + "</listener-class>\n" + "| </listener>");
467: }
468: return tldParser.getTags();
469: }
470:
471: private static final String[] getTldPath(String uri, Map locations) {
472: String[] path = (String[]) locations.get(uri);
473: // If location was explicitly defined in web.xml, or discovered in a
474: // JAR file, use it. (Hopefully this is 99% of the cases)
475: if (path != null) {
476: return path;
477: }
478:
479: // If there was no explicit mapping in web.xml, but URI is a
480: // ROOT_REL_URI, return it (JSP.7.6.3.2)
481: if (uri.startsWith("/")) {
482: path = new String[2];
483: path[0] = uri;
484: if (uri.endsWith(".jar") || uri.endsWith(".zip")) {
485: path[1] = "META-INF/taglib.tld";
486: }
487: return path;
488: }
489:
490: return null;
491: }
492:
493: private static String resolveRelativeUri(String uri)
494: throws TemplateModelException {
495: // Absolute and root-relative URIs are left as they are.
496: if (uri.startsWith("/") || uri.indexOf("://") != -1) {
497: return uri;
498: }
499:
500: // Otherwise it is a NOROOT_REL_URI, and has to be resolved relative
501: // to current page... We have to obtain the request object to know what
502: // is the URL of the current page (this assumes there's a
503: // HttpRequestHashModel under name FreemarkerServlet.KEY_REQUEST in the
504: // environment...) (JSP.7.6.3.2)
505: TemplateModel reqHash = Environment.getCurrentEnvironment()
506: .getVariable(FreemarkerServlet.KEY_REQUEST_PRIVATE);
507: if (reqHash instanceof HttpRequestHashModel) {
508: HttpServletRequest req = ((HttpRequestHashModel) reqHash)
509: .getRequest();
510: String pi = req.getPathInfo();
511: String reqPath = req.getServletPath();
512: if (reqPath == null) {
513: reqPath = "";
514: }
515: reqPath += (pi == null ? "" : pi);
516: // We don't care about paths with ".." in them. If the container
517: // wishes to resolve them on its own, let it be.
518: int lastSlash = reqPath.lastIndexOf('/');
519: if (lastSlash != -1) {
520: return reqPath.substring(0, lastSlash + 1) + uri;
521: } else {
522: return '/' + uri;
523: }
524: }
525: throw new TemplateModelException("Can't resolve relative URI "
526: + uri + " as request URL information is unavailable.");
527: }
528:
529: private static final class TldParser extends DefaultHandler {
530: private final Map tags = new HashMap();
531: private final List listeners = new ArrayList();
532:
533: private Locator locator;
534: private StringBuffer buf;
535: private String tagName;
536: private String tagClass;
537:
538: Map getTags() {
539: return tags;
540: }
541:
542: List getListeners() {
543: return listeners;
544: }
545:
546: public void setDocumentLocator(Locator locator) {
547: this .locator = locator;
548: }
549:
550: public void startElement(String nsuri, String localName,
551: String qName, Attributes atts) {
552: if ("name".equals(qName) || "tagclass".equals(qName)
553: || "tag-class".equals(qName)
554: || "listener-class".equals(qName)) {
555: buf = new StringBuffer();
556: }
557: }
558:
559: public void characters(char[] chars, int off, int len) {
560: if (buf != null) {
561: buf.append(chars, off, len);
562: }
563: }
564:
565: public void endElement(String nsuri, String localName,
566: String qName) throws SAXParseException {
567: if ("name".equals(qName)) {
568: if (tagName == null) {
569: tagName = buf.toString().trim();
570: }
571: buf = null;
572: } else if ("tagclass".equals(qName)
573: || "tag-class".equals(qName)) {
574: tagClass = buf.toString().trim();
575: buf = null;
576: } else if ("tag".equals(qName)) {
577: try {
578: tags.put(tagName, new TagTransformModel(ClassUtil
579: .forName(tagClass)));
580: tagName = null;
581: tagClass = null;
582: } catch (IntrospectionException e) {
583: throw new SAXParseException(
584: "Can't introspect tag class " + tagClass,
585: locator, e);
586: } catch (ClassNotFoundException e) {
587: throw new SAXParseException("Can't find tag class "
588: + tagClass, locator, e);
589: }
590: } else if ("listener-class".equals(qName)) {
591: String listenerClass = buf.toString().trim();
592: buf = null;
593: try {
594: listeners.add(ClassUtil.forName(listenerClass)
595: .newInstance());
596: } catch (Exception e) {
597: throw new SAXParseException(
598: "Can't instantiate listener class "
599: + listenerClass, locator, e);
600: }
601: }
602: }
603: }
604:
605: private static final Map dtds = new HashMap();
606: static {
607: // JSP taglib 2.0
608: dtds
609: .put(
610: "http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd",
611: "web-jsptaglibrary_2_0.xsd");
612: // JSP taglib 1.2
613: dtds
614: .put(
615: "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN",
616: "web-jsptaglibrary_1_2.dtd");
617: dtds.put("http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd",
618: "web-jsptaglibrary_1_2.dtd");
619: // JSP taglib 1.1
620: dtds
621: .put(
622: "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN",
623: "web-jsptaglibrary_1_1.dtd");
624: dtds
625: .put(
626: "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd",
627: "web-jsptaglibrary_1_1.dtd");
628: // Servlet 2.4
629: dtds.put("http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd",
630: "web-app_2_4.xsd");
631: // Servlet 2.3
632: dtds
633: .put(
634: "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN",
635: "web-app_2_3.dtd");
636: dtds.put("http://java.sun.com/dtd/web-app_2_3.dtd",
637: "web-app_2_3.dtd");
638: // Servlet 2.2
639: dtds
640: .put(
641: "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN",
642: "web-app_2_2.dtd");
643: dtds.put("http://java.sun.com/j2ee/dtds/web-app_2_2.dtd",
644: "web-app_2_2.dtd");
645: }
646:
647: private static final class LocalTaglibDtds implements
648: EntityResolver {
649: public InputSource resolveEntity(String publicId,
650: String systemId) {
651: String resourceName = (String) dtds.get(publicId);
652: if (resourceName == null) {
653: resourceName = (String) dtds.get(systemId);
654: }
655: InputStream resourceStream;
656: if (resourceName != null) {
657: resourceStream = getClass().getResourceAsStream(
658: resourceName);
659: } else {
660: // Fake an empty stream for unknown DTDs
661: resourceStream = new ByteArrayInputStream(new byte[0]);
662: }
663: InputSource is = new InputSource();
664: is.setPublicId(publicId);
665: is.setSystemId(systemId);
666: is.setByteStream(resourceStream);
667: return is;
668: }
669: }
670: }
|