michael@0: /* michael@0: * Copyright (c) 2007 Henri Sivonen michael@0: * Copyright (c) 2008-2011 Mozilla Foundation michael@0: * michael@0: * Permission is hereby granted, free of charge, to any person obtaining a michael@0: * copy of this software and associated documentation files (the "Software"), michael@0: * to deal in the Software without restriction, including without limitation michael@0: * the rights to use, copy, modify, merge, publish, distribute, sublicense, michael@0: * and/or sell copies of the Software, and to permit persons to whom the michael@0: * Software is furnished to do so, subject to the following conditions: michael@0: * michael@0: * The above copyright notice and this permission notice shall be included in michael@0: * all copies or substantial portions of the Software. michael@0: * michael@0: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR michael@0: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, michael@0: * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL michael@0: * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER michael@0: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING michael@0: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER michael@0: * DEALINGS IN THE SOFTWARE. michael@0: */ michael@0: michael@0: package nu.validator.htmlparser.impl; michael@0: michael@0: import nu.validator.htmlparser.annotation.Auto; michael@0: import nu.validator.htmlparser.annotation.IdType; michael@0: import nu.validator.htmlparser.annotation.Local; michael@0: import nu.validator.htmlparser.annotation.NsUri; michael@0: import nu.validator.htmlparser.annotation.Prefix; michael@0: import nu.validator.htmlparser.annotation.QName; michael@0: import nu.validator.htmlparser.common.Interner; michael@0: import nu.validator.htmlparser.common.XmlViolationPolicy; michael@0: michael@0: import org.xml.sax.Attributes; michael@0: import org.xml.sax.SAXException; michael@0: michael@0: /** michael@0: * Be careful with this class. QName is the name in from HTML tokenization. michael@0: * Otherwise, please refer to the interface doc. michael@0: * michael@0: * @version $Id: AttributesImpl.java 206 2008-03-20 14:09:29Z hsivonen $ michael@0: * @author hsivonen michael@0: */ michael@0: public final class HtmlAttributes implements Attributes { michael@0: michael@0: // [NOCPP[ michael@0: michael@0: private static final AttributeName[] EMPTY_ATTRIBUTENAMES = new AttributeName[0]; michael@0: michael@0: private static final String[] EMPTY_STRINGS = new String[0]; michael@0: michael@0: // ]NOCPP] michael@0: michael@0: public static final HtmlAttributes EMPTY_ATTRIBUTES = new HtmlAttributes( michael@0: AttributeName.HTML); michael@0: michael@0: private int mode; michael@0: michael@0: private int length; michael@0: michael@0: private @Auto AttributeName[] names; michael@0: michael@0: private @Auto String[] values; // XXX perhaps make this @NoLength? michael@0: michael@0: // [NOCPP[ michael@0: michael@0: private String idValue; michael@0: michael@0: private int xmlnsLength; michael@0: michael@0: private AttributeName[] xmlnsNames; michael@0: michael@0: private String[] xmlnsValues; michael@0: michael@0: // ]NOCPP] michael@0: michael@0: public HtmlAttributes(int mode) { michael@0: this.mode = mode; michael@0: this.length = 0; michael@0: /* michael@0: * The length of 5 covers covers 98.3% of elements michael@0: * according to Hixie michael@0: */ michael@0: this.names = new AttributeName[5]; michael@0: this.values = new String[5]; michael@0: michael@0: // [NOCPP[ michael@0: michael@0: this.idValue = null; michael@0: michael@0: this.xmlnsLength = 0; michael@0: michael@0: this.xmlnsNames = HtmlAttributes.EMPTY_ATTRIBUTENAMES; michael@0: michael@0: this.xmlnsValues = HtmlAttributes.EMPTY_STRINGS; michael@0: michael@0: // ]NOCPP] michael@0: } michael@0: /* michael@0: public HtmlAttributes(HtmlAttributes other) { michael@0: this.mode = other.mode; michael@0: this.length = other.length; michael@0: this.names = new AttributeName[other.length]; michael@0: this.values = new String[other.length]; michael@0: // [NOCPP[ michael@0: this.idValue = other.idValue; michael@0: this.xmlnsLength = other.xmlnsLength; michael@0: this.xmlnsNames = new AttributeName[other.xmlnsLength]; michael@0: this.xmlnsValues = new String[other.xmlnsLength]; michael@0: // ]NOCPP] michael@0: } michael@0: */ michael@0: michael@0: void destructor() { michael@0: clear(0); michael@0: } michael@0: michael@0: /** michael@0: * Only use with a static argument michael@0: * michael@0: * @param name michael@0: * @return michael@0: */ michael@0: public int getIndex(AttributeName name) { michael@0: for (int i = 0; i < length; i++) { michael@0: if (names[i] == name) { michael@0: return i; michael@0: } michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: /** michael@0: * Only use with static argument. michael@0: * michael@0: * @see org.xml.sax.Attributes#getValue(java.lang.String) michael@0: */ michael@0: public String getValue(AttributeName name) { michael@0: int index = getIndex(name); michael@0: if (index == -1) { michael@0: return null; michael@0: } else { michael@0: return getValueNoBoundsCheck(index); michael@0: } michael@0: } michael@0: michael@0: public int getLength() { michael@0: return length; michael@0: } michael@0: michael@0: /** michael@0: * Variant of getLocalName(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the local name at index michael@0: */ michael@0: public @Local String getLocalNameNoBoundsCheck(int index) { michael@0: // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; michael@0: return names[index].getLocal(mode); michael@0: } michael@0: michael@0: /** michael@0: * Variant of getURI(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the namespace URI at index michael@0: */ michael@0: public @NsUri String getURINoBoundsCheck(int index) { michael@0: // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; michael@0: return names[index].getUri(mode); michael@0: } michael@0: michael@0: /** michael@0: * Variant of getPrefix(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the namespace prefix at index michael@0: */ michael@0: public @Prefix String getPrefixNoBoundsCheck(int index) { michael@0: // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; michael@0: return names[index].getPrefix(mode); michael@0: } michael@0: michael@0: /** michael@0: * Variant of getValue(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the attribute value at index michael@0: */ michael@0: public String getValueNoBoundsCheck(int index) { michael@0: // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; michael@0: return values[index]; michael@0: } michael@0: michael@0: /** michael@0: * Variant of getAttributeName(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the attribute name at index michael@0: */ michael@0: public AttributeName getAttributeNameNoBoundsCheck(int index) { michael@0: // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; michael@0: return names[index]; michael@0: } michael@0: michael@0: // [NOCPP[ michael@0: michael@0: /** michael@0: * Variant of getQName(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the QName at index michael@0: */ michael@0: public @QName String getQNameNoBoundsCheck(int index) { michael@0: return names[index].getQName(mode); michael@0: } michael@0: michael@0: /** michael@0: * Variant of getType(int index) without bounds check. michael@0: * @param index a valid attribute index michael@0: * @return the attribute type at index michael@0: */ michael@0: public @IdType String getTypeNoBoundsCheck(int index) { michael@0: return (names[index] == AttributeName.ID) ? "ID" : "CDATA"; michael@0: } michael@0: michael@0: public int getIndex(String qName) { michael@0: for (int i = 0; i < length; i++) { michael@0: if (names[i].getQName(mode).equals(qName)) { michael@0: return i; michael@0: } michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: public int getIndex(String uri, String localName) { michael@0: for (int i = 0; i < length; i++) { michael@0: if (names[i].getLocal(mode).equals(localName) michael@0: && names[i].getUri(mode).equals(uri)) { michael@0: return i; michael@0: } michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: public @IdType String getType(String qName) { michael@0: int index = getIndex(qName); michael@0: if (index == -1) { michael@0: return null; michael@0: } else { michael@0: return getType(index); michael@0: } michael@0: } michael@0: michael@0: public @IdType String getType(String uri, String localName) { michael@0: int index = getIndex(uri, localName); michael@0: if (index == -1) { michael@0: return null; michael@0: } else { michael@0: return getType(index); michael@0: } michael@0: } michael@0: michael@0: public String getValue(String qName) { michael@0: int index = getIndex(qName); michael@0: if (index == -1) { michael@0: return null; michael@0: } else { michael@0: return getValue(index); michael@0: } michael@0: } michael@0: michael@0: public String getValue(String uri, String localName) { michael@0: int index = getIndex(uri, localName); michael@0: if (index == -1) { michael@0: return null; michael@0: } else { michael@0: return getValue(index); michael@0: } michael@0: } michael@0: michael@0: public @Local String getLocalName(int index) { michael@0: if (index < length && index >= 0) { michael@0: return names[index].getLocal(mode); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public @QName String getQName(int index) { michael@0: if (index < length && index >= 0) { michael@0: return names[index].getQName(mode); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public @IdType String getType(int index) { michael@0: if (index < length && index >= 0) { michael@0: return (names[index] == AttributeName.ID) ? "ID" : "CDATA"; michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public AttributeName getAttributeName(int index) { michael@0: if (index < length && index >= 0) { michael@0: return names[index]; michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public @NsUri String getURI(int index) { michael@0: if (index < length && index >= 0) { michael@0: return names[index].getUri(mode); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public @Prefix String getPrefix(int index) { michael@0: if (index < length && index >= 0) { michael@0: return names[index].getPrefix(mode); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public String getValue(int index) { michael@0: if (index < length && index >= 0) { michael@0: return values[index]; michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public String getId() { michael@0: return idValue; michael@0: } michael@0: michael@0: public int getXmlnsLength() { michael@0: return xmlnsLength; michael@0: } michael@0: michael@0: public @Local String getXmlnsLocalName(int index) { michael@0: if (index < xmlnsLength && index >= 0) { michael@0: return xmlnsNames[index].getLocal(mode); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public @NsUri String getXmlnsURI(int index) { michael@0: if (index < xmlnsLength && index >= 0) { michael@0: return xmlnsNames[index].getUri(mode); michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public String getXmlnsValue(int index) { michael@0: if (index < xmlnsLength && index >= 0) { michael@0: return xmlnsValues[index]; michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public int getXmlnsIndex(AttributeName name) { michael@0: for (int i = 0; i < xmlnsLength; i++) { michael@0: if (xmlnsNames[i] == name) { michael@0: return i; michael@0: } michael@0: } michael@0: return -1; michael@0: } michael@0: michael@0: public String getXmlnsValue(AttributeName name) { michael@0: int index = getXmlnsIndex(name); michael@0: if (index == -1) { michael@0: return null; michael@0: } else { michael@0: return getXmlnsValue(index); michael@0: } michael@0: } michael@0: michael@0: public AttributeName getXmlnsAttributeName(int index) { michael@0: if (index < xmlnsLength && index >= 0) { michael@0: return xmlnsNames[index]; michael@0: } else { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: // ]NOCPP] michael@0: michael@0: void addAttribute(AttributeName name, String value michael@0: // [NOCPP[ michael@0: , XmlViolationPolicy xmlnsPolicy michael@0: // ]NOCPP] michael@0: ) throws SAXException { michael@0: // [NOCPP[ michael@0: if (name == AttributeName.ID) { michael@0: idValue = value; michael@0: } michael@0: michael@0: if (name.isXmlns()) { michael@0: if (xmlnsNames.length == xmlnsLength) { michael@0: int newLen = xmlnsLength == 0 ? 2 : xmlnsLength << 1; michael@0: AttributeName[] newNames = new AttributeName[newLen]; michael@0: System.arraycopy(xmlnsNames, 0, newNames, 0, xmlnsNames.length); michael@0: xmlnsNames = newNames; michael@0: String[] newValues = new String[newLen]; michael@0: System.arraycopy(xmlnsValues, 0, newValues, 0, xmlnsValues.length); michael@0: xmlnsValues = newValues; michael@0: } michael@0: xmlnsNames[xmlnsLength] = name; michael@0: xmlnsValues[xmlnsLength] = value; michael@0: xmlnsLength++; michael@0: switch (xmlnsPolicy) { michael@0: case FATAL: michael@0: // this is ugly michael@0: throw new SAXException("Saw an xmlns attribute."); michael@0: case ALTER_INFOSET: michael@0: return; michael@0: case ALLOW: michael@0: // fall through michael@0: } michael@0: } michael@0: michael@0: // ]NOCPP] michael@0: michael@0: if (names.length == length) { michael@0: int newLen = length << 1; // The first growth covers virtually michael@0: // 100% of elements according to michael@0: // Hixie michael@0: AttributeName[] newNames = new AttributeName[newLen]; michael@0: System.arraycopy(names, 0, newNames, 0, names.length); michael@0: names = newNames; michael@0: String[] newValues = new String[newLen]; michael@0: System.arraycopy(values, 0, newValues, 0, values.length); michael@0: values = newValues; michael@0: } michael@0: names[length] = name; michael@0: values[length] = value; michael@0: length++; michael@0: } michael@0: michael@0: void clear(int m) { michael@0: for (int i = 0; i < length; i++) { michael@0: names[i].release(); michael@0: names[i] = null; michael@0: Portability.releaseString(values[i]); michael@0: values[i] = null; michael@0: } michael@0: length = 0; michael@0: mode = m; michael@0: // [NOCPP[ michael@0: idValue = null; michael@0: for (int i = 0; i < xmlnsLength; i++) { michael@0: xmlnsNames[i] = null; michael@0: xmlnsValues[i] = null; michael@0: } michael@0: xmlnsLength = 0; michael@0: // ]NOCPP] michael@0: } michael@0: michael@0: /** michael@0: * This is used in C++ to release special isindex michael@0: * attribute values whose ownership is not transferred. michael@0: */ michael@0: void releaseValue(int i) { michael@0: Portability.releaseString(values[i]); michael@0: } michael@0: michael@0: /** michael@0: * This is only used for AttributeName ownership transfer michael@0: * in the isindex case to avoid freeing custom names twice in C++. michael@0: */ michael@0: void clearWithoutReleasingContents() { michael@0: for (int i = 0; i < length; i++) { michael@0: names[i] = null; michael@0: values[i] = null; michael@0: } michael@0: length = 0; michael@0: } michael@0: michael@0: boolean contains(AttributeName name) { michael@0: for (int i = 0; i < length; i++) { michael@0: if (name.equalsAnother(names[i])) { michael@0: return true; michael@0: } michael@0: } michael@0: // [NOCPP[ michael@0: for (int i = 0; i < xmlnsLength; i++) { michael@0: if (name.equalsAnother(xmlnsNames[i])) { michael@0: return true; michael@0: } michael@0: } michael@0: // ]NOCPP] michael@0: return false; michael@0: } michael@0: michael@0: public void adjustForMath() { michael@0: mode = AttributeName.MATHML; michael@0: } michael@0: michael@0: public void adjustForSvg() { michael@0: mode = AttributeName.SVG; michael@0: } michael@0: michael@0: public HtmlAttributes cloneAttributes(Interner interner) michael@0: throws SAXException { michael@0: assert (length == 0 michael@0: // [NOCPP[ michael@0: && xmlnsLength == 0 michael@0: // ]NOCPP] michael@0: ) michael@0: || mode == 0 || mode == 3; michael@0: HtmlAttributes clone = new HtmlAttributes(0); michael@0: for (int i = 0; i < length; i++) { michael@0: clone.addAttribute(names[i].cloneAttributeName(interner), michael@0: Portability.newStringFromString(values[i]) michael@0: // [NOCPP[ michael@0: , XmlViolationPolicy.ALLOW michael@0: // ]NOCPP] michael@0: ); michael@0: } michael@0: // [NOCPP[ michael@0: for (int i = 0; i < xmlnsLength; i++) { michael@0: clone.addAttribute(xmlnsNames[i], xmlnsValues[i], michael@0: XmlViolationPolicy.ALLOW); michael@0: } michael@0: // ]NOCPP] michael@0: return clone; // XXX!!! michael@0: } michael@0: michael@0: public boolean equalsAnother(HtmlAttributes other) { michael@0: assert mode == 0 || mode == 3 : "Trying to compare attributes in foreign content."; michael@0: int otherLength = other.getLength(); michael@0: if (length != otherLength) { michael@0: return false; michael@0: } michael@0: for (int i = 0; i < length; i++) { michael@0: // Work around the limitations of C++ michael@0: boolean found = false; michael@0: // The comparing just the local names is OK, since these attribute michael@0: // holders are both supposed to belong to HTML formatting elements michael@0: @Local String ownLocal = names[i].getLocal(AttributeName.HTML); michael@0: for (int j = 0; j < otherLength; j++) { michael@0: if (ownLocal == other.names[j].getLocal(AttributeName.HTML)) { michael@0: found = true; michael@0: if (!Portability.stringEqualsString(values[i], other.values[j])) { michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: if (!found) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: // [NOCPP[ michael@0: michael@0: void processNonNcNames(TreeBuilder treeBuilder, XmlViolationPolicy namePolicy) throws SAXException { michael@0: for (int i = 0; i < length; i++) { michael@0: AttributeName attName = names[i]; michael@0: if (!attName.isNcName(mode)) { michael@0: String name = attName.getLocal(mode); michael@0: switch (namePolicy) { michael@0: case ALTER_INFOSET: michael@0: names[i] = AttributeName.create(NCName.escapeName(name)); michael@0: // fall through michael@0: case ALLOW: michael@0: if (attName != AttributeName.XML_LANG) { michael@0: treeBuilder.warn("Attribute \u201C" + name + "\u201D is not serializable as XML 1.0."); michael@0: } michael@0: break; michael@0: case FATAL: michael@0: treeBuilder.fatal("Attribute \u201C" + name + "\u201D is not serializable as XML 1.0."); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: public void merge(HtmlAttributes attributes) throws SAXException { michael@0: int len = attributes.getLength(); michael@0: for (int i = 0; i < len; i++) { michael@0: AttributeName name = attributes.getAttributeNameNoBoundsCheck(i); michael@0: if (!contains(name)) { michael@0: addAttribute(name, attributes.getValueNoBoundsCheck(i), XmlViolationPolicy.ALLOW); michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: // ]NOCPP] michael@0: michael@0: }