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

org.whattf.checker.schematronequiv.Html4Assertions Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2005 Petr Nalevka
 * Copyright (c) 2013 Mozilla Foundation
 *
 * Ported to Java from a set of Schematron assertiongs mechanically
 * extracted from RelaxNG files which had the following license:
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.whattf.checker.schematronequiv;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.whattf.checker.Checker;
import org.whattf.checker.LocatorImpl;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

public class Html4Assertions extends Checker {

    private static boolean lowerCaseLiteralEqualsIgnoreAsciiCaseString(
            String lowerCaseLiteral, String string) {
        if (string == null) {
            return false;
        }
        if (lowerCaseLiteral.length() != string.length()) {
            return false;
        }
        for (int i = 0; i < lowerCaseLiteral.length(); i++) {
            char c0 = lowerCaseLiteral.charAt(i);
            char c1 = string.charAt(i);
            if (c1 >= 'A' && c1 <= 'Z') {
                c1 += 0x20;
            }
            if (c0 != c1) {
                return false;
            }
        }
        return true;
    }

    private static boolean equalsIgnoreAsciiCase(String one, String other) {
        if (other == null) {
            if (one == null) {
                return true;
            } else {
                return false;
            }
        }
        if (one.length() != other.length()) {
            return false;
        }
        for (int i = 0; i < one.length(); i++) {
            char c0 = one.charAt(i);
            char c1 = other.charAt(i);
            if (c0 >= 'A' && c0 <= 'Z') {
                c0 += 0x20;
            }
            if (c1 >= 'A' && c1 <= 'Z') {
                c1 += 0x20;
            }
            if (c0 != c1) {
                return false;
            }
        }
        return true;
    }

    private static final String[] SPECIAL_ANCESTORS = { "a", "button", "form",
            "label", "pre" };

    private static int specialAncestorNumber(String name) {
        for (int i = 0; i < SPECIAL_ANCESTORS.length; i++) {
            if (name == SPECIAL_ANCESTORS[i]) {
                return i;
            }
        }
        return -1;
    }

    private static Map ANCESTOR_MASK_BY_DESCENDANT = new HashMap();

    private static void registerProhibitedAncestor(String ancestor,
            String descendant) {
        int number = specialAncestorNumber(ancestor);
        if (number == -1) {
            throw new IllegalStateException("Ancestor not found in array: "
                    + ancestor);
        }
        Integer maskAsObject = ANCESTOR_MASK_BY_DESCENDANT.get(descendant);
        int mask = 0;
        if (maskAsObject != null) {
            mask = maskAsObject.intValue();
        }
        mask |= (1 << number);
        ANCESTOR_MASK_BY_DESCENDANT.put(descendant, new Integer(mask));
    }

    static {
        registerProhibitedAncestor("a", "a");
        registerProhibitedAncestor("button", "a");
        registerProhibitedAncestor("button", "button");
        registerProhibitedAncestor("button", "fieldset");
        registerProhibitedAncestor("button", "form");
        registerProhibitedAncestor("button", "iframe");
        registerProhibitedAncestor("button", "input");
        registerProhibitedAncestor("button", "isindex");
        registerProhibitedAncestor("button", "select");
        registerProhibitedAncestor("button", "textarea");
        registerProhibitedAncestor("form", "form");
        registerProhibitedAncestor("label", "label");
        registerProhibitedAncestor("pre", "pre");
        registerProhibitedAncestor("pre", "img");
        registerProhibitedAncestor("pre", "object");
        registerProhibitedAncestor("pre", "applet");
        registerProhibitedAncestor("pre", "big");
        registerProhibitedAncestor("pre", "small");
        registerProhibitedAncestor("pre", "sub");
        registerProhibitedAncestor("pre", "sup");
        registerProhibitedAncestor("pre", "font");
    }

    private static final int BUTTON_MASK = (1 << specialAncestorNumber("button"));

    private static final int LABEL_FOR_MASK = (1 << 28);

    private class IdrefLocator {
        private final Locator locator;

        private final String idref;

        /**
         * @param locator
         * @param idref
         */
        public IdrefLocator(Locator locator, String idref) {
            this.locator = new LocatorImpl(locator);
            this.idref = idref;
        }

        /**
         * Returns the locator.
         *
         * @return the locator
         */
        public Locator getLocator() {
            return locator;
        }

        /**
         * Returns the idref.
         *
         * @return the idref
         */
        public String getIdref() {
            return idref;
        }
    }

    private class StackNode {
        private final int ancestorMask;

        private boolean selectedOptions = false;

        private boolean optionFound = false;

        /**
         * @param ancestorMask
         */
        public StackNode(int ancestorMask, String name, String role,
                String activeDescendant, String forAttr) {
            this.ancestorMask = ancestorMask;
        }

        /**
         * Returns the ancestorMask.
         *
         * @return the ancestorMask
         */
        public int getAncestorMask() {
            return ancestorMask;
        }

        /**
         * Returns the selectedOptions.
         *
         * @return the selectedOptions
         */
        public boolean isSelectedOptions() {
            return selectedOptions;
        }

        /**
         * Sets the selectedOptions.
         *
         * @param selectedOptions
         *            the selectedOptions to set
         */
        public void setSelectedOptions() {
            this.selectedOptions = true;
        }

        /**
         * Returns the optionFound.
         *
         * @return the optionFound
         */
        public boolean hasOption() {
            return optionFound;
        }

        /**
         * Sets the optionFound.
         */
        public void setOptionFound() {
            this.optionFound = true;
        }

    }

    private StackNode[] stack;

    private int currentPtr;

    public Html4Assertions() {
        super();
    }

    private void push(StackNode node) {
        currentPtr++;
        if (currentPtr == stack.length) {
            StackNode[] newStack = new StackNode[stack.length + 64];
            System.arraycopy(stack, 0, newStack, 0, stack.length);
            stack = newStack;
        }
        stack[currentPtr] = node;
    }

    private StackNode pop() {
        return stack[currentPtr--];
    }

    private StackNode peek() {
        return stack[currentPtr];
    }

    private Map openSingleSelects = new HashMap();

    private LinkedHashSet formControlReferences = new LinkedHashSet();

    private Set formControlIds = new HashSet();

    private LinkedHashSet listReferences = new LinkedHashSet();

    private Set listIds = new HashSet();

    private Set allIds = new HashSet();

    /**
     * @see org.whattf.checker.Checker#endDocument()
     */
    @Override public void endDocument() throws SAXException {
        // label for
        for (IdrefLocator idrefLocator : formControlReferences) {
            if (!formControlIds.contains(idrefLocator.getIdref())) {
                err("The \u201Cfor\u201D attribute of the "
                        + "\u201Clabel\u201D element must refer to "
                        + "a form control.", idrefLocator.getLocator());
            }
        }

        reset();
        stack = null;
    }

    /**
     * @see org.whattf.checker.Checker#endElement(java.lang.String,
     *      java.lang.String, java.lang.String)
     */
    @Override public void endElement(String uri, String localName, String name)
            throws SAXException {
        StackNode node = pop();
        openSingleSelects.remove(node);
        if ("http://www.w3.org/1999/xhtml" == uri) {
            if ("option" == localName && !stack[currentPtr].hasOption()) {
                stack[currentPtr].setOptionFound();
            }
        }
    }

    /**
     * @see org.whattf.checker.Checker#startDocument()
     */
    @Override public void startDocument() throws SAXException {
        reset();
        stack = new StackNode[32];
        currentPtr = 0;
        stack[0] = null;
    }

    public void reset() {
        openSingleSelects.clear();
        formControlReferences.clear();
        formControlIds.clear();
        listReferences.clear();
        listIds.clear();
        allIds.clear();
    }

    /**
     * @see org.whattf.checker.Checker#startElement(java.lang.String,
     *      java.lang.String, java.lang.String, org.xml.sax.Attributes)
     */
    @Override public void startElement(String uri, String localName,
            String name, Attributes atts) throws SAXException {
        Set ids = new HashSet();
        String role = null;
        String activeDescendant = null;
        String forAttr = null;
        boolean href = false;
        boolean hreflang = false;

        StackNode parent = peek();
        int ancestorMask = 0;
        if (parent != null) {
            ancestorMask = parent.getAncestorMask();
        }
        if ("http://www.w3.org/1999/xhtml" == uri) {
            boolean hidden = false;
            boolean usemap = false;
            boolean selected = false;
            String xmlLang = null;
            String lang = null;

            int len = atts.getLength();
            for (int i = 0; i < len; i++) {
                String attUri = atts.getURI(i);
                if (attUri.length() == 0) {
                    String attLocal = atts.getLocalName(i);
                    if ("href" == attLocal) {
                        href = true;
                    } else if ("hreflang" == attLocal) {
                        hreflang = true;
                    } else if ("lang" == attLocal) {
                        lang = atts.getValue(i);
                    } else if ("for" == attLocal && "label" == localName) {
                        forAttr = atts.getValue(i);
                        ancestorMask |= LABEL_FOR_MASK;
                    } else if ("selected" == attLocal) {
                        selected = true;
                    } else if ("usemap" == attLocal && "input" != localName) {
                        usemap = true;
                    }
                } else if ("http://www.w3.org/XML/1998/namespace" == attUri) {
                    if ("lang" == atts.getLocalName(i)) {
                        xmlLang = atts.getValue(i);
                    }
                }

                if (atts.getType(i) == "ID") {
                    String attVal = atts.getValue(i);
                    if (attVal.length() != 0) {
                        ids.add(attVal);
                    }
                }
            }

            // Exclusions
            Integer maskAsObject;
            int mask = 0;
            String descendantUiString = "";
            if ((maskAsObject = ANCESTOR_MASK_BY_DESCENDANT.get(localName)) != null) {
                mask = maskAsObject.intValue();
                descendantUiString = localName;
            } else if ("img" == localName && usemap) {
                mask = BUTTON_MASK;
                descendantUiString = "img\u201D with the attribute \u201Cusemap";
            }
            if (mask != 0) {
                int maskHit = ancestorMask & mask;
                if (maskHit != 0) {
                    for (int j = 0; j < SPECIAL_ANCESTORS.length; j++) {
                        if ((maskHit & 1) != 0) {
                            err("The element \u201C"
                                    + descendantUiString
                                    + "\u201D must not appear as a descendant "
                                    + "of the element \u201C"
                                    + SPECIAL_ANCESTORS[j] + "\u201D.");
                        }
                        maskHit >>= 1;
                    }
                }
            }

            // lang and xml:lang
            if (lang != null
                    && (xmlLang == null || !equalsIgnoreAsciiCase(lang, xmlLang))) {
                err("When attribute \u201Clang\u201D in no namespace "
                        + "is specified, attribute \u201Clang\u201D in the XML "
                        + "namespace must also be specified, and both "
                        + "attributes must have the same value.");
            }

            // label for
            if ("label" == localName) {
                String forVal = atts.getValue("", "for");
                if (forVal != null) {
                    formControlReferences.add(new IdrefLocator(new LocatorImpl(
                            getDocumentLocator()), forVal));
                }
            }
            if (("input" == localName && !hidden) || "textarea" == localName
                    || "select" == localName || "button" == localName) {
                formControlIds.addAll(ids);
            }

            // input@type=radio or input@type=checkbox
            if ("input" == localName
                    && (lowerCaseLiteralEqualsIgnoreAsciiCaseString("radio",
                            atts.getValue("", "type")) || lowerCaseLiteralEqualsIgnoreAsciiCaseString(
                            "checkbox", atts.getValue("", "type")))) {
                if (atts.getValue("", "value") == null
                        || "".equals(atts.getValue("", "value"))) {
                    err("Element \u201Cinput\u201D with attribute "
                            + "\u201Ctype\u201D whose value is \u201Cradio\u201D "
                            + "or \u201Ccheckbox\u201D "
                            + "must have non-empty attribute \u201Cvalue\u201D.");
                }
            }

            // multiple selected options
            if ("option" == localName && selected) {
                for (Map.Entry entry : openSingleSelects.entrySet()) {
                    StackNode node = entry.getKey();
                    if (node.isSelectedOptions()) {
                        err("The \u201Cselect\u201D element must not have more "
                            + "than one selected \u201Coption\u201D descendant "
                            + "unless the \u201Cmultiple\u201D attribute is specified.");
                    } else {
                        node.setSelectedOptions();
                    }
                }
            }
        }

        if ("http://www.w3.org/1999/xhtml" == uri) {
            int number = specialAncestorNumber(localName);
            if (number > -1) {
                ancestorMask |= (1 << number);
            }
            if ("a" == localName && hreflang && !href) {
                err("Element \u201Ca\u201D with attribute "
                    + "\u201Chreflang\u201D must have "
                    + "\u201Chref\u201D attribute.");
            }
            StackNode child = new StackNode(ancestorMask, localName, role,
                    activeDescendant, forAttr);
            if ("select" == localName && atts.getIndex("", "multiple") == -1) {
                openSingleSelects.put(child, getDocumentLocator());
            }
            push(child);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy