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);
}
}
}