blob: 54cf6a0c6e0c857444b5f430a0dec393f0d631a1 [file] [log] [blame]
/*
* Copyright (c) 2003 The Visigoth Software Society. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowledgement:
* "This product includes software developed by the
* Visigoth Software Society (http://www.visigoths.org/)."
* Alternately, this acknowledgement may appear in the software itself,
* if and wherever such third-party acknowledgements normally appear.
*
* 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
* project contributors may be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact visigoths@visigoths.org.
*
* 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
* nor may "FreeMarker" or "Visigoth" appear in their names
* without prior written permission of the Visigoth Software Society.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Visigoth Software Society. For more
* information on the Visigoth Software Society, please see
* http://www.visigoths.org/
*/
package freemarker.ext.jsp;
import java.beans.IntrospectionException;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.tagext.Tag;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import freemarker.core.Environment;
import freemarker.ext.servlet.FreemarkerServlet;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.log.Logger;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.StringUtil;
/**
* A hash model associated with a servlet context that can load JSP tag
* libraries associated with that servlet context. An instance of this class is
* made available in the root data model of templates executed by
* {@link freemarker.ext.servlet.FreemarkerServlet} under key
* <tt>JspTaglibs</tt>. It can be added to custom servlets as well to enable JSP
* taglib integration in them as well.
* @author Attila Szegedi
*/
public class TaglibFactory implements TemplateHashModel {
private static final Logger logger = Logger.getLogger("freemarker.jsp");
private static final String DEFAULT_JAR_TLD = "META-INF/taglib.tld";
// No TLDs have been looked up yet
private static final int LOOKUP_NONE = 0;
// Only taglibs defined in web.xml have been looked up
private static final int LOOKUP_WEB_XML = 1;
// Both taglibs in web.xml and those in JARs and TLD files have been looked up
private static final int LOOKUP_WEB_APP = 2;
private static final int ABS_URI = 0;
private static final int ROOT_REL_URI = 1;
private static final int NOROOT_REL_URI = 2;
private final ServletContext ctx;
private final Map taglibs = new HashMap();
private final Map locations = new HashMap();
private int lookupPhase = LOOKUP_NONE;
/**
* Creates a new JSP taglib factory that will be used to load JSP taglibs
* for the web application represented by the passed servlet context.
* @param ctx the servlet context whose JSP tag libraries will this factory
* load.
*/
public TaglibFactory(ServletContext ctx) {
this.ctx = ctx;
}
/**
* Retrieves a JSP tag library identified by an URI. The matching of the URI
* to a JSP taglib is done as described in the JSP 1.2 FCS specification.
* @param uri the URI that describes the JSP taglib. It can be any of the
* three forms allowed by the JSP specification: absolute URI, root relative
* URI and non-root relative URI. Note that if a non-root relative URI is
* used it's resolved relative to the URL of the current request. In this
* case, the current request is obtained by looking up a
* {@link HttpRequestHashModel} object named <tt>Request</tt> in the root
* data model. FreemarkerServlet provides this object under the expected
* name, and custom servlets that want to integrate JSP taglib support
* should do the same.
* @return a hash model representing the JSP taglib. Each element of this
* hash model represents a single custom tag from the library, implemented
* as a {@link freemarker.template.TemplateTransformModel}.
*/
public TemplateModel get(String uri) throws TemplateModelException {
synchronized (taglibs) {
final Taglib taglib = (Taglib) taglibs.get(uri);
if(taglib != null) {
return taglib;
}
try {
// Make sure we have mappings from at least web.xml
if(lookupPhase == LOOKUP_NONE) {
addLocationsFromWebXml();
lookupPhase = LOOKUP_WEB_XML;
}
// Try explicit mapping
TldPath path = (TldPath)locations.get(uri);
if(path != null) {
return loadTaglib(path, uri);
}
// Make sure we have mappings from .jar and .tld files, too.
// Note that this delays scanning the WEB-INF directory as long
// as taglibs can be found based on web.xml mapping.
if(lookupPhase == LOOKUP_WEB_XML) {
addLocationsFromWebApp();
lookupPhase = LOOKUP_WEB_APP;
// Try newly found explicit mappings
path = (TldPath)locations.get(uri);
if(path != null) {
return loadTaglib(path, uri);
}
}
// No mappings found, try treating the path as explicit path
switch(getUriType(uri)) {
case ABS_URI: {
// Absolute URIs can only be resolved through mapping
throw new TemplateModelException("No mapping defined for " + uri);
}
case NOROOT_REL_URI: {
// Resolve URI relative to the current page
uri = resolveRelativeUri(uri);
// Intentional fallthrough
}
case ROOT_REL_URI: {
// If it's a .jar or .zip, add default TLD entry within it.
if(uri.endsWith(".jar") || uri.endsWith(".zip")) {
return loadTaglib(new TldPath(uri, DEFAULT_JAR_TLD), uri);
}
// Treat the URI verbatim.
return loadTaglib(new TldPath(uri), uri);
}
default: {
throw new RuntimeException("Cannot happen");
}
}
}
catch(TemplateModelException e) {
throw e;
}
catch(RuntimeException e) {
throw e;
}
catch(Exception e) {
throw new TemplateModelException("Could not load taglib information for " + uri, e);
}
}
}
private static class TldPath {
final String filePath;
final String jarItemPath;
TldPath(String filePath) {
this(filePath, null);
}
TldPath(String filePath, String jarItemPath) {
this.filePath = filePath;
this.jarItemPath = jarItemPath;
}
public String toString() {
if(jarItemPath == null) {
return filePath;
}
return filePath + "!" + jarItemPath;
}
}
private TemplateModel loadTaglib(TldPath tldPath, String uri) throws Exception {
if(logger.isDebugEnabled()) {
logger.debug("Loading taglib " + StringUtil.jQuoteNoXSS(uri) +
" from location " + StringUtil.jQuoteNoXSS(tldPath));
}
final Taglib taglib = new Taglib(ctx, tldPath, uri);
taglibs.put(uri, taglib);
locations.remove(uri);
return taglib;
}
private static int getUriType(String uri) throws TemplateModelException {
if(uri == null) {
throw new TemplateModelException("null is not a valid URI");
}
if(uri.length() == 0) {
throw new TemplateModelException("empty string is not a valid URI");
}
final char c0 = uri.charAt(0);
if(c0 == '/') {
return ROOT_REL_URI;
}
// Check if it conforms to RFC 3986 3.1 in order to qualify as ABS_URI
if(c0 < 'a' || c0 > 'z') { // First char of scheme must be alpha
return NOROOT_REL_URI;
}
final int colon = uri.indexOf(':');
if(colon == -1) { // Must have a colon
return NOROOT_REL_URI;
}
// Subsequent chars must be [a-z,0-9,+,-,.]
for(int i = 1; i < colon; ++i) {
final char c = uri.charAt(i);
if((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '+' && c != '-' && c != '.') {
return NOROOT_REL_URI;
}
}
return ABS_URI;
}
/**
* Returns false.
*/
public boolean isEmpty() {
return false;
}
private void addLocationsFromWebXml() throws Exception
{
WebXmlParser webXmlParser = new WebXmlParser();
InputStream in = ctx.getResourceAsStream("/WEB-INF/web.xml");
if (in == null) {
// No /WEB-INF/web.xml - do nothing
return;
}
try {
parseXml(in, ctx.getResource("/WEB-INF/web.xml").toExternalForm(), webXmlParser);
}
finally {
in.close();
}
}
private class WebXmlParser extends DefaultHandler {
private StringBuffer buf;
private String uri;
private String location;
private Locator locator;
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
public void startElement(
String nsuri,
String localName,
String qName,
Attributes atts) {
if ("taglib-uri".equals(qName)
|| "taglib-location".equals(qName)) {
buf = new StringBuffer();
}
}
public void characters(char[] chars, int off, int len) {
if (buf != null) {
buf.append(chars, off, len);
}
}
public void endElement(String nsuri, String localName, String qName) throws SAXParseException {
if ("taglib-uri".equals(qName)) {
uri = buf.toString().trim();
buf = null;
}
else if ("taglib-location".equals(qName)) {
location = buf.toString().trim();
try {
if(getUriType(location) == NOROOT_REL_URI) {
location = "/WEB-INF/" + location;
}
}
catch(TemplateModelException e) {
throw new SAXParseException(e.getMessage(), locator, e);
}
buf = null;
}
else if ("taglib".equals(qName)) {
final String zname;
if(location.endsWith(".jar") || location.endsWith(".zip")) {
zname = DEFAULT_JAR_TLD;
}
else {
zname = null;
}
addLocation("web.xml", location, zname, uri);
}
}
}
private void addLocationsFromWebApp() throws Exception
{
Set libs = ctx.getResourcePaths("/WEB-INF/lib");
if (libs != null) {
for (Iterator iter = libs.iterator(); iter.hasNext();) {
String path = (String) iter.next();
if(path.endsWith(".jar") || path.endsWith(".zip")) {
addLocationsFromJarFile(path);
}
else if(path.endsWith(".tld")) {
addLocationFromTldFile(path);
}
}
}
libs = ctx.getResourcePaths("/WEB-INF");
if (libs != null) {
for (Iterator iter = libs.iterator(); iter.hasNext();) {
String path = (String) iter.next();
if(path.endsWith(".tld")) {
addLocationFromTldFile(path);
}
}
}
}
private void addLocationsFromJarFile(String path) throws Exception {
ZipInputStream zin = new ZipInputStream(ctx.getResourceAsStream(path));
// Make stream uncloseable by XML parsers
InputStream uin = new FilterInputStream(zin) {
public void close() {
}
};
try {
for(;;) {
ZipEntry ze = zin.getNextEntry();
if(ze == null) {
break;
}
String zname = ze.getName();
if(zname.startsWith("META-INF/") && zname.endsWith(".tld")) {
String url = "jar:" +
ctx.getResource(path).toExternalForm() +
"!" + zname;
addLocationFromTldResource(uin, path, zname, url);
}
}
}
finally {
zin.close();
}
}
private void addLocationFromTldFile(String path) throws Exception {
InputStream in = ctx.getResourceAsStream(path);
try {
addLocationFromTldResource(in, path, null, ctx.getResource(path).toExternalForm());
}
finally {
in.close();
}
}
private void addLocationFromTldResource(InputStream uin, String path, String zname, String url)
throws Exception {
String uri = getTldUri(uin, url);
if(uri != null) {
addLocation(zname == null ? "tld file" : "jar file", path, zname, uri);
}
}
private void addLocation(String source, String filePath, String jarItemPath, String uri) {
final TldPath tldPath = new TldPath(filePath, jarItemPath);
if(locations.containsKey(uri)) {
logger.debug("Ignored duplicate URI " + StringUtil.jQuoteNoXSS(uri) +
" in " + source + " " + StringUtil.jQuoteNoXSS(tldPath));
} else {
locations.put(uri, tldPath);
if(logger.isDebugEnabled()) {
logger.debug(source + " assigned URI " + StringUtil.jQuoteNoXSS(uri) +
" to location " + StringUtil.jQuoteNoXSS(tldPath));
}
}
}
private String getTldUri(InputStream in, String url) throws Exception
{
TldUriReader tur = new TldUriReader();
parseXml(in, url, tur);
return tur.getUri();
}
private static class TldUriReader extends DefaultHandler {
private StringBuffer buf;
private String uri;
TldUriReader() {
}
String getUri() {
return uri;
}
public void startElement(
String nsuri,
String localName,
String qName,
Attributes atts) {
if ("uri".equals(qName)) {
buf = new StringBuffer();
}
}
public void characters(char[] chars, int off, int len) {
if (buf != null) {
buf.append(chars, off, len);
}
}
public void endElement(String nsuri, String localName, String qName) {
if ("uri".equals(qName)) {
uri = buf.toString().trim();
buf = null;
}
}
}
private static void parseXml(InputStream in, String url, DefaultHandler handler)
throws Exception
{
InputSource is = new InputSource();
is.setByteStream(in);
is.setSystemId(url);
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(false);
factory.setValidating(false);
XMLReader reader = factory.newSAXParser().getXMLReader();
reader.setEntityResolver(new LocalTaglibDtds());
reader.setContentHandler(handler);
reader.setErrorHandler(handler);
reader.parse(is);
}
private static final class Taglib implements TemplateHashModel {
private final Map tags;
Taglib(ServletContext ctx, TldPath tldPath, String uri) throws Exception {
tags = loadTaglib(ctx, tldPath, uri);
}
public TemplateModel get(String key) {
return (TemplateModel)tags.get(key);
}
public boolean isEmpty() {
return tags.isEmpty();
}
private static final Map loadTaglib(ServletContext ctx, TldPath tldPath, String uri)
throws Exception
{
final TldParser tldParser = new TldParser();
final String filePath = tldPath.filePath;
final InputStream in = ctx.getResourceAsStream(filePath);
if(in == null) {
throw new TemplateModelException("Could not find webapp resource " +
filePath + " for URI " + uri);
}
final String fileUrl = ctx.getResource(filePath).toExternalForm();
try {
final String jarItemPath = tldPath.jarItemPath;
if(jarItemPath != null) {
final ZipInputStream zin = new ZipInputStream(in);
for(;;) {
final ZipEntry ze = zin.getNextEntry();
if(ze == null) {
throw new TemplateModelException(
"Could not find JAR entry " + jarItemPath +
" inside webapp resource " + filePath +
" for URI " + uri);
}
final String zname = ze.getName();
if(zname.equals(jarItemPath)) {
parseXml(zin, "jar:" + fileUrl + "!" + zname, tldParser);
break;
}
}
}
else {
parseXml(in, fileUrl, tldParser);
}
}
finally {
in.close();
}
EventForwarding eventForwarding = EventForwarding.getInstance(ctx);
if(eventForwarding != null) {
eventForwarding.addListeners(tldParser.getListeners());
}
else if(tldParser.getListeners().size() > 0) {
throw new TemplateModelException(
"Event listeners specified in the TLD could not be " +
" registered since the web application doesn't have a" +
" listener of class " + EventForwarding.class.getName() +
". To remedy this, add this element to web.xml:\n" +
"| <listener>\n" +
"| <listener-class>" + EventForwarding.class.getName() + "</listener-class>\n" +
"| </listener>");
}
return tldParser.getTags();
}
}
private static String resolveRelativeUri(String uri)
throws
TemplateModelException
{
TemplateModel reqHash =
Environment.getCurrentEnvironment().getVariable(
FreemarkerServlet.KEY_REQUEST_PRIVATE);
if(reqHash instanceof HttpRequestHashModel) {
HttpServletRequest req =
((HttpRequestHashModel)reqHash).getRequest();
String pi = req.getPathInfo();
String reqPath = req.getServletPath();
if(reqPath == null) {
reqPath = "";
}
reqPath += (pi == null ? "" : pi);
// We don't care about paths with ".." in them. If the container
// wishes to resolve them on its own, let it be.
int lastSlash = reqPath.lastIndexOf('/');
if(lastSlash != -1) {
return reqPath.substring(0, lastSlash + 1) + uri;
}
else {
return '/' + uri;
}
}
throw new TemplateModelException(
"Can't resolve relative URI " + uri +
" as request URL information is unavailable.");
}
private static final class TldParser extends DefaultHandler {
private final Map tags = new HashMap();
private final List listeners = new ArrayList();
private Locator locator;
private StringBuffer buf;
private String tagName;
private String tagClassName;
Map getTags() {
return tags;
}
List getListeners() {
return listeners;
}
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
public void startElement(
String nsuri,
String localName,
String qName,
Attributes atts) {
if ("name".equals(qName) || "tagclass".equals(qName) || "tag-class".equals(qName) || "listener-class".equals(qName)) {
buf = new StringBuffer();
}
}
public void characters(char[] chars, int off, int len) {
if (buf != null) {
buf.append(chars, off, len);
}
}
public void endElement(String nsuri, String localName, String qName)
throws SAXParseException {
if ("name".equals(qName)) {
if(tagName == null) {
tagName = buf.toString().trim();
}
buf = null;
}
else if ("tagclass".equals(qName) || "tag-class".equals(qName)) {
tagClassName = buf.toString().trim();
buf = null;
}
else if ("tag".equals(qName)) {
try {
Class tagClass = ClassUtil.forName(tagClassName);
TemplateModel impl;
if(Tag.class.isAssignableFrom(tagClass)) {
impl = new TagTransformModel(tagClass);
}
else {
impl = new SimpleTagDirectiveModel(tagClass);
}
tags.put(tagName, impl);
tagName = null;
tagClassName = null;
}
catch (IntrospectionException e) {
throw new SAXParseException(
"Can't introspect tag class " + tagClassName,
locator,
e);
}
catch (ClassNotFoundException e) {
throw new SAXParseException(
"Can't find tag class " + tagClassName,
locator,
e);
}
}
else if ("listener-class".equals(qName)) {
String listenerClass = buf.toString().trim();
buf = null;
try {
listeners.add(ClassUtil.forName(listenerClass).newInstance());
}
catch(Exception e) {
throw new SAXParseException(
"Can't instantiate listener class " + listenerClass,
locator,
e);
}
}
}
}
private static final Map dtds = new HashMap();
static
{
// JSP taglib 2.1
dtds.put("http://java.sun.com/xml/ns/jee/web-jsptaglibrary_2_1.xsd", "web-jsptaglibrary_2_1.xsd");
// JSP taglib 2.0
dtds.put("http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd", "web-jsptaglibrary_2_0.xsd");
// JSP taglib 1.2
dtds.put("-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN", "web-jsptaglibrary_1_2.dtd");
dtds.put("http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd", "web-jsptaglibrary_1_2.dtd");
// JSP taglib 1.1
dtds.put("-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN", "web-jsptaglibrary_1_1.dtd");
dtds.put("http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd", "web-jsptaglibrary_1_1.dtd");
// Servlet 2.5
dtds.put("http://java.sun.com/xml/ns/jee/web-app_2_5.xsd", "web-app_2_5.xsd");
// Servlet 2.4
dtds.put("http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd", "web-app_2_4.xsd");
// Servlet 2.3
dtds.put("-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN", "web-app_2_3.dtd");
dtds.put("http://java.sun.com/dtd/web-app_2_3.dtd", "web-app_2_3.dtd");
// Servlet 2.2
dtds.put("-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN", "web-app_2_2.dtd");
dtds.put("http://java.sun.com/j2ee/dtds/web-app_2_2.dtd", "web-app_2_2.dtd");
}
private static final class LocalTaglibDtds implements EntityResolver {
public InputSource resolveEntity(String publicId, String systemId)
{
String resourceName = (String)dtds.get(publicId);
if(resourceName == null)
{
resourceName = (String)dtds.get(systemId);
}
InputStream resourceStream;
if(resourceName != null)
{
resourceStream = getClass().getResourceAsStream(resourceName);
}
else
{
// Fake an empty stream for unknown DTDs
resourceStream = new ByteArrayInputStream(new byte[0]);
}
InputSource is = new InputSource();
is.setPublicId(publicId);
is.setSystemId(systemId);
is.setByteStream(resourceStream);
return is;
}
}
}