001package org.jsoup.nodes; 002 003import org.jsoup.SerializationException; 004import org.jsoup.helper.Validate; 005 006import java.io.IOException; 007import java.util.AbstractMap; 008import java.util.AbstractSet; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collections; 012import java.util.Iterator; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016 017import static org.jsoup.internal.Normalizer.lowerCase; 018 019/** 020 * The attributes of an Element. 021 * <p> 022 * Attributes are treated as a map: there can be only one value associated with an attribute key/name. 023 * </p> 024 * <p> 025 * Attribute name and value comparisons are generally <b>case sensitive</b>. By default for HTML, attribute names are 026 * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by 027 * name. 028 * </p> 029 * 030 * @author Jonathan Hedley, jonathan@hedley.net 031 */ 032public class Attributes implements Iterable<Attribute>, Cloneable { 033 protected static final String dataPrefix = "data-"; 034 private static final int InitialCapacity = 4; // todo - analyze Alexa 1MM sites, determine best setting 035 036 // manages the key/val arrays 037 private static final int GrowthFactor = 2; 038 private static final String[] Empty = {}; 039 static final int NotFound = -1; 040 private static final String EmptyString = ""; 041 042 private int size = 0; // number of slots used (not capacity, which is keys.length 043 String[] keys = Empty; 044 String[] vals = Empty; 045 046 // check there's room for more 047 private void checkCapacity(int minNewSize) { 048 Validate.isTrue(minNewSize >= size); 049 int curSize = keys.length; 050 if (curSize >= minNewSize) 051 return; 052 053 int newSize = curSize >= InitialCapacity ? size * GrowthFactor : InitialCapacity; 054 if (minNewSize > newSize) 055 newSize = minNewSize; 056 057 keys = copyOf(keys, newSize); 058 vals = copyOf(vals, newSize); 059 } 060 061 // simple implementation of Arrays.copy, for support of Android API 8. 062 private static String[] copyOf(String[] orig, int size) { 063 final String[] copy = new String[size]; 064 System.arraycopy(orig, 0, copy, 0, 065 Math.min(orig.length, size)); 066 return copy; 067 } 068 069 int indexOfKey(String key) { 070 Validate.notNull(key); 071 for (int i = 0; i < size; i++) { 072 if (key.equals(keys[i])) 073 return i; 074 } 075 return NotFound; 076 } 077 078 private int indexOfKeyIgnoreCase(String key) { 079 Validate.notNull(key); 080 for (int i = 0; i < size; i++) { 081 if (key.equalsIgnoreCase(keys[i])) 082 return i; 083 } 084 return NotFound; 085 } 086 087 // we track boolean attributes as null in values - they're just keys. so returns empty for consumers 088 static String checkNotNull(String val) { 089 return val == null ? EmptyString : val; 090 } 091 092 /** 093 Get an attribute value by key. 094 @param key the (case-sensitive) attribute key 095 @return the attribute value if set; or empty string if not set (or a boolean attribute). 096 @see #hasKey(String) 097 */ 098 public String get(String key) { 099 int i = indexOfKey(key); 100 return i == NotFound ? EmptyString : checkNotNull(vals[i]); 101 } 102 103 /** 104 * Get an attribute's value by case-insensitive key 105 * @param key the attribute name 106 * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute). 107 */ 108 public String getIgnoreCase(String key) { 109 int i = indexOfKeyIgnoreCase(key); 110 return i == NotFound ? EmptyString : checkNotNull(vals[i]); 111 } 112 113 // adds without checking if this key exists 114 private void add(String key, String value) { 115 checkCapacity(size + 1); 116 keys[size] = key; 117 vals[size] = value; 118 size++; 119 } 120 121 /** 122 * Set a new attribute, or replace an existing one by key. 123 * @param key case sensitive attribute key 124 * @param value attribute value 125 * @return these attributes, for chaining 126 */ 127 public Attributes put(String key, String value) { 128 int i = indexOfKey(key); 129 if (i != NotFound) 130 vals[i] = value; 131 else 132 add(key, value); 133 return this; 134 } 135 136 void putIgnoreCase(String key, String value) { 137 int i = indexOfKeyIgnoreCase(key); 138 if (i != NotFound) { 139 vals[i] = value; 140 if (!keys[i].equals(key)) // case changed, update 141 keys[i] = key; 142 } 143 else 144 add(key, value); 145 } 146 147 /** 148 * Set a new boolean attribute, remove attribute if value is false. 149 * @param key case <b>insensitive</b> attribute key 150 * @param value attribute value 151 * @return these attributes, for chaining 152 */ 153 public Attributes put(String key, boolean value) { 154 if (value) 155 putIgnoreCase(key, null); 156 else 157 remove(key); 158 return this; 159 } 160 161 /** 162 Set a new attribute, or replace an existing one by key. 163 @param attribute attribute with case sensitive key 164 @return these attributes, for chaining 165 */ 166 public Attributes put(Attribute attribute) { 167 Validate.notNull(attribute); 168 put(attribute.getKey(), attribute.getValue()); 169 attribute.parent = this; 170 return this; 171 } 172 173 // removes and shifts up 174 private void remove(int index) { 175 Validate.isFalse(index >= size); 176 int shifted = size - index - 1; 177 if (shifted > 0) { 178 System.arraycopy(keys, index + 1, keys, index, shifted); 179 System.arraycopy(vals, index + 1, vals, index, shifted); 180 } 181 size--; 182 keys[size] = null; // release hold 183 vals[size] = null; 184 } 185 186 /** 187 Remove an attribute by key. <b>Case sensitive.</b> 188 @param key attribute key to remove 189 */ 190 public void remove(String key) { 191 int i = indexOfKey(key); 192 if (i != NotFound) 193 remove(i); 194 } 195 196 /** 197 Remove an attribute by key. <b>Case insensitive.</b> 198 @param key attribute key to remove 199 */ 200 public void removeIgnoreCase(String key) { 201 int i = indexOfKeyIgnoreCase(key); 202 if (i != NotFound) 203 remove(i); 204 } 205 206 /** 207 Tests if these attributes contain an attribute with this key. 208 @param key case-sensitive key to check for 209 @return true if key exists, false otherwise 210 */ 211 public boolean hasKey(String key) { 212 return indexOfKey(key) != NotFound; 213 } 214 215 /** 216 Tests if these attributes contain an attribute with this key. 217 @param key key to check for 218 @return true if key exists, false otherwise 219 */ 220 public boolean hasKeyIgnoreCase(String key) { 221 return indexOfKeyIgnoreCase(key) != NotFound; 222 } 223 224 /** 225 Get the number of attributes in this set. 226 @return size 227 */ 228 public int size() { 229 return size; 230 } 231 232 /** 233 Add all the attributes from the incoming set to this set. 234 @param incoming attributes to add to these attributes. 235 */ 236 public void addAll(Attributes incoming) { 237 if (incoming.size() == 0) 238 return; 239 checkCapacity(size + incoming.size); 240 241 for (Attribute attr : incoming) { 242 // todo - should this be case insensitive? 243 put(attr); 244 } 245 246 } 247 248 public Iterator<Attribute> iterator() { 249 return new Iterator<Attribute>() { 250 int i = 0; 251 252 @Override 253 public boolean hasNext() { 254 return i < size; 255 } 256 257 @Override 258 public Attribute next() { 259 final Attribute attr = new Attribute(keys[i], vals[i], Attributes.this); 260 i++; 261 return attr; 262 } 263 264 @Override 265 public void remove() { 266 Attributes.this.remove(--i); // next() advanced, so rewind 267 } 268 }; 269 } 270 271 /** 272 Get the attributes as a List, for iteration. 273 @return an view of the attributes as an unmodifialbe List. 274 */ 275 public List<Attribute> asList() { 276 ArrayList<Attribute> list = new ArrayList<>(size); 277 for (int i = 0; i < size; i++) { 278 Attribute attr = vals[i] == null ? 279 new BooleanAttribute(keys[i]) : // deprecated class, but maybe someone still wants it 280 new Attribute(keys[i], vals[i], Attributes.this); 281 list.add(attr); 282 } 283 return Collections.unmodifiableList(list); 284 } 285 286 /** 287 * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys 288 * starting with {@code data-}. 289 * @return map of custom data attributes. 290 */ 291 public Map<String, String> dataset() { 292 return new Dataset(this); 293 } 294 295 /** 296 Get the HTML representation of these attributes. 297 @return HTML 298 @throws SerializationException if the HTML representation of the attributes cannot be constructed. 299 */ 300 public String html() { 301 StringBuilder accum = new StringBuilder(); 302 try { 303 html(accum, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used 304 } catch (IOException e) { // ought never happen 305 throw new SerializationException(e); 306 } 307 return accum.toString(); 308 } 309 310 final void html(final Appendable accum, final Document.OutputSettings out) throws IOException { 311 final int sz = size; 312 for (int i = 0; i < sz; i++) { 313 // inlined from Attribute.html() 314 final String key = keys[i]; 315 final String val = vals[i]; 316 accum.append(' ').append(key); 317 318 // collapse checked=null, checked="", checked=checked; write out others 319 if (!(out.syntax() == Document.OutputSettings.Syntax.html 320 && (val == null || val.equals(key) && Attribute.isBooleanAttribute(key)))) { 321 322 accum.append("=\""); 323 Entities.escape(accum, val == null ? EmptyString : val, out, true, false, false); 324 accum.append('"'); 325 } 326 } 327 } 328 329 @Override 330 public String toString() { 331 return html(); 332 } 333 334 /** 335 * Checks if these attributes are equal to another set of attributes, by comparing the two sets 336 * @param o attributes to compare with 337 * @return if both sets of attributes have the same content 338 */ 339 @Override 340 public boolean equals(Object o) { 341 if (this == o) return true; 342 if (o == null || getClass() != o.getClass()) return false; 343 344 Attributes that = (Attributes) o; 345 346 if (size != that.size) return false; 347 if (!Arrays.equals(keys, that.keys)) return false; 348 return Arrays.equals(vals, that.vals); 349 } 350 351 /** 352 * Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes. 353 * @return calculated hashcode 354 */ 355 @Override 356 public int hashCode() { 357 int result = size; 358 result = 31 * result + Arrays.hashCode(keys); 359 result = 31 * result + Arrays.hashCode(vals); 360 return result; 361 } 362 363 @Override 364 public Attributes clone() { 365 Attributes clone; 366 try { 367 clone = (Attributes) super.clone(); 368 } catch (CloneNotSupportedException e) { 369 throw new RuntimeException(e); 370 } 371 clone.size = size; 372 keys = copyOf(keys, size); 373 vals = copyOf(vals, size); 374 return clone; 375 } 376 377 /** 378 * Internal method. Lowercases all keys. 379 */ 380 public void normalize() { 381 for (int i = 0; i < size; i++) { 382 keys[i] = lowerCase(keys[i]); 383 } 384 } 385 386 private static class Dataset extends AbstractMap<String, String> { 387 private final Attributes attributes; 388 389 private Dataset(Attributes attributes) { 390 this.attributes = attributes; 391 } 392 393 @Override 394 public Set<Entry<String, String>> entrySet() { 395 return new EntrySet(); 396 } 397 398 @Override 399 public String put(String key, String value) { 400 String dataKey = dataKey(key); 401 String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null; 402 attributes.put(dataKey, value); 403 return oldValue; 404 } 405 406 private class EntrySet extends AbstractSet<Map.Entry<String, String>> { 407 408 @Override 409 public Iterator<Map.Entry<String, String>> iterator() { 410 return new DatasetIterator(); 411 } 412 413 @Override 414 public int size() { 415 int count = 0; 416 Iterator iter = new DatasetIterator(); 417 while (iter.hasNext()) 418 count++; 419 return count; 420 } 421 } 422 423 private class DatasetIterator implements Iterator<Map.Entry<String, String>> { 424 private Iterator<Attribute> attrIter = attributes.iterator(); 425 private Attribute attr; 426 public boolean hasNext() { 427 while (attrIter.hasNext()) { 428 attr = attrIter.next(); 429 if (attr.isDataAttribute()) return true; 430 } 431 return false; 432 } 433 434 public Entry<String, String> next() { 435 return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue()); 436 } 437 438 public void remove() { 439 attributes.remove(attr.getKey()); 440 } 441 } 442 } 443 444 private static String dataKey(String key) { 445 return dataPrefix + key; 446 } 447}