package org.jsoup.nodes;

import org.jsoup.SerializationException;
import org.jsoup.internal.StringUtil;
import org.jsoup.helper.Validate;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.jsoup.internal.Normalizer.lowerCase;

The attributes of an Element.

Attributes are treated as a map: there can be only one value associated with an attribute key/name.

Attribute name and value comparisons are generally case sensitive. By default for HTML, attribute names are normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by name.

Author:Jonathan Hedley, jonathan@hedley.net
/** * The attributes of an Element. * <p> * Attributes are treated as a map: there can be only one value associated with an attribute key/name. * </p> * <p> * Attribute name and value comparisons are generally <b>case sensitive</b>. By default for HTML, attribute names are * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by * name. * </p> * * @author Jonathan Hedley, jonathan@hedley.net */
public class Attributes implements Iterable<Attribute>, Cloneable { protected static final String dataPrefix = "data-"; private static final int InitialCapacity = 4; // todo - analyze Alexa 1MM sites, determine best setting // manages the key/val arrays private static final int GrowthFactor = 2; private static final String[] Empty = {}; static final int NotFound = -1; private static final String EmptyString = ""; private int size = 0; // number of slots used (not capacity, which is keys.length String[] keys = Empty; String[] vals = Empty; // check there's room for more private void checkCapacity(int minNewSize) { Validate.isTrue(minNewSize >= size); int curSize = keys.length; if (curSize >= minNewSize) return; int newSize = curSize >= InitialCapacity ? size * GrowthFactor : InitialCapacity; if (minNewSize > newSize) newSize = minNewSize; keys = copyOf(keys, newSize); vals = copyOf(vals, newSize); } // simple implementation of Arrays.copy, for support of Android API 8. private static String[] copyOf(String[] orig, int size) { final String[] copy = new String[size]; System.arraycopy(orig, 0, copy, 0, Math.min(orig.length, size)); return copy; } int indexOfKey(String key) { Validate.notNull(key); for (int i = 0; i < size; i++) { if (key.equals(keys[i])) return i; } return NotFound; } private int indexOfKeyIgnoreCase(String key) { Validate.notNull(key); for (int i = 0; i < size; i++) { if (key.equalsIgnoreCase(keys[i])) return i; } return NotFound; } // we track boolean attributes as null in values - they're just keys. so returns empty for consumers static String checkNotNull(String val) { return val == null ? EmptyString : val; }
Get an attribute value by key.
Params:
  • key – the (case-sensitive) attribute key
See Also:
Returns:the attribute value if set; or empty string if not set (or a boolean attribute).
/** Get an attribute value by key. @param key the (case-sensitive) attribute key @return the attribute value if set; or empty string if not set (or a boolean attribute). @see #hasKey(String) */
public String get(String key) { int i = indexOfKey(key); return i == NotFound ? EmptyString : checkNotNull(vals[i]); }
Get an attribute's value by case-insensitive key
Params:
  • key – the attribute name
Returns:the first matching attribute value if set; or empty string if not set (ora boolean attribute).
/** * Get an attribute's value by case-insensitive key * @param key the attribute name * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute). */
public String getIgnoreCase(String key) { int i = indexOfKeyIgnoreCase(key); return i == NotFound ? EmptyString : checkNotNull(vals[i]); } // adds without checking if this key exists private void add(String key, String value) { checkCapacity(size + 1); keys[size] = key; vals[size] = value; size++; }
Set a new attribute, or replace an existing one by key.
Params:
  • key – case sensitive attribute key
  • value – attribute value
Returns:these attributes, for chaining
/** * Set a new attribute, or replace an existing one by key. * @param key case sensitive attribute key * @param value attribute value * @return these attributes, for chaining */
public Attributes put(String key, String value) { int i = indexOfKey(key); if (i != NotFound) vals[i] = value; else add(key, value); return this; } void putIgnoreCase(String key, String value) { int i = indexOfKeyIgnoreCase(key); if (i != NotFound) { vals[i] = value; if (!keys[i].equals(key)) // case changed, update keys[i] = key; } else add(key, value); }
Set a new boolean attribute, remove attribute if value is false.
Params:
  • key – case insensitive attribute key
  • value – attribute value
Returns:these attributes, for chaining
/** * Set a new boolean attribute, remove attribute if value is false. * @param key case <b>insensitive</b> attribute key * @param value attribute value * @return these attributes, for chaining */
public Attributes put(String key, boolean value) { if (value) putIgnoreCase(key, null); else remove(key); return this; }
Set a new attribute, or replace an existing one by key.
Params:
  • attribute – attribute with case sensitive key
Returns:these attributes, for chaining
/** Set a new attribute, or replace an existing one by key. @param attribute attribute with case sensitive key @return these attributes, for chaining */
public Attributes put(Attribute attribute) { Validate.notNull(attribute); put(attribute.getKey(), attribute.getValue()); attribute.parent = this; return this; } // removes and shifts up private void remove(int index) { Validate.isFalse(index >= size); int shifted = size - index - 1; if (shifted > 0) { System.arraycopy(keys, index + 1, keys, index, shifted); System.arraycopy(vals, index + 1, vals, index, shifted); } size--; keys[size] = null; // release hold vals[size] = null; }
Remove an attribute by key. Case sensitive.
Params:
  • key – attribute key to remove
/** Remove an attribute by key. <b>Case sensitive.</b> @param key attribute key to remove */
public void remove(String key) { int i = indexOfKey(key); if (i != NotFound) remove(i); }
Remove an attribute by key. Case insensitive.
Params:
  • key – attribute key to remove
/** Remove an attribute by key. <b>Case insensitive.</b> @param key attribute key to remove */
public void removeIgnoreCase(String key) { int i = indexOfKeyIgnoreCase(key); if (i != NotFound) remove(i); }
Tests if these attributes contain an attribute with this key.
Params:
  • key – case-sensitive key to check for
Returns:true if key exists, false otherwise
/** Tests if these attributes contain an attribute with this key. @param key case-sensitive key to check for @return true if key exists, false otherwise */
public boolean hasKey(String key) { return indexOfKey(key) != NotFound; }
Tests if these attributes contain an attribute with this key.
Params:
  • key – key to check for
Returns:true if key exists, false otherwise
/** Tests if these attributes contain an attribute with this key. @param key key to check for @return true if key exists, false otherwise */
public boolean hasKeyIgnoreCase(String key) { return indexOfKeyIgnoreCase(key) != NotFound; }
Get the number of attributes in this set.
Returns:size
/** Get the number of attributes in this set. @return size */
public int size() { return size; }
Add all the attributes from the incoming set to this set.
Params:
  • incoming – attributes to add to these attributes.
/** Add all the attributes from the incoming set to this set. @param incoming attributes to add to these attributes. */
public void addAll(Attributes incoming) { if (incoming.size() == 0) return; checkCapacity(size + incoming.size); for (Attribute attr : incoming) { // todo - should this be case insensitive? put(attr); } } public Iterator<Attribute> iterator() { return new Iterator<Attribute>() { int i = 0; @Override public boolean hasNext() { return i < size; } @Override public Attribute next() { final Attribute attr = new Attribute(keys[i], vals[i], Attributes.this); i++; return attr; } @Override public void remove() { Attributes.this.remove(--i); // next() advanced, so rewind } }; }
Get the attributes as a List, for iteration.
Returns:an view of the attributes as an unmodifialbe List.
/** Get the attributes as a List, for iteration. @return an view of the attributes as an unmodifialbe List. */
public List<Attribute> asList() { ArrayList<Attribute> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { Attribute attr = vals[i] == null ? new BooleanAttribute(keys[i]) : // deprecated class, but maybe someone still wants it new Attribute(keys[i], vals[i], Attributes.this); list.add(attr); } return Collections.unmodifiableList(list); }
Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys starting with data-.
Returns:map of custom data attributes.
/** * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys * starting with {@code data-}. * @return map of custom data attributes. */
public Map<String, String> dataset() { return new Dataset(this); }
Get the HTML representation of these attributes.
Throws:
Returns:HTML
/** Get the HTML representation of these attributes. @return HTML @throws SerializationException if the HTML representation of the attributes cannot be constructed. */
public String html() { StringBuilder sb = StringUtil.borrowBuilder(); try { html(sb, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used } catch (IOException e) { // ought never happen throw new SerializationException(e); } return StringUtil.releaseBuilder(sb); } final void html(final Appendable accum, final Document.OutputSettings out) throws IOException { final int sz = size; for (int i = 0; i < sz; i++) { // inlined from Attribute.html() final String key = keys[i]; final String val = vals[i]; accum.append(' ').append(key); // collapse checked=null, checked="", checked=checked; write out others if (!Attribute.shouldCollapseAttribute(key, val, out)) { accum.append("=\""); Entities.escape(accum, val == null ? EmptyString : val, out, true, false, false); accum.append('"'); } } } @Override public String toString() { return html(); }
Checks if these attributes are equal to another set of attributes, by comparing the two sets
Params:
  • o – attributes to compare with
Returns:if both sets of attributes have the same content
/** * Checks if these attributes are equal to another set of attributes, by comparing the two sets * @param o attributes to compare with * @return if both sets of attributes have the same content */
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Attributes that = (Attributes) o; if (size != that.size) return false; if (!Arrays.equals(keys, that.keys)) return false; return Arrays.equals(vals, that.vals); }
Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes.
Returns:calculated hashcode
/** * Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes. * @return calculated hashcode */
@Override public int hashCode() { int result = size; result = 31 * result + Arrays.hashCode(keys); result = 31 * result + Arrays.hashCode(vals); return result; } @Override public Attributes clone() { Attributes clone; try { clone = (Attributes) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } clone.size = size; keys = copyOf(keys, size); vals = copyOf(vals, size); return clone; }
Internal method. Lowercases all keys.
/** * Internal method. Lowercases all keys. */
public void normalize() { for (int i = 0; i < size; i++) { keys[i] = lowerCase(keys[i]); } } private static class Dataset extends AbstractMap<String, String> { private final Attributes attributes; private Dataset(Attributes attributes) { this.attributes = attributes; } @Override public Set<Entry<String, String>> entrySet() { return new EntrySet(); } @Override public String put(String key, String value) { String dataKey = dataKey(key); String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null; attributes.put(dataKey, value); return oldValue; } private class EntrySet extends AbstractSet<Map.Entry<String, String>> { @Override public Iterator<Map.Entry<String, String>> iterator() { return new DatasetIterator(); } @Override public int size() { int count = 0; Iterator iter = new DatasetIterator(); while (iter.hasNext()) count++; return count; } } private class DatasetIterator implements Iterator<Map.Entry<String, String>> { private Iterator<Attribute> attrIter = attributes.iterator(); private Attribute attr; public boolean hasNext() { while (attrIter.hasNext()) { attr = attrIter.next(); if (attr.isDataAttribute()) return true; } return false; } public Entry<String, String> next() { return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue()); } public void remove() { attributes.remove(attr.getKey()); } } } private static String dataKey(String key) { return dataPrefix + key; } }