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(); - } - } - -}