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