/******************************************************************************* * * Copyright (c) 2004-2009 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi * * *******************************************************************************/ package hudson.util; import org.xml.sax.helpers.XMLFilterImpl; import org.xml.sax.XMLFilter; import org.xml.sax.ContentHandler; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.apache.commons.jelly.XMLOutput; import java.util.Locale; import java.util.Stack; import java.util.Map; import java.util.HashMap; import java.util.Set; import java.util.HashSet; import java.util.Arrays; /** * {@link XMLFilter} that checks the proper nesting of table related tags. * * <p> Browser often "fixes" HTML by moving tables into the right place, so * failure to generate proper tables can result in a hard-to-track bugs. * * <p> TODO: where to apply this in stapler? JellyClassTearOff creates * XMLOutput. Perhaps we define a decorator? We can also wrap Script. would that * work better? * * @author Kohsuke Kawaguchi */ public class TableNestChecker extends XMLFilterImpl { private final Stack<Checker> elements = new Stack<Checker>(); private final Stack<String> tagNames = new Stack<String>(); public static void applyTo(XMLOutput xo) { xo.setContentHandler(new TableNestChecker(xo.getContentHandler())); } public TableNestChecker() { elements.push(ALL_ALLOWED); } public TableNestChecker(ContentHandler target) { this(); setContentHandler(target); } @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { String tagName = localName.toUpperCase(Locale.ENGLISH); // make sure that this tag occurs in the proper context if (!elements.peek().isAllowed(tagName)) { throw new SAXException(tagName + " is not allowed inside " + tagNames.peek()); } Checker next = CHECKERS.get(tagName); if (next == null) { next = ALL_ALLOWED; } elements.push(next); tagNames.push(tagName); super.startElement(uri, localName, qName, atts); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { elements.pop(); tagNames.pop(); super.endElement(uri, localName, qName); } private interface Checker { boolean isAllowed(String childTag); } private static final Checker ALL_ALLOWED = new Checker() { public boolean isAllowed(String childTag) { return true; } }; private static final class InList implements Checker { private final Set<String> tags; private InList(String... tags) { this.tags = new HashSet<String>(Arrays.asList(tags)); } public boolean isAllowed(String childTag) { return tags.contains(childTag); } } private static final Map<String, Checker> CHECKERS = new HashMap<String, Checker>(); static { CHECKERS.put("TABLE", new InList("TR", "THEAD", "TBODY")); InList rows = new InList("TR"); CHECKERS.put("THEAD", rows); CHECKERS.put("THEAD", rows); CHECKERS.put("TR", new InList("TD", "TH")); } }