From e1f2832fbdba5a3d8b939bf4bd94d1a9f339314c Mon Sep 17 00:00:00 2001 From: j2blake Date: Mon, 11 Mar 2013 13:29:20 -0400 Subject: [PATCH] VIVO-25 Check that web.xml is container-neutral Create an Ant target that checks web.xml against the assembled webapp, looking for conditions that violate the Servlet spec or the JSP spec, but that Tomcat does not complain about. This will not be a main-stream target, but must be specifically invoked by developers or by Jenkins in order to be effective. --- .../CheckContainerNeutrality.java | 401 ++++++++++++++++++ webapp/build.xml | 31 +- .../mannlib/vitro/webapp/WebXmlTest.java | 308 -------------- 3 files changed, 426 insertions(+), 314 deletions(-) create mode 100644 utilities/buildutils/src/edu/cornell/mannlib/vitro/utilities/containerneutral/CheckContainerNeutrality.java delete mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/WebXmlTest.java diff --git a/utilities/buildutils/src/edu/cornell/mannlib/vitro/utilities/containerneutral/CheckContainerNeutrality.java b/utilities/buildutils/src/edu/cornell/mannlib/vitro/utilities/containerneutral/CheckContainerNeutrality.java new file mode 100644 index 000000000..9475d792e --- /dev/null +++ b/utilities/buildutils/src/edu/cornell/mannlib/vitro/utilities/containerneutral/CheckContainerNeutrality.java @@ -0,0 +1,401 @@ +/* $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.Iterator; +import java.util.List; + +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 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 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 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 that conflicts with the embedded + * in the taglib itself. As far as I can see in the spec, the embedded + * 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 specified in web.xml. If the taglib has an embedded + * tag, it should match the 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 or entries are + * unsuitable. I check them anyway, since the mechanism was already assembled + * for checking entries. + * + * Check each to insure that the class can be loaded and + * instantiated and assigned to ServletContextListener. + * + * Check each to insure that the class can be loaded and + * instantiated and assigned to Filter. + * + * --------------------------------------------------------------------- + * + * 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 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(); + } + + // ---------------------------------------------------------------------- + // Tests + // ---------------------------------------------------------------------- + + @Test + public void checkAll() throws IOException { + checkDispatcherValues(); + checkServletClasses(); + checkListenerClasses(); + checkFilterClasses(); + checkTaglibLocations(); + + if (!messages.isEmpty()) { + fail("Found these problems with '" + webXmlFile.getCanonicalPath() + + "'\n " + StringUtils.join(messages, "\n ")); + } + } + + private void checkDispatcherValues() { + List 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("" + text + + " 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("" + text + + " 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("" + text + + " 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("" + text + + " 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 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 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() + + " 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 findNodes(String pattern) { + return findNodes(pattern, webXmlDoc.getDocumentElement()); + } + + /** + * Search for an Xpath within a node of web.xml, returning a handy list. + */ + private List findNodes(String pattern, Node context) { + try { + XPathExpression xpe = xpath.compile(pattern); + NodeList nodes = (NodeList) xpe.evaluate(context, + XPathConstants.NODESET); + List list = new ArrayList(); + 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 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(); + } + } + +} diff --git a/webapp/build.xml b/webapp/build.xml index b522e1531..e9cda0354 100644 --- a/webapp/build.xml +++ b/webapp/build.xml @@ -84,8 +84,13 @@ + + + + + - + - + @@ -160,7 +165,7 @@ - + @@ -210,7 +215,7 @@ - + - + @@ -422,6 +427,21 @@ deploy - Deploy the application directly into the Tomcat webapps directory. + + + + + + + + + + + + + ", 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(); - } - } - -}