edu.cornell.mannlib.vitro.utilities.containerneutral.CheckContainerNeutrality.java Source code

Java tutorial

Introduction

Here is the source code for edu.cornell.mannlib.vitro.utilities.containerneutral.CheckContainerNeutrality.java

Source

/* $This file is distributed under the terms of the license in /doc/license.txt$ */

package edu.cornell.mannlib.vitro.utilities.containerneutral;

import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpServlet;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang.StringUtils;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Look at web.xml, and check for conditions that violate the Servlet 2.4 spec,
 * but that might not be noticed because Tomcat doesn't complain.
 * 
 * ------
 * 
 * Values of the <dispatcher/> tag:
 * 
 * The spec permits only these values: "FORWARD", "REQUEST", "INCLUDE", "ERROR",
 * but Tomcat also allows the lower-case equivalents. GlassFish or WebLogic will
 * barf on lower-case.
 * 
 * Check to see that only the upper-case values are used.
 * 
 * ------
 * 
 * Existence of Servlet classes:
 * 
 * The spec allows the container to either load all servlets at startup, or to
 * load them when requested. Since Tomcat only loads servlet when requested, it
 * doesn't notice or complain if web.xml cites a <servlet-class/> that doesn't
 * exist, as long as it is never invoked. On the other hand, WebLogic loads all
 * serlvets at startup, and will barf if the class is not found.
 * 
 * Check each <servlet-class/> to insure that the class can be loaded and
 * instantiated and assigned to HttpServlet.
 * 
 * ------
 * 
 * Embedded URIs in taglibs.
 * 
 * I can't find this definitively in the JSP spec, but some containers complain
 * if web.xml specifies a <taglib-uri/> that conflicts with the <uri/> embedded
 * in the taglib itself. As far as I can see in the spec, the embedded <uri/>
 * tag is not required or referenced unless we are using
 * "Implicit Map Entries From TLDs", which in turn is only relevant for TLDs
 * packaged in JAR files. So, I can't find support for this complaint, but it
 * seems a reasonable one.
 * 
 * Check each <taglib/> specified in web.xml. If the taglib has an embedded
 * <uri/> tag, it should match the <taglib-uri/> from web.xml.
 * 
 * ------
 * 
 * Existence of Listener and Filter classes.
 * 
 * As far as I can tell, there is no ambiguity here, and every container will
 * complain if any of the <listener-class/> or <filter-class/> entries are
 * unsuitable. I check them anyway, since the mechanism was already assembled
 * for checking <servlet-class/> entries.
 * 
 * Check each <listener-class/> to insure that the class can be loaded and
 * instantiated and assigned to ServletContextListener.
 * 
 * Check each <filter-class/> to insure that the class can be loaded and
 * instantiated and assigned to Filter.
 * 
 * ------
 * 
 * A <servlet/> tag for every <servlet-mapping/> tag
 * 
 * I can't find a mention of this in the spec, but Tomcat complains and refuses
 * to load the app if there is a <servlet-mapping/> tag whose <servlet-name/> is
 * not matched by a <servlet-name/> in a <servlet/> tag.
 * 
 * Get sets of all <servlet-name/> tags that are specified in <servlet/> and
 * <servlet-mapping/> tags. There should not be any names in the
 * servlet-mappings that are not in the servlets.
 * 
 * ---------------------------------------------------------------------
 * 
 * Although this class is executed as a JUnit test, it doesn't have the usual
 * structure for a unit test.
 * 
 * In order to produce the most diagnostic information, the test does not abort
 * on the first failure. Rather, failure messages are accumulated until all
 * checks have been performed, and the test list all such messages on failure.
 * 
 * ---------------------------------------------------------------------
 * 
 * Since this is not executed as part of the standard Vitro unit tests, it also
 * cannot use the standard logging mechanism. Log4J has not been initialized.
 * 
 */
public class CheckContainerNeutrality {
    private static final String PROPERTY_WEBAPP_DIR = "CheckContainerNeutrality.webapp.dir";

    private static DocumentBuilder docBuilder;
    private static XPath xpath;

    @BeforeClass
    public static void createDocBuilder() {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true); // never forget this!
            docBuilder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    @BeforeClass
    public static void createXPath() {
        xpath = XPathFactory.newInstance().newXPath();
        xpath.setNamespaceContext(new StupidNamespaceContext());
    }

    private File webappDir;
    private File webXmlFile;
    private Document webXmlDoc;
    private List<String> messages;

    @Before
    public void setup() throws SAXException, IOException {
        String webappDirPath = System.getProperty(PROPERTY_WEBAPP_DIR);
        if (webappDirPath == null) {
            fail("System property '" + PROPERTY_WEBAPP_DIR + "' was not provided.");
        }
        webappDir = new File(webappDirPath);
        if (!webappDir.isDirectory()) {
            fail("'" + webappDirPath + "' is not a directory");
        }
        webXmlFile = new File(webappDir, "WEB-INF/web.xml");
        if (!webXmlFile.isFile()) {
            fail("Can't find '" + webXmlFile.getAbsolutePath() + "'");
        }

        webXmlDoc = docBuilder.parse(webXmlFile);

        messages = new ArrayList<String>();
    }

    // ----------------------------------------------------------------------
    // Tests
    // ----------------------------------------------------------------------

    @Test
    public void checkAll() throws IOException {
        checkDispatcherValues();
        checkServletClasses();
        checkListenerClasses();
        checkFilterClasses();
        checkTaglibLocations();
        checkServletNames();

        if (!messages.isEmpty()) {
            fail("Found these problems with '" + webXmlFile.getCanonicalPath() + "'\n   "
                    + StringUtils.join(messages, "\n   "));
        }
    }

    private void checkDispatcherValues() {
        List<String> okValues = Arrays.asList(new String[] { "FORWARD", "REQUEST", "INCLUDE", "ERROR" });
        for (Node n : findNodes("//j2ee:dispatcher")) {
            String text = n.getTextContent();
            if (!okValues.contains(text)) {
                messages.add(
                        "<dispatcher>" + text + "</dispatcher> is not valid. Acceptable values are " + okValues);
            }
        }
    }

    private void checkServletClasses() {
        for (Node n : findNodes("//j2ee:servlet-class")) {
            String text = n.getTextContent();
            String problem = confirmClassNameIsValid(text, HttpServlet.class);
            if (problem != null) {
                messages.add("<servlet-class>" + text + "</servlet-class> is not valid: " + problem);
            }
        }
    }

    private void checkListenerClasses() {
        for (Node n : findNodes("//j2ee:listener-class")) {
            String text = n.getTextContent();
            String problem = confirmClassNameIsValid(text, ServletContextListener.class);
            if (problem != null) {
                messages.add("<listener-class>" + text + "</listener-class> is not valid: " + problem);
            }
        }
    }

    private void checkFilterClasses() {
        for (Node n : findNodes("//j2ee:filter-class")) {
            String text = n.getTextContent();
            String problem = confirmClassNameIsValid(text, Filter.class);
            if (problem != null) {
                messages.add("<filter-class>" + text + "</filter-class> is not valid: " + problem);
            }
        }
    }

    private void checkTaglibLocations() {
        for (Node n : findNodes("//j2ee:jsp-config/j2ee:taglib")) {
            String taglibUri = findNode("j2ee:taglib-uri", n).getTextContent();
            String taglibLocation = findNode("j2ee:taglib-location", n).getTextContent();
            // System.out.println("taglibUri='" + taglibUri
            // + "', taglibLocation='" + taglibLocation + "'");
            String message = checkTaglibUri(taglibUri, taglibLocation);
            if (message != null) {
                messages.add(message);
            }
        }
    }

    private void checkServletNames() {
        Set<String> servletNames = new HashSet<String>();
        for (Node n : findNodes("//j2ee:servlet/j2ee:servlet-name")) {
            servletNames.add(n.getTextContent());
        }

        Set<String> servletMappingNames = new HashSet<String>();
        for (Node n : findNodes("//j2ee:servlet-mapping/j2ee:servlet-name")) {
            servletMappingNames.add(n.getTextContent());
        }

        servletMappingNames.removeAll(servletNames);
        for (String name : servletMappingNames) {
            messages.add("There is a <servlet-mapping> tag for <servlet-name>" + name
                    + "</servlet-name>, but there is " + "no matching <servlet> tag.");
        }
    }

    private String checkTaglibUri(String taglibUri, String taglibLocation) {
        File taglibFile = new File(webappDir, taglibLocation);
        if (!taglibFile.isFile()) {
            return "File '" + taglibLocation + "' can't be found ('" + taglibFile.getAbsolutePath() + "')";
        }

        Document taglibDoc;
        try {
            taglibDoc = docBuilder.parse(taglibFile);
        } catch (SAXException e) {
            return "Failed to parse the taglib file '" + taglibFile + "': " + e;
        } catch (IOException e) {
            return "Failed to parse the taglib file '" + taglibFile + "': " + e;
        }

        List<Node> uriNodes = findNodes("/j2ee:taglib/j2ee:uri", taglibDoc.getDocumentElement());
        // System.out.println("uriNodes: " + uriNodes);
        if (uriNodes.isEmpty()) {
            return null;
        }
        if (uriNodes.size() > 1) {
            return "taglib '" + taglibLocation + "' contains " + uriNodes.size()
                    + " <uri> nodes. Expecting no more than 1";
        }

        String embeddedUri = uriNodes.get(0).getTextContent();
        if (taglibUri.equals(embeddedUri)) {
            return null;
        } else {
            return "URI in taglib doesn't match the one in web.xml: taglib='" + taglibLocation + "', internal URI='"
                    + uriNodes.get(0).getTextContent() + "', URI from web.xml='" + taglibUri + "'";
        }
    }

    // ----------------------------------------------------------------------
    // Helper methods
    // ----------------------------------------------------------------------

    /**
     * Search for an Xpath in web.xml, returning a handy list.
     */
    private List<Node> findNodes(String pattern) {
        return findNodes(pattern, webXmlDoc.getDocumentElement());
    }

    /**
     * Search for an Xpath within a node of web.xml, returning a handy list.
     */
    private List<Node> findNodes(String pattern, Node context) {
        try {
            XPathExpression xpe = xpath.compile(pattern);
            NodeList nodes = (NodeList) xpe.evaluate(context, XPathConstants.NODESET);
            List<Node> list = new ArrayList<Node>();
            for (int i = 0; i < nodes.getLength(); i++) {
                list.add(nodes.item(i));
            }
            return list;
        } catch (XPathExpressionException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Search for an Xpath within a node of web.xml, returning a single node or
     * throwing an exception.
     */
    private Node findNode(String pattern, Node context) {
        List<Node> list = findNodes(pattern, context);
        if (list.size() != 1) {
            throw new RuntimeException(
                    "Expecting 1 node, but found " + list.size() + " nodes using '" + pattern + "'");
        } else {
            return list.get(0);
        }
    }

    /**
     * Check that the supplied className can be instantiated with a
     * zero-argument constructor, and assigned to a variable of the target
     * class.
     */
    private String confirmClassNameIsValid(String className, Class<?> targetClass) {
        try {
            Class<?> specifiedClass = Class.forName(className);
            Object o = specifiedClass.newInstance();
            if (!targetClass.isInstance(o)) {
                return specifiedClass.getSimpleName() + " is not a subclass of " + targetClass.getSimpleName()
                        + ".";
            }
        } catch (ClassNotFoundException e) {
            return "The class does not exist.";
        } catch (InstantiationException e) {
            return "The class does not have a public constructor " + "that takes zero arguments.";
        } catch (IllegalAccessException e) {
            return "The class does not have a public constructor " + "that takes zero arguments.";
        }
        return null;
    }

    /**
     * Dump the first 20 nodes of an XML context, excluding comments and blank
     * text nodes.
     */
    @SuppressWarnings("unused")
    private int dumpXml(Node xmlNode, int... parms) {
        int remaining = (parms.length == 0) ? 20 : parms[0];
        int level = (parms.length < 2) ? 1 : parms[1];

        Node n = xmlNode;

        if (Node.COMMENT_NODE == n.getNodeType()) {
            return 0;
        }
        if (Node.TEXT_NODE == n.getNodeType()) {
            if (StringUtils.isBlank(n.getTextContent())) {
                return 0;
            }
        }

        int used = 1;

        System.out.println(StringUtils.repeat("-->", level) + n);
        NodeList nl = n.getChildNodes();
        for (int i = 0; (i < nl.getLength() && remaining > used); i++) {
            used += dumpXml(nl.item(i), remaining - used, level + 1);
        }
        return used;
    }

    // ----------------------------------------------------------------------
    // Helper classes
    // ----------------------------------------------------------------------

    private static class StupidNamespaceContext implements NamespaceContext {
        @Override
        public String getNamespaceURI(String prefix) {
            if ("j2ee".equals(prefix)) {
                return "http://java.sun.com/xml/ns/j2ee";
            } else {
                throw new UnsupportedOperationException();
            }
        }

        @Override
        public String getPrefix(String namespaceURI) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Iterator<?> getPrefixes(String namespaceURI) {
            throw new UnsupportedOperationException();
        }
    }

}