| /* |
| * Copyright 2009 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.dev.util.collect.HashSet; |
| |
| import org.xml.sax.Attributes; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| import java.io.File; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.Stack; |
| |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| /** |
| * Validates that <code><servlet></code> tags in a GWT module match ones |
| * from a <code>WEB-INF/web.xml</code>. |
| */ |
| class ServletValidator { |
| |
| /** |
| * Collect servlet information from a web xml. |
| */ |
| private static final class WebXmlDataCollector extends DefaultHandler { |
| /* |
| * TODO(scottb): this should have been implemented as a Schema. |
| */ |
| |
| private static class ElementStack { |
| private final Stack<String> stack = new Stack<String>(); |
| |
| public boolean exactly(String elementName, int depth) { |
| return (depth == stack.size() - 1) |
| && elementName.equals(stack.get(depth)); |
| } |
| |
| public String peek() { |
| return stack.peek(); |
| } |
| |
| public String pop() { |
| return stack.pop(); |
| } |
| |
| public void push(String elementName) { |
| stack.push(elementName); |
| } |
| |
| public boolean within(String elementName, int depth) { |
| return (depth < stack.size()) && elementName.equals(stack.get(depth)); |
| } |
| } |
| |
| private Set<String> accumulateClasses = new HashSet<String>(); |
| private Set<String> accumulatePaths = new HashSet<String>(); |
| private final TreeLogger branch; |
| private final Stack<StringBuilder> cdataStack = new Stack<StringBuilder>(); |
| private final Map<String, String> classNameToServletName; |
| private final ElementStack context = new ElementStack(); |
| private String currentServletName; |
| private String indent = ""; |
| private final Map<String, Set<String>> servletNameToPaths; |
| |
| private WebXmlDataCollector(TreeLogger branch, |
| Map<String, String> classNameToServletName, |
| Map<String, Set<String>> servletNameToPaths) { |
| this.branch = branch; |
| this.classNameToServletName = classNameToServletName; |
| this.servletNameToPaths = servletNameToPaths; |
| } |
| |
| @Override |
| public void characters(char[] ch, int start, int length) |
| throws SAXException { |
| cdataStack.peek().append(ch, start, length); |
| } |
| |
| @Override |
| public void endElement(String uri, String localName, String qName) |
| throws SAXException { |
| String cdata = cdataStack.pop().toString().trim(); |
| if (context.within("web-app", 0)) { |
| if (context.within("servlet", 1)) { |
| if (context.exactly("servlet-name", 2)) { |
| currentServletName = cdata; |
| } else if (context.exactly("servlet-class", 2)) { |
| accumulateClasses.add(cdata); |
| } else if (context.exactly("servlet", 1)) { |
| if (currentServletName != null) { |
| for (String className : accumulateClasses) { |
| classNameToServletName.put(className, currentServletName); |
| } |
| currentServletName = null; |
| } |
| accumulateClasses.clear(); |
| } |
| } else if (context.within("servlet-mapping", 1)) { |
| if (context.exactly("servlet-name", 2)) { |
| currentServletName = cdata; |
| } else if (context.exactly("url-pattern", 2)) { |
| accumulatePaths.add(cdata); |
| } else if (context.exactly("servlet-mapping", 1)) { |
| if (currentServletName != null) { |
| Set<String> servletPaths = servletNameToPaths.get(currentServletName); |
| if (servletPaths == null) { |
| servletPaths = new HashSet<String>(); |
| servletNameToPaths.put(currentServletName, servletPaths); |
| } |
| servletPaths.addAll(accumulatePaths); |
| } |
| currentServletName = null; |
| accumulatePaths.clear(); |
| } |
| } |
| } |
| |
| assert qName.equals(context.peek()); |
| context.pop(); |
| |
| if (cdata.length() > 0) { |
| if (branch.isLoggable(TreeLogger.DEBUG)) { |
| branch.log(TreeLogger.DEBUG, " characters: " + indent + cdata); |
| } |
| } |
| indent = indent.substring(2); |
| if (branch.isLoggable(TreeLogger.DEBUG)) { |
| branch.log(TreeLogger.DEBUG, " endElement: " + indent + qName); |
| } |
| } |
| |
| @Override |
| public void startElement(String uri, String localName, String qName, |
| Attributes attributes) throws SAXException { |
| context.push(qName); |
| cdataStack.push(new StringBuilder()); |
| |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < attributes.getLength(); ++i) { |
| sb.append(' '); |
| sb.append(attributes.getQName(i)); |
| sb.append("=\""); |
| sb.append(attributes.getValue(i)); |
| sb.append('"'); |
| } |
| if (branch.isLoggable(TreeLogger.DEBUG)) { |
| branch.log(TreeLogger.DEBUG, "startElement: " + indent + qName |
| + sb.toString()); |
| } |
| indent += " "; |
| } |
| } |
| |
| public static ServletValidator create(TreeLogger logger, File webXml) { |
| try { |
| return create(logger, webXml.toURI().toURL()); |
| } catch (MalformedURLException e) { |
| logger.log(TreeLogger.WARN, "Unable to process '" |
| + webXml.getAbsolutePath() + "' for servlet validation", e); |
| return null; |
| } |
| } |
| |
| public static ServletValidator create(TreeLogger logger, URL webXmlUrl) { |
| String webXmlUrlString = webXmlUrl.toExternalForm(); |
| try { |
| final TreeLogger branch = logger.branch(TreeLogger.DEBUG, "Parsing " |
| + webXmlUrlString); |
| SAXParserFactory fac = SAXParserFactory.newInstance(); |
| fac.setValidating(false); |
| fac.setNamespaceAware(false); |
| fac.setFeature( |
| "http://apache.org/xml/features/nonvalidating/load-external-dtd", |
| false); |
| SAXParser parser = fac.newSAXParser(); |
| parser.getXMLReader().setFeature( |
| "http://xml.org/sax/features/validation", false); |
| parser.getXMLReader().setFeature( |
| "http://xml.org/sax/features/namespaces", false); |
| parser.getXMLReader().setFeature( |
| "http://xml.org/sax/features/namespace-prefixes", false); |
| |
| Map<String, String> classNameToServletName = new HashMap<String, String>(); |
| Map<String, Set<String>> servletNameToPaths = new HashMap<String, Set<String>>(); |
| parser.parse(webXmlUrlString, new WebXmlDataCollector(branch, |
| classNameToServletName, servletNameToPaths)); |
| |
| Map<String, Set<String>> classNameToPaths = new HashMap<String, Set<String>>(); |
| for (Entry<String, String> entry : classNameToServletName.entrySet()) { |
| classNameToPaths.put(entry.getKey(), |
| servletNameToPaths.get(entry.getValue())); |
| } |
| return new ServletValidator(classNameToServletName, classNameToPaths); |
| } catch (Exception e) { |
| logger.log(TreeLogger.WARN, "Unable to process '" + webXmlUrlString |
| + "' for servlet validation", e); |
| return null; |
| } |
| } |
| |
| static String generateMissingMappingMessage(String servletClass, |
| String servletPath, String servletName) { |
| return "Module declares a servlet class '" |
| + servletClass |
| + "' with a mapping to '" |
| + servletPath |
| + "', but the web.xml has no corresponding mapping; please add the following lines to your web.xml:\n" |
| + ServletWriter.generateServletMappingTag(servletName, servletPath); |
| } |
| |
| static String generateMissingServletMessage(String servletClass, |
| String servletPath) { |
| String servletName = suggestServletName(servletClass); |
| return "Module declares a servlet class '" |
| + servletClass |
| + "', but the web.xml has no corresponding declaration; please add the following lines to your web.xml:\n" |
| + ServletWriter.generateServletTag(servletName, servletClass) + "\n" |
| + ServletWriter.generateServletMappingTag(servletName, servletPath); |
| } |
| |
| static String suggestServletName(String servletClass) { |
| int pos = servletClass.lastIndexOf('.'); |
| String suggest = (pos < 0) ? servletClass : servletClass.substring(pos + 1); |
| String firstChar = suggest.substring(0, 1).toLowerCase(Locale.ROOT); |
| suggest = firstChar + suggest.substring(1); |
| return suggest; |
| } |
| |
| private final Map<String, Set<String>> classNameToPaths; |
| |
| private final Map<String, String> classNameToServletName; |
| |
| private ServletValidator(Map<String, String> classNameToServletName, |
| Map<String, Set<String>> classNameToPaths) { |
| this.classNameToServletName = classNameToServletName; |
| this.classNameToPaths = classNameToPaths; |
| } |
| |
| public void validate(TreeLogger logger, String servletClass, |
| String servletPath) { |
| if (containsServletClass(servletClass)) { |
| if (!containsServletMapping(servletClass, servletPath)) { |
| String servletName = getNameForClass(servletClass); |
| assert (servletName != null); |
| logger.log(TreeLogger.WARN, generateMissingMappingMessage(servletClass, |
| servletPath, servletName)); |
| } |
| } else { |
| logger.log(TreeLogger.WARN, generateMissingServletMessage(servletClass, |
| servletPath)); |
| } |
| } |
| |
| boolean containsServletClass(String servletClass) { |
| return classNameToServletName.get(servletClass) != null; |
| } |
| |
| boolean containsServletMapping(String servletClass, String servletPath) { |
| Set<String> paths = classNameToPaths.get(servletClass); |
| return (paths != null) && paths.contains(servletPath); |
| } |
| |
| String getNameForClass(String servletClass) { |
| return classNameToServletName.get(servletClass); |
| } |
| } |