All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nu.validator.messages.MessageEmitterAdapter Maven / Gradle / Ivy

Go to download

An HTML-checking library (used by https://html5.validator.nu and the HTML5 facet of the W3C Validator)

There is a newer version: 20.7.2
Show newest version
/*
 * Copyright (c) 2005, 2006, 2007 Henri Sivonen
 * Copyright (c) 2007-2015 Mozilla Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

package nu.validator.messages;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Set;

import nu.validator.io.SystemIdIOException;
import nu.validator.messages.types.MessageType;
import nu.validator.saxtree.DocumentFragment;
import nu.validator.saxtree.TreeParser;
import nu.validator.servlet.imagereview.Image;
import nu.validator.servlet.imagereview.ImageCollector;
import nu.validator.source.Location;
import nu.validator.source.SourceCode;
import nu.validator.source.SourceHandler;
import nu.validator.spec.EmptySpec;
import nu.validator.spec.Spec;
import nu.validator.spec.html5.Html5AttributeDatatypeBuilder;
import nu.validator.spec.html5.ImageReportAdviceBuilder;
import nu.validator.xml.AttributesImpl;
import nu.validator.xml.CharacterUtil;
import nu.validator.xml.XhtmlSaxEmitter;

import org.apache.log4j.Logger;
import org.relaxng.datatype.DatatypeException;
import nu.validator.checker.NormalizationChecker;
import nu.validator.checker.DatatypeMismatchException;
import nu.validator.checker.VnuBadAttrValueException;
import nu.validator.datatype.Html5DatatypeException;
import nu.validator.io.DataUri;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.ibm.icu.text.Normalizer;
import com.thaiopensource.relaxng.exceptions.AbstractValidationException;
import com.thaiopensource.relaxng.exceptions.BadAttributeValueException;
import com.thaiopensource.relaxng.exceptions.ImpossibleAttributeIgnoredException;
import com.thaiopensource.relaxng.exceptions.OnlyTextNotAllowedException;
import com.thaiopensource.relaxng.exceptions.OutOfContextElementException;
import com.thaiopensource.relaxng.exceptions.RequiredAttributesMissingException;
import com.thaiopensource.relaxng.exceptions.RequiredAttributesMissingOneOfException;
import com.thaiopensource.relaxng.exceptions.RequiredElementsMissingException;
import com.thaiopensource.relaxng.exceptions.RequiredElementsMissingOneOfException;
import com.thaiopensource.relaxng.exceptions.StringNotAllowedException;
import com.thaiopensource.relaxng.exceptions.TextNotAllowedException;
import com.thaiopensource.relaxng.exceptions.UnfinishedElementException;
import com.thaiopensource.relaxng.exceptions.UnfinishedElementOneOfException;
import com.thaiopensource.relaxng.exceptions.UnknownElementException;
import com.thaiopensource.xml.util.Name;

public final class MessageEmitterAdapter implements ErrorHandler {

    private static final Logger log4j = Logger.getLogger(MessageEmitterAdapter.class);

    private final static Map WELL_KNOWN_NAMESPACES = new HashMap();

    static {
        WELL_KNOWN_NAMESPACES.put("", "unnamespaced".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1999/xhtml",
                "XHTML".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2000/svg",
                "SVG".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1998/Math/MathML",
                "MathML".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2005/Atom",
                "Atom".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1999/xlink",
                "XLink".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://docbook.org/ns/docbook",
                "DocBook".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://relaxng.org/ns/structure/1.0",
                "RELAX NG".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/XML/1998/namespace",
                "XML".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/1999/XSL/Transform",
                "XSLT".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/ns/xbl",
                "XBL2".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
                "XUL".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
                "RDF".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://purl.org/dc/elements/1.1/",
                "Dublin Core".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2001/XMLSchema-instance",
                "XML Schema Instance".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.w3.org/2002/06/xhtml2/",
                "XHTML2".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.ascc.net/xml/schematron",
                "Schematron".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://purl.oclc.org/dsdl/schematron",
                "ISO Schematron".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://www.inkscape.org/namespaces/inkscape",
                "Inkscape".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
                "Sodipodi".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://www.openmath.org/OpenMath",
                "OpenMath".toCharArray());
        WELL_KNOWN_NAMESPACES.put(
                "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
                "Adobe SVG Viewer 3.0 extension".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/AdobeIllustrator/10.0/",
                "Adobe Illustrator 10.0".toCharArray());
        WELL_KNOWN_NAMESPACES.put("adobe:ns:meta/",
                "XMP Container".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/xap/1.0/",
                "XMP".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/pdf/1.3/",
                "Adobe PDF 1.3".toCharArray());
        WELL_KNOWN_NAMESPACES.put("http://ns.adobe.com/tiff/1.0/",
                "Adobe TIFF".toCharArray());
    }

    private final static Map HTML5_DATATYPE_ADVICE = new HashMap();

    private final static DocumentFragment IMAGE_REPORT_GENERAL;

    private final static DocumentFragment IMAGE_REPORT_EMPTY;

    private final static DocumentFragment NO_ALT_NO_LINK_ADVICE;

    private final static DocumentFragment NO_ALT_LINK_ADVICE;

    private final static DocumentFragment EMPTY_ALT_ADVICE;

    private final static DocumentFragment HAS_ALT_ADVICE;

    private final static DocumentFragment IMAGE_REPORT_FATAL;

    private static final String SPEC_LINK_URI = System.getProperty(
            "nu.validator.spec.html5-link",
            "http://www.whatwg.org/specs/web-apps/current-work/");

    private static final long MAX_MESSAGES = Integer.parseInt(System.getProperty(
            "nu.validator.messages.limit", "1000"));

    private static final Map validInputTypesByAttributeName = new TreeMap();

    static {
        validInputTypesByAttributeName.put("accept", new String[] {
                "#attr-input-accept", "file" });
        validInputTypesByAttributeName.put("alt", new String[] {
                "#attr-input-alt", "image" });
        validInputTypesByAttributeName.put("autocomplete", new String[] {
                "#attr-input-autocomplete", "text", "search", "url", "tel",
                "e-mail", "password", "datetime", "date", "month", "week",
                "time", "datetime-local", "number", "range", "color" });
        validInputTypesByAttributeName.put("autofocus",
                new String[] { "#attr-fe-autofocus" });
        validInputTypesByAttributeName.put("checked", new String[] {
                "#attr-input-checked", "checkbox", "radio" });
        validInputTypesByAttributeName.put("dirname", new String[] {
                "#attr-input-dirname", "text", "search" });
        validInputTypesByAttributeName.put("disabled",
                new String[] { "#attr-fe-disabled" });
        validInputTypesByAttributeName.put("form",
                new String[] { "#attr-fae-form" });
        validInputTypesByAttributeName.put("formaction", new String[] {
                "#attr-fs-formaction", "submit", "image" });
        validInputTypesByAttributeName.put("formenctype", new String[] {
                "#attr-fs-formenctype", "submit", "image" });
        validInputTypesByAttributeName.put("formmethod", new String[] {
                "#attr-fs-formmethod", "submit", "image" });
        validInputTypesByAttributeName.put("formnovalidate", new String[] {
                "#attr-fs-formnovalidate", "submit", "image" });
        validInputTypesByAttributeName.put("formtarget", new String[] {
                "#attr-fs-formtarget", "submit", "image" });
        validInputTypesByAttributeName.put("height", new String[] {
                "#attr-dim-height", "image" });
        validInputTypesByAttributeName.put("list", new String[] {
                "#attr-input-list", "text", "search", "url", "tel", "e-mail",
                "datetime", "date", "month", "week", "time", "datetime-local",
                "number", "range", "color" });
        validInputTypesByAttributeName.put("max", new String[] {
                "#attr-input-max", "datetime", "date", "month", "week", "time",
                "datetime-local", "number", "range", });
        validInputTypesByAttributeName.put("maxlength", new String[] {
                "#attr-input-maxlength", "text", "search", "url", "tel",
                "e-mail", "password" });
        validInputTypesByAttributeName.put("min", new String[] {
                "#attr-input-min", "datetime", "date", "month", "week", "time",
                "datetime-local", "number", "range", });
        validInputTypesByAttributeName.put("multiple", new String[] {
                "#attr-input-multiple", "email", "file" });
        validInputTypesByAttributeName.put("name",
                new String[] { "#attr-fe-name" });
        validInputTypesByAttributeName.put("pattern", new String[] {
                "#attr-input-pattern", "text", "search", "url", "tel",
                "e-mail", "password" });
        validInputTypesByAttributeName.put("placeholder", new String[] {
                "#attr-input-placeholder", "text", "search", "url", "tel",
                "e-mail", "password", "number" });
        validInputTypesByAttributeName.put("readonly", new String[] {
                "#attr-input-readonly", "text", "search", "url", "tel",
                "e-mail", "password", "datetime", "date", "month", "week",
                "time", "datetime-local", "number" });
        validInputTypesByAttributeName.put("required",
                new String[] { "#attr-input-required", "text", "search", "url",
                        "tel", "e-mail", "password", "datetime", "date",
                        "month", "week", "time", "datetime-local", "number",
                        "checkbox", "radio", "file" });
        validInputTypesByAttributeName.put("size", new String[] {
                "#attr-input-size", "text", "search", "url", "tel", "e-mail",
                "password" });
        validInputTypesByAttributeName.put("src", new String[] {
                "#attr-input-src", "image" });
        validInputTypesByAttributeName.put("step", new String[] {
                "#attr-input-step", "datetime", "date", "month", "week",
                "time", "datetime-local", "number", "range", });
        validInputTypesByAttributeName.put("type",
                new String[] { "#attr-input-type" });
        validInputTypesByAttributeName.put("value",
                new String[] { "#attr-input-value" });
        validInputTypesByAttributeName.put("width", new String[] {
                "#attr-dim-width", "image" });
    }

    private static final Map fragmentIdByInputType = new TreeMap();

    static {
        fragmentIdByInputType.put("hidden", "#hidden-state-type-hidden");
        fragmentIdByInputType.put("text",
                "#text-type-text-state-and-search-state-type-search");
        fragmentIdByInputType.put("search",
                "#text-type-text-state-and-search-state-type-search");
        fragmentIdByInputType.put("url", "#url-state-type-url");
        fragmentIdByInputType.put("tel", "#telephone-state-type-tel");
        fragmentIdByInputType.put("email", "#e-mail-state-type-email");
        fragmentIdByInputType.put("password", "#password-state-type-password");
        fragmentIdByInputType.put("datetime",
                "#date-and-time-state-type-datetime");
        fragmentIdByInputType.put("date", "#date-state-type-date");
        fragmentIdByInputType.put("month", "#month-state-type-month");
        fragmentIdByInputType.put("week", "#week-state-type-week");
        fragmentIdByInputType.put("time", "#time-state-type-time");
        fragmentIdByInputType.put("datetime-local",
                "#local-date-and-time-state-type-datetime-local");
        fragmentIdByInputType.put("number", "#number-state-type-number");
        fragmentIdByInputType.put("range", "#range-state-type-range");
        fragmentIdByInputType.put("color", "#color-state-type-color");
        fragmentIdByInputType.put("checkbox", "#checkbox-state-type-checkbox");
        fragmentIdByInputType.put("radio", "#radio-button-state-type-radio");
        fragmentIdByInputType.put("file", "#file-upload-state-type-file");
        fragmentIdByInputType.put("submit", "#submit-button-state-type-submit");
        fragmentIdByInputType.put("image", "#image-button-state-type-image");
        fragmentIdByInputType.put("reset", "#reset-button-state-type-reset");
        fragmentIdByInputType.put("button", "#button-state-type-button");
    }

    static {
        try {
            HTML5_DATATYPE_ADVICE.putAll(Html5AttributeDatatypeBuilder.parseSyntaxDescriptions());
            List list = ImageReportAdviceBuilder.parseAltAdvice();
            IMAGE_REPORT_GENERAL = list.get(0);
            NO_ALT_NO_LINK_ADVICE = list.get(1);
            NO_ALT_LINK_ADVICE = list.get(2);
            EMPTY_ALT_ADVICE = list.get(3);
            HAS_ALT_ADVICE = list.get(4);
            IMAGE_REPORT_EMPTY = list.get(5);
            IMAGE_REPORT_FATAL = list.get(6);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (SAXException e) {
            throw new RuntimeException(e);
        }
    }

    private final static char[] INDETERMINATE_MESSAGE = "The result cannot be determined due to a non-document-error.".toCharArray();

    private final static char[] ELEMENT_SPECIFIC_ATTRIBUTES_BEFORE = "Attributes for element ".toCharArray();

    private final static char[] ELEMENT_SPECIFIC_ATTRIBUTES_AFTER = ":".toCharArray();

    private final static char[] CONTENT_MODEL_BEFORE = "Content model for element ".toCharArray();

    private final static char[] CONTENT_MODEL_AFTER = ":".toCharArray();

    private final static char[] CONTEXT_BEFORE = "Contexts in which element ".toCharArray();

    private final static char[] CONTEXT_AFTER = " may be used:".toCharArray();

    private final static char[] BAD_VALUE = "Bad value ".toCharArray();

    private final static char[] POTENTIALLY_BAD_VALUE = "Potentially bad value ".toCharArray();

    private final static char[] FOR = " for ".toCharArray();

    private final static char[] ATTRIBUTE = "attribute ".toCharArray();

    private final static char[] FROM_NAMESPACE = " from namespace ".toCharArray();

    private final static char[] SPACE = " ".toCharArray();

    private final static char[] ON = " on ".toCharArray();

    private final static char[] ELEMENT = "element ".toCharArray();

    private final static char[] PERIOD = ".".toCharArray();

    private final static char[] COMMA = ", ".toCharArray();

    private final static char[] COLON = ":".toCharArray();

    private final static char[] NOT_ALLOWED_ON = " not allowed on ".toCharArray();

    private final static char[] AT_THIS_POINT = " at this point.".toCharArray();

    private final static char[] ONLY_TEXT = " is not allowed to have content that consists solely of text.".toCharArray();

    private final static char[] NOT_ALLOWED = " not allowed".toCharArray();

    private final static char[] AS_CHILD_OF = " as child of ".toCharArray();

    private final static char[] IN_THIS_CONTEXT_SUPPRESSING = " in this context. (Suppressing further errors from this subtree.)".toCharArray();

    private final static char[] REQUIRED_ATTRIBUTES_MISSING = " is missing required attribute ".toCharArray();

    private final static char[] REQUIRED_ATTRIBUTES_MISSING_ONE_OF = " is missing one or more of the following attributes: ".toCharArray();

    private final static char[] REQUIRED_ELEMENTS_MISSING = "Required elements missing.".toCharArray();

    private final static char[] IS_MISSING_A_REQUIRED_CHILD = " is missing a required child element".toCharArray();

    private final static char[] REQUIRED_CHILDREN_MISSING_FROM = " is missing a required instance of child element ".toCharArray();

    private final static char[] REQUIRED_CHILDREN_MISSING_ONE_OF_FROM = " is missing a required instance of one or more of the following child elements: ".toCharArray();

    private final static char[] BAD_CHARACTER_CONTENT = "Bad character content ".toCharArray();

    private final static char[] IN_THIS_CONTEXT = " in this context.".toCharArray();

    private final static char[] TEXT_NOT_ALLOWED_IN = "Text not allowed in ".toCharArray();

    private final static char[] UNKNOWN = "Unknown ".toCharArray();

    private static final char[] NO_ALT_NO_LINK_HEADING = "No textual alternative available, not linked".toCharArray();

    private static final char[] NO_ALT_LINK_HEADING = "No textual alternative available, image linked".toCharArray();

    private static final char[] EMPTY_ALT = "Empty textual alternative\u2014Omitted from non-graphical presentation".toCharArray();

    private static final char[] HAS_ALT = "Images with textual alternative".toCharArray();

    private final AttributesImpl attributesImpl = new AttributesImpl();

    private final char[] oneChar = { '\u0000' };

    private int warnings = 0;

    private int errors = 0;

    private int fatalErrors = 0;

    private final boolean batchMode;

    private int nonDocumentErrors = 0;

    private final SourceCode sourceCode;

    private final MessageEmitter emitter;

    private final ExactErrorHandler exactErrorHandler;

    private final boolean showSource;

    private final ImageCollector imageCollector;

    private final int lineOffset;

    private Spec spec = EmptySpec.THE_INSTANCE;

    private boolean html = false;

    private boolean loggingOk = false;

    private boolean errorsOnly = false;

    protected static String scrub(String s) throws SAXException {
        if (s == null) {
            return null;
        }
        s = CharacterUtil.prudentlyScrubCharacterData(s);
        if (NormalizationChecker.startsWithComposingChar(s)) {
            s = " " + s;
        }
        return Normalizer.normalize(s, Normalizer.NFC, 0);
    }

    private StringBuilder zapLf(StringBuilder builder) {
        int len = builder.length();
        for (int i = 0; i < len; i++) {
            char c = builder.charAt(i);
            if (c == '\n' || c == '\r') {
                builder.setCharAt(i, ' ');
            }
        }
        return builder;
    }

    private void throwIfTooManyMessages() throws SAXException {
        if (!batchMode && (warnings + errors > MAX_MESSAGES)) {
            throw new TooManyErrorsException("Too many messages.");
        }
    }

    public MessageEmitterAdapter(SourceCode sourceCode, boolean showSource,
            ImageCollector imageCollector, int lineOffset, boolean batchMode,
            MessageEmitter messageEmitter) {
        super();
        this.sourceCode = sourceCode;
        this.emitter = messageEmitter;
        this.exactErrorHandler = new ExactErrorHandler(this);
        this.showSource = showSource;
        this.lineOffset = lineOffset;
        this.batchMode = batchMode;
        this.imageCollector = imageCollector;
    }

    /**
     * @return Returns the errors.
     */
    public int getErrors() {
        return errors;
    }

    /**
     * @return Returns the fatalErrors.
     */
    public int getFatalErrors() {
        return fatalErrors;
    }

    /**
     * @return Returns the warnings.
     */
    public int getWarnings() {
        return warnings;
    }

    private boolean isErrors() {
        return !(errors == 0 && fatalErrors == 0);
    }

    /**
     * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
     */
    public void warning(SAXParseException e) throws SAXException {
        warning(e, false);
    }

    /**
     * @param e
     * @throws SAXException
     */
    private void warning(SAXParseException e, boolean exact)
            throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        this.warnings++;
        throwIfTooManyMessages();
        messageFromSAXParseException(MessageType.WARNING, e, exact);
    }

    /**
     * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
     */
    public void error(SAXParseException e) throws SAXException {
        error(e, false);
    }

    /**
     * @param e
     * @throws SAXException
     */
    private void error(SAXParseException e, boolean exact) throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        Map datatypeErrors = null;
        if (e instanceof BadAttributeValueException) {
            datatypeErrors = ((BadAttributeValueException) e).getExceptions();
        }
        if (e instanceof VnuBadAttrValueException) {
            datatypeErrors = ((VnuBadAttrValueException) e).getExceptions();
        }
        if (e instanceof DatatypeMismatchException) {
            datatypeErrors = ((DatatypeMismatchException) e).getExceptions();
        }
        if (datatypeErrors != null) {
            for (Map.Entry entry : datatypeErrors.entrySet()) {
                DatatypeException dex = entry.getValue();
                if (dex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                    if (ex5.isWarning()) {
                        this.warnings++;
                        throwIfTooManyMessages();
                        messageFromSAXParseException(MessageType.WARNING, e,
                                exact);
                        return;
                    }
                }
            }
        }
        this.errors++;
        throwIfTooManyMessages();
        messageFromSAXParseException(MessageType.ERROR, e, exact);
    }

    /**
     * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
     */
    public void fatalError(SAXParseException e) throws SAXException {
        fatalError(e, false);
    }

    /**
     * @param e
     * @throws SAXException
     */
    private void fatalError(SAXParseException e, boolean exact)
            throws SAXException {
        if ((!batchMode && fatalErrors > 0) || nonDocumentErrors > 0) {
            return;
        }
        this.fatalErrors++;
        Exception wrapped = e.getException();
        String systemId = null;
        if (wrapped instanceof SystemIdIOException) {
            SystemIdIOException siie = (SystemIdIOException) wrapped;
            systemId = siie.getSystemId();
        }
        if (wrapped instanceof IOException) {
            message(MessageType.IO, wrapped, systemId, -1, -1, false);
        } else {
            messageFromSAXParseException(MessageType.FATAL, e, exact);
        }
    }

    public void info(String str) throws SAXException {
        if (emitter instanceof GnuMessageEmitter)
            return;
        message(MessageType.INFO, new Exception(str), null, -1, -1, false);
    }

    public void ioError(IOException e) throws SAXException {
        this.nonDocumentErrors++;
        String systemId = null;
        if (e instanceof SystemIdIOException) {
            SystemIdIOException siie = (SystemIdIOException) e;
            systemId = siie.getSystemId();
        }
        message(MessageType.IO, e, systemId, -1, -1, false);
    }

    public void internalError(Throwable e, String message) throws SAXException {
        this.nonDocumentErrors++;
        message(MessageType.INTERNAL, new Exception(message), null, -1, -1,
                false);
    }

    public void schemaError(Exception e) throws SAXException {
        this.nonDocumentErrors++;
        message(MessageType.SCHEMA, e, null, -1, -1, false);
    }

    public void start(String documentUri) throws SAXException {
        emitter.startMessages(scrub(shortenDataUri(documentUri)), showSource);
    }

    private String shortenDataUri(String uri) {
        if (DataUri.startsWithData(uri)) {
            return "data:\u2026";
        } else {
            return uri;
        }
    }

    public void end(String successMessage, String failureMessage)
            throws SAXException {
        ResultHandler resultHandler = emitter.startResult();
        if (resultHandler != null) {
            if (isIndeterminate()) {
                resultHandler.startResult(Result.INDETERMINATE);
                resultHandler.characters(INDETERMINATE_MESSAGE, 0,
                        INDETERMINATE_MESSAGE.length);
                resultHandler.endResult();
            } else if (isErrors()) {
                resultHandler.startResult(Result.FAILURE);
                resultHandler.characters(failureMessage.toCharArray(), 0,
                        failureMessage.length());
                resultHandler.endResult();
            } else {
                resultHandler.startResult(Result.SUCCESS);
                resultHandler.characters(successMessage.toCharArray(), 0,
                        successMessage.length());
                resultHandler.endResult();
            }
        }
        emitter.endResult();

        if (imageCollector != null) {
            DocumentFragment instruction = IMAGE_REPORT_GENERAL;
            boolean fatal = false;
            if (getFatalErrors() > 0) {
                fatal = true;
                instruction = IMAGE_REPORT_FATAL;
            } else if (imageCollector.isEmpty()) {
                instruction = IMAGE_REPORT_EMPTY;
            }

            ImageReviewHandler imageReviewHandler = emitter.startImageReview(
                    instruction, fatal);
            if (imageReviewHandler != null && !fatal) {
                emitImageReview(imageReviewHandler);
            }
            emitter.endImageReview();
        }

        if (showSource) {
            SourceHandler sourceHandler = emitter.startFullSource(lineOffset);
            if (sourceHandler != null) {
                sourceCode.emitSource(sourceHandler);
            }
            emitter.endFullSource();
        }
        emitter.endMessages();
    }

    private void emitImageReview(ImageReviewHandler imageReviewHandler)
            throws SAXException {
        List noAltNoLink = new LinkedList();
        List noAltLink = new LinkedList();
        List emptyAlt = new LinkedList();
        List hasAlt = new LinkedList();

        for (Image image : imageCollector) {
            String alt = image.getAlt();
            if (alt == null) {
                if (image.isLinked()) {
                    noAltLink.add(image);
                } else {
                    noAltNoLink.add(image);
                }
            } else if ("".equals(alt)) {
                emptyAlt.add(image);
            } else {
                hasAlt.add(image);
            }
        }

        emitImageList(imageReviewHandler, noAltLink, NO_ALT_LINK_HEADING,
                NO_ALT_LINK_ADVICE, false);
        emitImageList(imageReviewHandler, noAltNoLink, NO_ALT_NO_LINK_HEADING,
                NO_ALT_NO_LINK_ADVICE, false);
        emitImageList(imageReviewHandler, emptyAlt, EMPTY_ALT,
                EMPTY_ALT_ADVICE, false);
        emitImageList(imageReviewHandler, hasAlt, HAS_ALT, HAS_ALT_ADVICE, true);
    }

    private void emitImageList(ImageReviewHandler imageReviewHandler,
            List list, char[] heading, DocumentFragment instruction,
            boolean hasAlt) throws SAXException {
        if (!list.isEmpty()) {
            imageReviewHandler.startImageGroup(heading, instruction, hasAlt);
            for (Image image : list) {
                String systemId = image.getSystemId();
                int oneBasedLine = image.getLineNumber();
                int oneBasedColumn = image.getColumnNumber();
                Location rangeLast = sourceCode.newLocatorLocation(
                        oneBasedLine, oneBasedColumn);
                if (sourceCode.isWithinKnownSource(rangeLast)) {
                    Location rangeStart = sourceCode.rangeStartForRangeLast(rangeLast);
                    imageReviewHandler.image(image, hasAlt, systemId,
                            rangeStart.getLine() + 1,
                            rangeStart.getColumn() + 1, oneBasedLine,
                            oneBasedColumn);
                } else {
                    imageReviewHandler.image(image, hasAlt, systemId, -1, -1,
                            -1, -1);
                }
            }
            imageReviewHandler.endImageGroup();
        }
    }

    private boolean isIndeterminate() {
        return nonDocumentErrors > 0;
    }

    private void messageFromSAXParseException(MessageType type,
            SAXParseException spe, boolean exact) throws SAXException {
        message(type, spe, spe.getSystemId(), spe.getLineNumber(),
                spe.getColumnNumber(), exact);
    }

    private void message(MessageType type, Exception message, String systemId,
            int oneBasedLine, int oneBasedColumn, boolean exact)
            throws SAXException {
        if (loggingOk
                && (type.getSuperType() == "error")
                && spec != EmptySpec.THE_INSTANCE
                && systemId != null
                && (systemId.startsWith("http:") || systemId.startsWith("https:"))) {
            log4j.info(zapLf(new StringBuilder().append(systemId).append('\t').append(
                    message.getMessage())));
        }
        if (errorsOnly && type.getSuperType() == "info") {
            return;
        }
        String uri = sourceCode.getUri();
        if (oneBasedLine > -1
                && (uri == systemId || (uri != null && uri.equals(systemId)))) {
            if (oneBasedColumn > -1) {
                if (exact) {
                    messageWithExact(type, message, oneBasedLine,
                            oneBasedColumn);
                } else {
                    messageWithRange(type, message, oneBasedLine,
                            oneBasedColumn);
                }
            } else {
                messageWithLine(type, message, oneBasedLine);
            }
        } else {
            messageWithoutExtract(type, message, systemId, oneBasedLine,
                    oneBasedColumn);
        }
    }

    private void messageWithRange(MessageType type, Exception message,
            int oneBasedLine, int oneBasedColumn) throws SAXException {
        Location rangeLast = sourceCode.newLocatorLocation(oneBasedLine,
                oneBasedColumn);
        if (!sourceCode.isWithinKnownSource(rangeLast)) {
            messageWithoutExtract(type, message, null, oneBasedLine,
                    oneBasedColumn);
            return;
        }
        Location rangeStart = sourceCode.rangeStartForRangeLast(rangeLast);
        startMessage(type, null, rangeStart.getLine() + 1,
                rangeStart.getColumn() + 1, oneBasedLine, oneBasedColumn, false);
        messageText(message);
        SourceHandler sourceHandler = emitter.startSource();
        if (sourceHandler != null) {
            sourceCode.rangeEndError(rangeStart, rangeLast, sourceHandler);
        }
        emitter.endSource();
        elaboration(message);
        endMessage();
    }

    private void messageWithExact(MessageType type, Exception message,
            int oneBasedLine, int oneBasedColumn) throws SAXException {
        startMessage(type, null, oneBasedLine, oneBasedColumn, oneBasedLine,
                oneBasedColumn, true);
        messageText(message);
        Location location = sourceCode.newLocatorLocation(oneBasedLine,
                oneBasedColumn);
        if (sourceCode.isWithinKnownSource(location)) {
            SourceHandler sourceHandler = emitter.startSource();
            if (sourceHandler != null) {
                sourceCode.exactError(location, sourceHandler);
            }
            emitter.endSource();
        } else {
            sourceCode.rememberExactError(location);
        }
        elaboration(message);
        endMessage();
    }

    private void messageWithLine(MessageType type, Exception message,
            int oneBasedLine) throws SAXException {
        if (!sourceCode.isWithinKnownSource(oneBasedLine)) {
            throw new RuntimeException("Bug. Line out of range!");
        }
        startMessage(type, null, oneBasedLine, -1, oneBasedLine, -1, false);
        messageText(message);
        SourceHandler sourceHandler = emitter.startSource();
        if (sourceHandler != null) {
            sourceCode.lineError(oneBasedLine, sourceHandler);
        }
        emitter.endSource();
        elaboration(message);
        endMessage();
    }

    private void messageWithoutExtract(MessageType type, Exception message,
            String systemId, int oneBasedLine, int oneBasedColumn)
            throws SAXException {
        startMessage(type, scrub(shortenDataUri(systemId)), oneBasedLine,
                oneBasedColumn, oneBasedLine, oneBasedColumn, false);
        messageText(message);
        elaboration(message);
        endMessage();
    }

    /**
     * @param message
     * @throws SAXException
     */
    private void messageText(Exception message) throws SAXException {
        if (message instanceof AbstractValidationException) {
            AbstractValidationException ave = (AbstractValidationException) message;
            rngMessageText(ave);
        } else if (message instanceof VnuBadAttrValueException) {
            VnuBadAttrValueException e = (VnuBadAttrValueException) message;
            vnuBadAttrValueMessageText(e);
        } else {
            String msg = message.getMessage();
            if (msg != null) {
                MessageTextHandler messageTextHandler = emitter.startText();
                if (messageTextHandler != null) {
                    emitStringWithQurlyQuotes(messageTextHandler, msg);
                }
                emitter.endText();
            }
        }
    }

    private void vnuBadAttrValueMessageText(VnuBadAttrValueException e)
            throws SAXException {
        MessageTextHandler messageTextHandler = emitter.startText();
        if (messageTextHandler != null) {
            boolean isWarning = false;
            Map datatypeErrors = e.getExceptions();
            for (Map.Entry entry : datatypeErrors.entrySet()) {
                DatatypeException dex = entry.getValue();
                if (dex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                    if (ex5.isWarning()) {
                        isWarning = true;
                    }
                }
            }
            if (isWarning) {
                messageTextString(messageTextHandler, POTENTIALLY_BAD_VALUE,
                        false);
            } else {
                messageTextString(messageTextHandler, BAD_VALUE, false);
            }
            if (e.getAttributeValue().length() < 200) {
                codeString(messageTextHandler, e.getAttributeValue());
            }
            messageTextString(messageTextHandler, FOR, false);
            attribute(messageTextHandler, e.getAttributeName(),
                    e.getCurrentElement(), false);
            messageTextString(messageTextHandler, ON, false);
            element(messageTextHandler, e.getCurrentElement(), false);
            emitDatatypeErrors(messageTextHandler, e.getExceptions());
        }
        emitter.endText();
    }

    @SuppressWarnings("unchecked") private void rngMessageText(
            AbstractValidationException e) throws SAXException {
        MessageTextHandler messageTextHandler = emitter.startText();
        if (messageTextHandler != null) {
            if (e instanceof BadAttributeValueException) {
                BadAttributeValueException ex = (BadAttributeValueException) e;
                boolean isWarning = false;
                Map datatypeErrors = ex.getExceptions();
                for (Map.Entry entry : datatypeErrors.entrySet()) {
                    DatatypeException dex = entry.getValue();
                    if (dex instanceof Html5DatatypeException) {
                        Html5DatatypeException ex5 = (Html5DatatypeException) dex;
                        if (ex5.isWarning()) {
                            isWarning = true;
                        }
                    }
                }
                if (isWarning) {
                    messageTextString(messageTextHandler,
                            POTENTIALLY_BAD_VALUE, false);
                } else {
                    messageTextString(messageTextHandler, BAD_VALUE, false);
                }
                if (ex.getAttributeValue().length() < 200) {
                    codeString(messageTextHandler, ex.getAttributeValue());
                }
                messageTextString(messageTextHandler, FOR, false);
                attribute(messageTextHandler, ex.getAttributeName(),
                        ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, ON, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                emitDatatypeErrors(messageTextHandler, ex.getExceptions());
            } else if (e instanceof ImpossibleAttributeIgnoredException) {
                ImpossibleAttributeIgnoredException ex = (ImpossibleAttributeIgnoredException) e;
                attribute(messageTextHandler, ex.getAttributeName(),
                        ex.getCurrentElement(), true);
                messageTextString(messageTextHandler, NOT_ALLOWED_ON, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, AT_THIS_POINT, false);
            } else if (e instanceof OnlyTextNotAllowedException) {
                OnlyTextNotAllowedException ex = (OnlyTextNotAllowedException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler, ONLY_TEXT, false);
            } else if (e instanceof OutOfContextElementException) {
                OutOfContextElementException ex = (OutOfContextElementException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler, NOT_ALLOWED, false);
                if (ex.getParent() != null) {
                    messageTextString(messageTextHandler, AS_CHILD_OF, false);
                    element(messageTextHandler, ex.getParent(), false);
                }
                messageTextString(messageTextHandler,
                        IN_THIS_CONTEXT_SUPPRESSING, false);
            } else if (e instanceof RequiredAttributesMissingOneOfException) {
                RequiredAttributesMissingOneOfException ex = (RequiredAttributesMissingOneOfException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_ATTRIBUTES_MISSING_ONE_OF, false);
                for (Iterator iter = ex.getAttributeLocalNames().iterator(); iter.hasNext();) {
                    codeString(messageTextHandler, iter.next());
                    if (iter.hasNext()) {
                        messageTextString(messageTextHandler, COMMA, false);
                    }
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof RequiredAttributesMissingException) {
                RequiredAttributesMissingException ex = (RequiredAttributesMissingException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_ATTRIBUTES_MISSING, false);
                codeString(messageTextHandler, ex.getAttributeLocalName());
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof RequiredElementsMissingException) {
                RequiredElementsMissingException ex = (RequiredElementsMissingException) e;
                if (ex.getParent() == null) {
                    messageTextString(messageTextHandler,
                            REQUIRED_ELEMENTS_MISSING, false);
                } else {
                    element(messageTextHandler, ex.getParent(), true);
                    if (ex.getMissingElementName() == null) {
                        messageTextString(messageTextHandler,
                                IS_MISSING_A_REQUIRED_CHILD, false);
                    } else {
                        messageTextString(messageTextHandler,
                                REQUIRED_CHILDREN_MISSING_FROM, false);
                        codeString(messageTextHandler,
                                ex.getMissingElementName());
                    }
                    messageTextString(messageTextHandler, PERIOD, false);
                }
            } else if (e instanceof StringNotAllowedException) {
                StringNotAllowedException ex = (StringNotAllowedException) e;
                messageTextString(messageTextHandler, BAD_CHARACTER_CONTENT,
                        false);
                codeString(messageTextHandler, ex.getValue());
                messageTextString(messageTextHandler, FOR, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                emitDatatypeErrors(messageTextHandler, ex.getExceptions());
            } else if (e instanceof TextNotAllowedException) {
                TextNotAllowedException ex = (TextNotAllowedException) e;
                messageTextString(messageTextHandler, TEXT_NOT_ALLOWED_IN,
                        false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, IN_THIS_CONTEXT, false);
            } else if (e instanceof UnfinishedElementException) {
                UnfinishedElementException ex = (UnfinishedElementException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                if (ex.getMissingElementName() == null) {
                    messageTextString(messageTextHandler,
                            IS_MISSING_A_REQUIRED_CHILD, false);
                } else {
                    messageTextString(messageTextHandler,
                            REQUIRED_CHILDREN_MISSING_FROM, false);
                    codeString(messageTextHandler, ex.getMissingElementName());
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof UnfinishedElementOneOfException) {
                UnfinishedElementOneOfException ex = (UnfinishedElementOneOfException) e;
                element(messageTextHandler, ex.getCurrentElement(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_CHILDREN_MISSING_ONE_OF_FROM, false);
                for (Iterator iter = ex.getMissingElementNames().iterator(); iter.hasNext();) {
                    String missingElementName = iter.next();
                    if (!("http://www.w3.org/1999/xhtml".equals(ex.getCurrentElement().getNamespaceUri()) && "frameset".equals(missingElementName))) {
                        codeString(messageTextHandler, missingElementName);
                        if (iter.hasNext()) {
                            messageTextString(messageTextHandler, COMMA, false);
                        }
                    }
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof RequiredElementsMissingOneOfException) {
                RequiredElementsMissingOneOfException ex = (RequiredElementsMissingOneOfException) e;
                element(messageTextHandler, ex.getParent(), true);
                messageTextString(messageTextHandler,
                        REQUIRED_CHILDREN_MISSING_ONE_OF_FROM, false);
                for (Iterator iter = ex.getMissingElementNames().iterator(); iter.hasNext();) {
                    String missingElementName = iter.next();
                    if (!("http://www.w3.org/1999/xhtml".equals(ex.getCurrentElement().getNamespaceUri()) && "frameset".equals(missingElementName))) {
                        codeString(messageTextHandler, missingElementName);
                        if (iter.hasNext()) {
                            messageTextString(messageTextHandler, COMMA, false);
                        }
                    }
                }
                messageTextString(messageTextHandler, PERIOD, false);
            } else if (e instanceof UnknownElementException) {
                UnknownElementException ex = (UnknownElementException) e;
                messageTextString(messageTextHandler, UNKNOWN, false);
                element(messageTextHandler, ex.getCurrentElement(), false);
                messageTextString(messageTextHandler, NOT_ALLOWED, false);
                if (ex.getParent() != null) {
                    messageTextString(messageTextHandler, AS_CHILD_OF, false);
                    element(messageTextHandler, ex.getParent(), false);
                }
                messageTextString(messageTextHandler, PERIOD, false);
            }
        }
        emitter.endText();
    }

    /**
     * @param messageTextHandler
     * @param datatypeErrors
     * @throws SAXException
     */
    private void emitDatatypeErrors(MessageTextHandler messageTextHandler,
            Map datatypeErrors) throws SAXException {
        if (datatypeErrors.isEmpty()) {
            messageTextString(messageTextHandler, PERIOD, false);
        } else {
            messageTextString(messageTextHandler, COLON, false);
            for (Map.Entry entry : datatypeErrors.entrySet()) {
                messageTextString(messageTextHandler, SPACE, false);
                DatatypeException ex = entry.getValue();
                if (ex instanceof Html5DatatypeException) {
                    Html5DatatypeException ex5 = (Html5DatatypeException) ex;
                    String[] segments = ex5.getSegments();
                    for (int i = 0; i < segments.length; i++) {
                        String segment = segments[i];
                        if (i % 2 == 0) {
                            emitStringWithQurlyQuotes(messageTextHandler,
                                    segment);
                        } else {
                            String scrubbed = scrub(segment);
                            messageTextHandler.startCode();
                            messageTextHandler.characters(
                                    scrubbed.toCharArray(), 0,
                                    scrubbed.length());
                            messageTextHandler.endCode();
                        }
                    }
                } else {
                    emitStringWithQurlyQuotes(messageTextHandler,
                            ex.getMessage());
                }
            }
        }
    }

    private void element(MessageTextHandler messageTextHandler, Name element,
            boolean atSentenceStart) throws SAXException {
        if (html) {
            messageTextString(messageTextHandler, ELEMENT, atSentenceStart);
            linkedCodeString(messageTextHandler, element.getLocalName(),
                    spec.elementLink(element));
        } else {
            String ns = element.getNamespaceUri();
            char[] humanReadable = WELL_KNOWN_NAMESPACES.get(ns);
            if (humanReadable == null) {
                if (loggingOk) {
                    log4j.info(new StringBuilder().append("UNKNOWN_NS:\t").append(
                            ns));
                }
                messageTextString(messageTextHandler, ELEMENT, atSentenceStart);
                linkedCodeString(messageTextHandler, element.getLocalName(),
                        spec.elementLink(element));
                messageTextString(messageTextHandler, FROM_NAMESPACE, false);
                codeString(messageTextHandler, ns);
            } else {
                messageTextString(messageTextHandler, humanReadable,
                        atSentenceStart);
                messageTextString(messageTextHandler, SPACE, false);
                messageTextString(messageTextHandler, ELEMENT, false);
                linkedCodeString(messageTextHandler, element.getLocalName(),
                        spec.elementLink(element));
            }
        }
    }

    private void linkedCodeString(MessageTextHandler messageTextHandler,
            String str, String url) throws SAXException {
        if (url != null) {
            messageTextHandler.startLink(url, null);
        }
        codeString(messageTextHandler, str);
        if (url != null) {
            messageTextHandler.endLink();
        }

    }

    private void attribute(MessageTextHandler messageTextHandler,
            Name attributeName, Name elementName, boolean atSentenceStart)
            throws SAXException {
        String ns = attributeName.getNamespaceUri();
        if (html || "".equals(ns)) {
            messageTextString(messageTextHandler, ATTRIBUTE, atSentenceStart);
            codeString(messageTextHandler, attributeName.getLocalName());
        } else if ("http://www.w3.org/XML/1998/namespace".equals(ns)) {
            messageTextString(messageTextHandler, ATTRIBUTE, atSentenceStart);
            codeString(messageTextHandler,
                    "xml:" + attributeName.getLocalName());
        } else {
            char[] humanReadable = WELL_KNOWN_NAMESPACES.get(ns);
            if (humanReadable == null) {
                if (loggingOk) {
                    log4j.info(new StringBuilder().append("UNKNOWN_NS:\t").append(
                            ns));
                }
                messageTextString(messageTextHandler, ATTRIBUTE,
                        atSentenceStart);
                codeString(messageTextHandler, attributeName.getLocalName());
                messageTextString(messageTextHandler, FROM_NAMESPACE, false);
                codeString(messageTextHandler, ns);
            } else {
                messageTextString(messageTextHandler, humanReadable,
                        atSentenceStart);
                messageTextString(messageTextHandler, SPACE, false);
                messageTextString(messageTextHandler, ATTRIBUTE, false);
                codeString(messageTextHandler, attributeName.getLocalName());
            }
        }
    }

    private void codeString(MessageTextHandler messageTextHandler, String str)
            throws SAXException {
        messageTextHandler.startCode();
        messageTextHandler.characters(str.toCharArray(), 0, str.length());
        messageTextHandler.endCode();
    }

    private void messageTextString(MessageTextHandler messageTextHandler,
            char[] ch, boolean capitalize) throws SAXException {
        if (capitalize && ch[0] >= 'a' && ch[0] <= 'z') {
            oneChar[0] = (char) (ch[0] - 0x20);
            messageTextHandler.characters(oneChar, 0, 1);
            if (ch.length > 1) {
                messageTextHandler.characters(ch, 1, ch.length - 1);
            }
        } else {
            messageTextHandler.characters(ch, 0, ch.length);
        }
    }

    private void emitStringWithQurlyQuotes(
            MessageTextHandler messageTextHandler, String message)
            throws SAXException {
        if (message == null) {
            message = "";
        }
        message = scrub(message);
        int len = message.length();
        int start = 0;
        int startQuotes = 0;
        for (int i = 0; i < len; i++) {
            char c = message.charAt(i);
            if (c == '\u201C') {
                startQuotes++;
                if (startQuotes == 1) {
                    char[] scrubbed = scrub(message.substring(start, i)).toCharArray();
                    messageTextHandler.characters(scrubbed, 0, scrubbed.length);
                    start = i + 1;
                    messageTextHandler.startCode();
                }
            } else if (c == '\u201D' && startQuotes > 0) {
                startQuotes--;
                if (startQuotes == 0) {
                    char[] scrubbed = scrub(message.substring(start, i)).toCharArray();
                    messageTextHandler.characters(scrubbed, 0, scrubbed.length);
                    start = i + 1;
                    messageTextHandler.endCode();
                }
            }
        }
        if (start < len) {
            char[] scrubbed = scrub(message.substring(start, len)).toCharArray();
            messageTextHandler.characters(scrubbed, 0, scrubbed.length);
        }
        if (startQuotes > 0) {
            messageTextHandler.endCode();
        }
    }

    @SuppressWarnings("unchecked") private void elaboration(Exception e)
            throws SAXException {
        if (!(e instanceof AbstractValidationException
                || e instanceof VnuBadAttrValueException || e instanceof DatatypeMismatchException)) {
            return;
        }

        if (e instanceof ImpossibleAttributeIgnoredException) {
            ImpossibleAttributeIgnoredException ex = (ImpossibleAttributeIgnoredException) e;
            Name elt = ex.getCurrentElement();
            elaborateElementSpecificAttributes(elt, ex.getAttributeName());
        } else if (e instanceof OnlyTextNotAllowedException) {
            OnlyTextNotAllowedException ex = (OnlyTextNotAllowedException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof OutOfContextElementException) {
            OutOfContextElementException ex = (OutOfContextElementException) e;
            Name parent = ex.getParent();
            Name child = ex.getCurrentElement();
            elaborateContentModelandContext(parent, child);
        } else if (e instanceof RequiredAttributesMissingException) {
            RequiredAttributesMissingException ex = (RequiredAttributesMissingException) e;
            Name elt = ex.getCurrentElement();
            elaborateElementSpecificAttributes(elt);
        } else if (e instanceof RequiredAttributesMissingOneOfException) {
            RequiredAttributesMissingOneOfException ex = (RequiredAttributesMissingOneOfException) e;
            Name elt = ex.getCurrentElement();
            elaborateElementSpecificAttributes(elt);
        } else if (e instanceof RequiredElementsMissingException) {
            RequiredElementsMissingException ex = (RequiredElementsMissingException) e;
            Name elt = ex.getParent();
            elaborateContentModel(elt);
        } else if (e instanceof RequiredElementsMissingOneOfException) {
            RequiredElementsMissingOneOfException ex = (RequiredElementsMissingOneOfException) e;
            Name elt = ex.getParent();
            elaborateContentModel(elt);
        } else if (e instanceof StringNotAllowedException) {
            StringNotAllowedException ex = (StringNotAllowedException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof TextNotAllowedException) {
            TextNotAllowedException ex = (TextNotAllowedException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof UnfinishedElementException) {
            UnfinishedElementException ex = (UnfinishedElementException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof UnfinishedElementOneOfException) {
            UnfinishedElementOneOfException ex = (UnfinishedElementOneOfException) e;
            Name elt = ex.getCurrentElement();
            elaborateContentModel(elt);
        } else if (e instanceof UnknownElementException) {
            UnknownElementException ex = (UnknownElementException) e;
            Name elt = ex.getParent();
            elaborateContentModel(elt);
        } else if (e instanceof BadAttributeValueException) {
            BadAttributeValueException ex = (BadAttributeValueException) e;
            Map map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof VnuBadAttrValueException) {
            VnuBadAttrValueException ex = (VnuBadAttrValueException) e;
            Map map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof DatatypeMismatchException) {
            DatatypeMismatchException ex = (DatatypeMismatchException) e;
            Map map = ex.getExceptions();
            elaborateDatatypes(map);
        } else if (e instanceof StringNotAllowedException) {
            StringNotAllowedException ex = (StringNotAllowedException) e;
            Map map = ex.getExceptions();
            elaborateDatatypes(map);
        }
    }

    private void elaborateDatatypes(Map map)
            throws SAXException {
        Set fragments = new HashSet();
        for (Map.Entry entry : map.entrySet()) {
            DatatypeException ex = entry.getValue();
            if (ex instanceof Html5DatatypeException) {
                Html5DatatypeException ex5 = (Html5DatatypeException) ex;
                DocumentFragment fragment = HTML5_DATATYPE_ADVICE.get(ex5.getDatatypeClass());
                if (fragment != null) {
                    fragments.add(fragment);
                }
            }
        }
        if (!fragments.isEmpty()) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                TreeParser treeParser = new TreeParser(ch, null);
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                xhtmlSaxEmitter.startElement("dl");
                for (DocumentFragment fragment : fragments) {
                    treeParser.parse(fragment);
                }
                xhtmlSaxEmitter.endElement("dl");
            }
            emitter.endElaboration();

        }
    }

    /**
     * @param elt
     * @throws SAXException
     */
    private void elaborateContentModel(Name elt) throws SAXException {
        DocumentFragment dds = spec.contentModelDescription(elt);
        if (dds != null) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                TreeParser treeParser = new TreeParser(ch, null);
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                xhtmlSaxEmitter.startElement("dl");
                emitContentModelDt(xhtmlSaxEmitter, elt);
                treeParser.parse(dds);
                xhtmlSaxEmitter.endElement("dl");
            }
            emitter.endElaboration();
        }
    }

    private void elaborateContentModelandContext(Name parent, Name child)
            throws SAXException {
        DocumentFragment contentModelDds = spec.contentModelDescription(parent);
        DocumentFragment contextDds = spec.contextDescription(child);
        if (contentModelDds != null || contextDds != null) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                TreeParser treeParser = new TreeParser(ch, null);
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                xhtmlSaxEmitter.startElement("dl");
                if (contextDds != null) {
                    emitContextDt(xhtmlSaxEmitter, child);
                    treeParser.parse(contextDds);
                }
                if (contentModelDds != null) {
                    emitContentModelDt(xhtmlSaxEmitter, parent);
                    treeParser.parse(contentModelDds);
                }
                xhtmlSaxEmitter.endElement("dl");
            }
            emitter.endElaboration();
        }
    }

    /**
     * @param elt
     * @throws SAXException
     */
    private void elaborateElementSpecificAttributes(Name elt)
            throws SAXException {
        this.elaborateElementSpecificAttributes(elt, null);
    }

    private void elaborateElementSpecificAttributes(Name elt, Name attribute)
            throws SAXException {
        if ("input".equals(elt.getLocalName())) {
            ContentHandler ch = emitter.startElaboration();
            if (ch != null) {
                XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                elaborateInputAttributes(xhtmlSaxEmitter, elt, attribute);
            }
            emitter.endElaboration();
        } else {
            DocumentFragment dds = spec.elementSpecificAttributesDescription(elt);
            if (dds != null) {
                ContentHandler ch = emitter.startElaboration();
                if (ch != null) {
                    TreeParser treeParser = new TreeParser(ch, null);
                    XhtmlSaxEmitter xhtmlSaxEmitter = new XhtmlSaxEmitter(ch);
                    xhtmlSaxEmitter.startElement("dl");
                    emitElementSpecificAttributesDt(xhtmlSaxEmitter, elt);
                    treeParser.parse(dds);
                    xhtmlSaxEmitter.endElement("dl");
                }
                emitter.endElaboration();
            }
        }
    }

    private void emitElementSpecificAttributesDt(
            XhtmlSaxEmitter xhtmlSaxEmitter, Name elt) throws SAXException {
        xhtmlSaxEmitter.startElement("dt");
        xhtmlSaxEmitter.characters(ELEMENT_SPECIFIC_ATTRIBUTES_BEFORE);
        emitLinkifiedLocalName(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.characters(ELEMENT_SPECIFIC_ATTRIBUTES_AFTER);
        xhtmlSaxEmitter.endElement("dt");
    }

    private void emitContextDt(XhtmlSaxEmitter xhtmlSaxEmitter, Name elt)
            throws SAXException {
        xhtmlSaxEmitter.startElement("dt");
        xhtmlSaxEmitter.characters(CONTEXT_BEFORE);
        emitLinkifiedLocalName(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.characters(CONTEXT_AFTER);
        xhtmlSaxEmitter.endElement("dt");
    }

    private void emitContentModelDt(XhtmlSaxEmitter xhtmlSaxEmitter, Name elt)
            throws SAXException {
        xhtmlSaxEmitter.startElement("dt");
        xhtmlSaxEmitter.characters(CONTENT_MODEL_BEFORE);
        emitLinkifiedLocalName(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.characters(CONTENT_MODEL_AFTER);
        xhtmlSaxEmitter.endElement("dt");
    }

    private void emitLinkifiedLocalName(XhtmlSaxEmitter xhtmlSaxEmitter,
            Name elt) throws SAXException {
        String url = spec.elementLink(elt);
        if (url != null) {
            attributesImpl.clear();
            attributesImpl.addAttribute("href", url);
            xhtmlSaxEmitter.startElement("a", attributesImpl);
        }
        xhtmlSaxEmitter.startElement("code");
        xhtmlSaxEmitter.characters(elt.getLocalName());
        xhtmlSaxEmitter.endElement("code");
        if (url != null) {
            xhtmlSaxEmitter.endElement("a");
        }
    }

    private void elaborateInputAttributes(XhtmlSaxEmitter xhtmlSaxEmitter,
            Name elt, Name badAttribute) throws SAXException {
        attributesImpl.clear();
        attributesImpl.addAttribute("class", "inputattrs");
        xhtmlSaxEmitter.startElement("dl", attributesImpl);
        emitElementSpecificAttributesDt(xhtmlSaxEmitter, elt);
        xhtmlSaxEmitter.startElement("dd");
        attributesImpl.clear();
        addHyperlink(xhtmlSaxEmitter, "Global attributes", SPEC_LINK_URI
                + "#global-attributes");
        attributesImpl.addAttribute("class", "inputattrtypes");
        xhtmlSaxEmitter.startElement("span", attributesImpl);
        xhtmlSaxEmitter.endElement("span");
        xhtmlSaxEmitter.endElement("dd");
        for (Map.Entry entry : validInputTypesByAttributeName.entrySet()) {
            String attributeName = entry.getKey();
            xhtmlSaxEmitter.startElement("dd");
            attributesImpl.clear();
            attributesImpl.addAttribute("class", "inputattrname");
            xhtmlSaxEmitter.startElement("code", attributesImpl);
            attributesImpl.clear();
            attributesImpl.addAttribute("href",
                    SPEC_LINK_URI + entry.getValue()[0]);
            xhtmlSaxEmitter.startElement("a", attributesImpl);
            addText(xhtmlSaxEmitter, attributeName);
            xhtmlSaxEmitter.endElement("a");
            xhtmlSaxEmitter.endElement("code");
            attributesImpl.addAttribute("class", "inputattrtypes");
            if (badAttribute != null
                    && attributeName.equals(badAttribute.getLocalName())) {
                listInputTypesForAttribute(xhtmlSaxEmitter, attributeName, true);
            } else {
                listInputTypesForAttribute(xhtmlSaxEmitter, attributeName,
                        false);
            }
            xhtmlSaxEmitter.endElement("dd");
        }
        xhtmlSaxEmitter.endElement("dl");
    }

    private void listInputTypesForAttribute(XhtmlSaxEmitter xhtmlSaxEmitter,
            String attributeName, boolean bad) throws SAXException {
        String[] typeNames = validInputTypesByAttributeName.get(attributeName);
        int typeCount = typeNames.length;
        String wrapper = (bad ? "b" : "span");
        String highlight = (bad ? " highlight" : "");
        if (typeCount > 1 || "value".equals(attributeName)) {
            addText(xhtmlSaxEmitter, " ");
            AttributesImpl attributesImpl = new AttributesImpl();
            attributesImpl.addAttribute("class", "inputattrtypes" + highlight);
            xhtmlSaxEmitter.startElement(wrapper, attributesImpl);
            addText(xhtmlSaxEmitter, "when ");
            xhtmlSaxEmitter.startElement("code");
            addText(xhtmlSaxEmitter, "type");
            xhtmlSaxEmitter.endElement("code", "code");
            addText(xhtmlSaxEmitter, " is ");
            if ("value".equals(attributeName)) {
                addText(xhtmlSaxEmitter, "not ");
                addHyperlink(xhtmlSaxEmitter, "file", SPEC_LINK_URI
                        + fragmentIdByInputType.get("file"));
                addText(xhtmlSaxEmitter, " or ");
                addHyperlink(xhtmlSaxEmitter, "image", SPEC_LINK_URI
                        + fragmentIdByInputType.get("image"));
            } else {
                for (int i = 1; i < typeCount; i++) {
                    String typeName = typeNames[i];
                    if (i > 1) {
                        addText(xhtmlSaxEmitter, " ");
                    }
                    if (typeCount > 2 && i == typeCount - 1) {
                        addText(xhtmlSaxEmitter, "or ");
                    }
                    addHyperlink(xhtmlSaxEmitter, typeName, SPEC_LINK_URI
                            + fragmentIdByInputType.get(typeName));
                    if (i < typeCount - 1 && typeCount > 3) {
                        addText(xhtmlSaxEmitter, ",");
                    }
                }
            }
            xhtmlSaxEmitter.endElement(wrapper);
        } else {
            AttributesImpl attributesImpl = new AttributesImpl();
            attributesImpl.addAttribute("class", "inputattrtypes");
            xhtmlSaxEmitter.startElement("span", attributesImpl);
            xhtmlSaxEmitter.endElement("span");
        }
    }

    private void addText(XhtmlSaxEmitter xhtmlSaxEmitter, String text)
            throws SAXException {
        char[] ch = text.toCharArray();
        xhtmlSaxEmitter.characters(ch, 0, ch.length);
    }

    private void addHyperlink(XhtmlSaxEmitter xhtmlSaxEmitter, String text,
            String href) throws SAXException {
        AttributesImpl attributesImpl = new AttributesImpl();
        attributesImpl.addAttribute("href", href);
        xhtmlSaxEmitter.startElement("a", attributesImpl);
        addText(xhtmlSaxEmitter, text);
        xhtmlSaxEmitter.endElement("a");
    }

    private final class ExactErrorHandler implements ErrorHandler {

        private final MessageEmitterAdapter owner;

        /**
         * @param owner
         */
        ExactErrorHandler(final MessageEmitterAdapter owner) {
            this.owner = owner;
        }

        public void error(SAXParseException exception) throws SAXException {
            owner.error(exception, true);
        }

        public void fatalError(SAXParseException exception) throws SAXException {
            owner.fatalError(exception, true);
        }

        public void warning(SAXParseException exception) throws SAXException {
            owner.warning(exception, true);
        }

    }

    /**
     * Returns the exactErrorHandler.
     *
     * @return the exactErrorHandler
     */
    public ErrorHandler getExactErrorHandler() {
        return exactErrorHandler;
    }

    /**
     * Sets the spec.
     *
     * @param spec
     *            the spec to set
     */
    public void setSpec(Spec spec) {
        this.spec = spec;
    }

    /**
     * Sets the html.
     *
     * @param html
     *            the html to set
     */
    public void setHtml(boolean html) {
        this.html = html;
    }

    public void setLoggingOk(boolean ok) {
        this.loggingOk = ok;
    }

    /**
     * Sets the errorsOnly.
     *
     * @param errorsOnly
     *            the errorsOnly to set
     */
    public void setErrorsOnly(boolean errorsOnly) {
        this.errorsOnly = errorsOnly;
    }

    /**
     * @throws SAXException
     * @see nu.validator.messages.MessageEmitter#endMessage()
     */
    public void endMessage() throws SAXException {
        emitter.endMessage();
    }

    /**
     * @param type
     * @param systemId
     * @param oneBasedFirstLine
     * @param oneBasedFirstColumn
     * @param oneBasedLastLine
     * @param oneBasedLastColumn
     * @param exact
     * @throws SAXException
     * @see nu.validator.messages.MessageEmitter#startMessage(nu.validator.messages.types.MessageType,
     *      java.lang.String, int, int, int, int, boolean)
     */
    public void startMessage(MessageType type, String systemId,
            int oneBasedFirstLine, int oneBasedFirstColumn,
            int oneBasedLastLine, int oneBasedLastColumn, boolean exact)
            throws SAXException {
        emitter.startMessage(type, systemId, (oneBasedFirstLine == -1) ? -1
                : oneBasedFirstLine + lineOffset, oneBasedFirstColumn,
                (oneBasedLastLine == -1) ? -1 : oneBasedLastLine + lineOffset,
                oneBasedLastColumn, exact);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy