/** * Copyright 2014 55 Minutes (http://www.55minutes.com) * * 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 fiftyfive.wicket.test; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.xml.parsers.DocumentBuilder; import org.w3c.dom.Document; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; /** * Base class for XHTML and HTML5 validators. */ public abstract class AbstractDocumentValidator implements ErrorHandler { static final int DEF_NUM_LINES_CONTEXT = 10; private int numLinesContext = DEF_NUM_LINES_CONTEXT; private boolean parsed = false; private String[] lines = null; private List<String> errors = new ArrayList(); private Document doc; /** * Sets the number of lines of context that will be listed along with * the line that has the validation error. Must be positive. * The default is 10. */ public void setNumLinesContext(int newNumLinesContext) { if(newNumLinesContext < 0) { throw new IllegalArgumentException( "numLinesContext cannot be zero or negative" ); } this.numLinesContext = newNumLinesContext; } /** * Parses a document provided as a String. To test whether the * document was parsed successfully and is valid, subsequently call * {@link #isValid}. */ public void parse(String document) throws IOException { if(this.parsed) { throw new IllegalStateException( "parse() can only be used once. " + "Create a new instance for each document you wish to parse." ); } try { this.lines = document.split("\\r?\\n"); this.doc = builder().parse(new InputSource(new StringReader(document))); this.parsed = true; } catch(SAXException saxe) { // ignore } } /** * Returns <code>true</code> if the previous call to {@link #parse} * completed without errors or warnings. */ public boolean isValid() { return this.errors.size() == 0 && this.parsed; } public Document getDocument() { if(!this.parsed) { throw new IllegalStateException( "A document does not exist because parse() has not yet been " + "called. Parse first and then call getDocument()." ); } return this.doc; } /** * Returns the errors and warnings that were encountered in the previous * call to {@link #parse}, if any. */ public List<String> getErrors() { return Collections.unmodifiableList(this.errors); } // ==== ErrorHandler callbacks=========================================== public void error(SAXParseException exception) { this.errors.add(format(exception)); } public void fatalError(SAXParseException exception) { this.errors.add(format(exception)); } public void warning(SAXParseException exception) { this.errors.add(format(exception)); } // ==== End ErrorHandler callbacks ====================================== /** * Construct a validating DocumentBuilder with <code>this</code> * registered as the error handler. This is the parsing and validation * strategy that will be used. * Subclasses should provide the appropriate XHTML or HTML5 implementation. */ protected abstract DocumentBuilder builder(); /** * Formats a SAXParseException as a string: error message, followed * by five lines of the markup centered around the line where the error * occurs. */ private String format(SAXParseException ex) { StringBuffer buf = new StringBuffer(); buf.append(ex.getMessage()); buf.append("\n"); final int ctx = this.numLinesContext; for(int offset = -1 * ctx/2; offset <= ctx/2; offset++) { int line = ex.getLineNumber() + offset; if(line >= 1 && line <= this.lines.length) { buf.append(" "); if(line == ex.getLineNumber()) { buf.append("*"); } else { buf.append(" "); } buf.append(String.format("%-5d", line)); buf.append("| "); buf.append(this.lines[line-1]); buf.append("\n"); } } return buf.toString(); } }