package com.smartandroid.sa.tag.nodes; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.smartandroid.sa.tag.helper.Validate; /** * The attributes of an Element. * <p/> * Attributes are treated as a map: there can be only one value associated with * an attribute key. * <p/> * Attribute key and value comparisons are done case insensitively, and keys are * normalised to lower-case. * * @author Jonathan Hedley, jonathan@hedley.net */ public class Attributes implements Iterable<Attribute>, Cloneable { protected static final String dataPrefix = "data-"; private LinkedHashMap<String, Attribute> attributes = null; // linked hash map to preserve insertion order. // null be default as so many elements have no attributes -- saves a good // chunk of memory /** * Get an attribute value by key. * * @param key * the attribute key * @return the attribute value if set; or empty string if not set. * @see #hasKey(String) */ public String get(String key) { Validate.notEmpty(key); if (attributes == null) return ""; Attribute attr = attributes.get(key.toLowerCase()); return attr != null ? attr.getValue() : ""; } /** * Set a new attribute, or replace an existing one by key. * * @param key * attribute key * @param value * attribute value */ public void put(String key, String value) { Attribute attr = new Attribute(key, value); put(attr); } /** * Set a new attribute, or replace an existing one by key. * * @param attribute * attribute */ public void put(Attribute attribute) { Validate.notNull(attribute); if (attributes == null) attributes = new LinkedHashMap<String, Attribute>(2); attributes.put(attribute.getKey(), attribute); } /** * Remove an attribute by key. * * @param key * attribute key to remove */ public void remove(String key) { Validate.notEmpty(key); if (attributes == null) return; attributes.remove(key.toLowerCase()); } /** * 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 hasKey(String key) { return attributes != null && attributes.containsKey(key.toLowerCase()); } /** * Get the number of attributes in this set. * * @return size */ public int size() { if (attributes == null) return 0; return attributes.size(); } /** * 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; if (attributes == null) attributes = new LinkedHashMap<String, Attribute>(incoming.size()); attributes.putAll(incoming.attributes); } public Iterator<Attribute> iterator() { return asList().iterator(); } /** * Get the attributes as a List, for iteration. Do not modify the keys of * the attributes via this view, as changes to keys will not be recognised * in the containing set. * * @return an view of the attributes as a List. */ public List<Attribute> asList() { if (attributes == null) return Collections.emptyList(); List<Attribute> list = new ArrayList<Attribute>(attributes.size()); for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { list.add(entry.getValue()); } return Collections.unmodifiableList(list); } /** * 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(); } /** * Get the HTML representation of these attributes. * * @return HTML */ public String html() { StringBuilder accum = new StringBuilder(); html(accum, (new Document("")).outputSettings()); // output settings a // bit funky, but // this html() // seldom used return accum.toString(); } void html(StringBuilder accum, Document.OutputSettings out) { if (attributes == null) return; for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { Attribute attribute = entry.getValue(); accum.append(" "); attribute.html(accum, out); } } public String toString() { return html(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Attributes)) return false; Attributes that = (Attributes) o; if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) return false; return true; } @Override public int hashCode() { return attributes != null ? attributes.hashCode() : 0; } @Override public Attributes clone() { if (attributes == null) return new Attributes(); Attributes clone; try { clone = (Attributes) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } clone.attributes = new LinkedHashMap<String, Attribute>( attributes.size()); for (Attribute attribute : this) clone.attributes.put(attribute.getKey(), attribute.clone()); return clone; } private class Dataset extends AbstractMap<String, String> { private Dataset() { if (attributes == null) attributes = new LinkedHashMap<String, Attribute>(2); } public Set<Entry<String, String>> entrySet() { return new EntrySet(); } @Override public String put(String key, String value) { String dataKey = dataKey(key); String oldValue = hasKey(dataKey) ? attributes.get(dataKey) .getValue() : null; Attribute attr = new Attribute(dataKey, value); attributes.put(dataKey, attr); return oldValue; } private class EntrySet extends AbstractSet<Map.Entry<String, String>> { public Iterator<Map.Entry<String, String>> iterator() { return new DatasetIterator(); } 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.values() .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; } }