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: }