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: - SerializationException – if the HTML representation of the attributes cannot be constructed.
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;
}
}