Open Source Repository

Home /freemarker/freemarker-2.3.16 | Repository Home



freemarker/ext/jsp/TaglibFactory.java
/*
 * 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 [email protected].
 *
 * 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.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
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.ParserConfigurationException;
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.SAXException;
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;



/**
 * 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.
 @version $Id: TaglibFactory.java,v 1.26.2.1 2007/05/16 12:13:04 szegedia Exp $
 @author Attila Szegedi
 */
public class TaglibFactory implements TemplateHashModel {
    private static final Logger logger = Logger.getLogger("freemarker.jsp");

    // No TLDs have been looked up yet
    private static final int LOOKUP_NONE = 0;
    // Only explicit TLDs in web.xml have been looked up
    private static final int LOOKUP_WEB_XML = 1;
    // Both explicit TLDs and those in JARs have been looked up
    private static final int LOOKUP_JARS = 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 is 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 urithrows TemplateModelException {
        synchronized (taglibs) {
            Taglib taglib = null;
            taglib = (Taglibtaglibs.get(uri);
            if(taglib != null) {
                return taglib;
            }
            
            taglib = new Taglib();
            try {
                do {
                    if(taglib.load(uri, ctx, locations)) {
                        taglibs.put(uri, taglib);
                        return taglib;
                    }
                }
                while(getMoreTaglibLocations());

                // Not found -- in case of NOROOT_REL_URI, let's resolve it and
                // try again.
                String resolvedUri = resolveRelativeUri(uri);
                if(resolvedUri != uri) {
                    taglib = (Taglibtaglibs.get(resolvedUri);
                    if(taglib != null) {
                        return taglib;
                    }
                    taglib = new Taglib();
                    if(taglib.load(resolvedUri, ctx, locations)) {
                        taglibs.put(resolvedUri, taglib);
                        return taglib;
                    }
                }
            }
            catch(TemplateModelException e) {
                throw e;
            }
            catch(Exception e) {
                throw new TemplateModelException("Could not load taglib information", e);
            }
            return null;
        }
    }

    /**
     * Returns false.
     */
    public boolean isEmpty() {
        return false;
    }

    private boolean getMoreTaglibLocations() throws MalformedURLException, ParserConfigurationException, IOException, SAXException
    {
        switch(lookupPhase) {
            case LOOKUP_NONE: {
                getLocationsFromWebXml();
                lookupPhase = LOOKUP_WEB_XML;
                return true;
            }
            case LOOKUP_WEB_XML: {
                getLocationsFromLibJars();
                lookupPhase = LOOKUP_JARS;
                return true;
            }
            default {
                return false;
            }
        }
    }
    
    private void getLocationsFromWebXml() throws MalformedURLException, ParserConfigurationException, IOException, SAXException
    {
        WebXmlParser webXmlParser = new WebXmlParser(locations);
        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 static class WebXmlParser extends DefaultHandler {
        private final Map locations;
        
        private StringBuffer buf;
        private String uri;
        private String location;

        WebXmlParser(Map locations) {
            this.locations = locations;
        }

        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) {
            if ("taglib-uri".equals(qName)) {
                uri = buf.toString().trim();
                buf = null;
            }
            else if ("taglib-location".equals(qName)) {
                location = buf.toString().trim();
                if(location.indexOf("://"== -&& !location.startsWith("/")) {
                    location = "/WEB-INF/" + location;
                }
                buf = null;
            }
            else if ("taglib".equals(qName)) {
                String[] loc = new String[2];
                loc[0= location;
                if(location.endsWith(".jar"|| location.endsWith(".zip")) {
                    loc[1"META-INF/taglib.tld";
                }
                locations.put(uri, loc);
                if(logger.isDebugEnabled()) {
                    logger.debug("web.xml assigned URI " + uri + " to location " + loc[0(loc[1!= null "!" + loc[1""));
                }
            }
        }
    }


    private void getLocationsFromLibJars() throws ParserConfigurationException, IOException, SAXException
    {
        Set libs = ctx.getResourcePaths("/WEB-INF/lib");
        for (Iterator iter = libs.iterator(); iter.hasNext();) {
            String path = (Stringiter.next();
            if(path.endsWith(".jar"|| path.endsWith(".zip")) {
                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;
                            String loc = getTldUri(uin, url);
                            if(loc != null) {
                                locations.put(loc, new String[] { path, zname });
                                if(logger.isDebugEnabled()) {
                                    logger.debug("libjar assigned URI " + loc + " to location " + path + "!" + zname);
                                }
                            
                        }
                    }
                }
                finally {
                    zin.close();
                }
            }
        }
    }

    private String getTldUri(InputStream in, String urlthrows ParserConfigurationException, IOException, SAXException 
    {
        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 
        ParserConfigurationException, IOException, SAXException
    {
        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 Map tags;

        Taglib() {
        }

        public TemplateModel get(String key) {
            return (TemplateModel)tags.get(key);
        }

        public boolean isEmpty() {
            return false;
        }
        
        boolean load(String uri, ServletContext ctx, Map locations)
        throws 
            ParserConfigurationException, 
            IOException, 
            SAXException,
            TemplateModelException
        {
            String[] tldPath = getTldPath(uri, locations);
            if(tldPath != null) {
                if(logger.isDebugEnabled()) {
                    logger.debug("Loading taglib " + uri + " from location " 
                        tldPath[0(tldPath[1!= null "!" + tldPath[1""));
                }
                tags = loadTaglib(tldPath, ctx);
                if(tags != null) {
                    locations.remove(uri);
                    return true;
                }
            }
            return false;
        }
    }

    private static final Map loadTaglib(String[] tldPath, ServletContext ctx)
    throws
        ParserConfigurationException, IOException, SAXException, TemplateModelException
    {
        String filePath = tldPath[0]
        TldParser tldParser = new TldParser();
        InputStream in = ctx.getResourceAsStream(filePath);
        if(in == null) {
            throw new TemplateModelException("Could not find webapp resource " + filePath);
        }
        String url = ctx.getResource(filePath).toExternalForm();
        try {
            String jarPath = tldPath[1];
            if(jarPath != null) {
                ZipInputStream zin = new ZipInputStream(in);
                for(;;) {
                    ZipEntry ze = zin.getNextEntry();
                    if(ze == null) {
                        throw new TemplateModelException("Could not find JAR entry " + jarPath + " inside webapp resource " + filePath);
                    }
                    String zname = ze.getName()
                    if(zname.equals(jarPath)) {
                        parseXml(zin, "jar:" + url + "!" + zname, tldParser);
                        break;
                    }
                }
            }
            else {
                parseXml(in, url, 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 final String[] getTldPath(String uri, Map locations)
    {
        String[] path = (String[])locations.get(uri);
        // If location was explicitly defined in web.xml, or discovered in a
        // JAR file, use it. (Hopefully this is 99% of the cases)
        if(path != null) {
            return path;
        }
        
        // If there was no explicit mapping in web.xml, but URI is a 
        // ROOT_REL_URI, return it (JSP.7.6.3.2)
        if(uri.startsWith("/")) {
            path = new String[2];
            path[0= uri;
            if(uri.endsWith(".jar"|| uri.endsWith(".zip")) {
                path[1"META-INF/taglib.tld";
            }
            return path;
        }

        // Unmapped NOROOT_REL_URI - do nothing with it, so eventually get() 
        // will resolve it and try again
        return null;
    }

    private static String resolveRelativeUri(String uri)
    throws
        TemplateModelException
    {
        // Absolute and root-relative URIs are left as they are.
        if(uri.startsWith("/"|| uri.indexOf("://"!= -1) {
            return uri;
        }
        
        // Otherwise it is a NOROOT_REL_URI, and has to be resolved relative
        // to current page... We have to obtain the request object to know what
        // is the URL of the current page (this assumes there's a 
        // HttpRequestHashModel under name FreemarkerServlet.KEY_REQUEST in the
        // environment...) (JSP.7.6.3.2)
        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;
        }
    }
}