001package org.jsoup.nodes; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.internal.QuietAppendable; 005import org.jsoup.internal.SharedConstants; 006import org.jsoup.internal.StringUtil; 007import org.jsoup.parser.ParseSettings; 008import org.jspecify.annotations.Nullable; 009 010import java.util.AbstractMap; 011import java.util.AbstractSet; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collections; 015import java.util.ConcurrentModificationException; 016import java.util.HashMap; 017import java.util.Iterator; 018import java.util.List; 019import java.util.Map; 020import java.util.NoSuchElementException; 021import java.util.Objects; 022import java.util.Set; 023 024import static org.jsoup.internal.Normalizer.lowerCase; 025import static org.jsoup.nodes.Range.AttributeRange.UntrackedAttr; 026 027/** 028 * The attributes of an Element. 029 * <p> 030 * During parsing, attributes in with the same name in an element are deduplicated, according to the configured parser's 031 * attribute case-sensitive setting. It is possible to have duplicate attributes subsequently if 032 * {@link #add(String, String)} vs {@link #put(String, String)} is used. 033 * </p> 034 * <p> 035 * Attribute name and value comparisons are generally <b>case sensitive</b>. By default for HTML, attribute names are 036 * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by 037 * name. 038 * </p> 039 * 040 * @author Jonathan Hedley, jonathan@hedley.net 041 */ 042public class Attributes implements Iterable<Attribute>, Cloneable { 043 // The Attributes object is only created on the first use of an attribute; the Element will just have a null 044 // Attribute slot otherwise 045 046 static final char InternalPrefix = '/'; // Indicates an internal key. Can't be set via HTML. (It could be set via accessor, but not too worried about that. Suppressed from list, iter, size.) 047 protected static final String dataPrefix = "data-"; // data attributes 048 private static final String EmptyString = ""; 049 050 // manages the key/val arrays 051 private static final int InitialCapacity = 3; // sampling found mean count when attrs present = 1.49; 1.08 overall. 2.6:1 don't have any attrs. 052 private static final int GrowthFactor = 2; 053 static final int NotFound = -1; 054 055 // the number of instance fields is kept as low as possible giving an object size of 24 bytes 056 int size = 0; // number of slots used (not total capacity, which is keys.length). Package visible for actual size (incl internal) 057 @Nullable String[] keys = new String[InitialCapacity]; // keys is not null, but contents may be. Same for vals 058 @Nullable Object[] vals = new Object[InitialCapacity]; // Genericish: all non-internal attribute values must be Strings and are cast on access. 059 // todo - make keys iterable without creating Attribute objects 060 061 // check there's room for more 062 private void checkCapacity(int minNewSize) { 063 Validate.isTrue(minNewSize >= size); 064 int curCap = keys.length; 065 if (curCap >= minNewSize) 066 return; 067 int newCap = curCap >= InitialCapacity ? size * GrowthFactor : InitialCapacity; 068 if (minNewSize > newCap) 069 newCap = minNewSize; 070 071 keys = Arrays.copyOf(keys, newCap); 072 vals = Arrays.copyOf(vals, newCap); 073 } 074 075 int indexOfKey(String key) { 076 Validate.notNull(key); 077 for (int i = 0; i < size; i++) { 078 if (key.equals(keys[i])) 079 return i; 080 } 081 return NotFound; 082 } 083 084 /** 085 Finds a visible attribute's range index, skipping internal metadata slots. 086 */ 087 int visibleIndexOfKey(String key) { 088 Validate.notNull(key); 089 int visible = 0; 090 for (int i = 0; i < size; i++) { 091 String attrKey = keys[i]; 092 if (isInternalKey(attrKey)) 093 continue; 094 if (key.equals(attrKey)) 095 return visible; 096 visible++; 097 } 098 return NotFound; 099 } 100 101 /** 102 Maps an attribute array slot to the matching visible attribute index. 103 */ 104 private int visibleIndex(int index) { 105 int visible = 0; 106 for (int i = 0; i < index; i++) { 107 if (!isInternalKey(keys[i])) 108 visible++; 109 } 110 return visible; 111 } 112 113 private int indexOfKeyIgnoreCase(String key) { 114 Validate.notNull(key); 115 for (int i = 0; i < size; i++) { 116 if (key.equalsIgnoreCase(keys[i])) 117 return i; 118 } 119 return NotFound; 120 } 121 122 /** 123 Boolean attributes have null values, and internal attributes may hold arbitrary objects; return empty for either. 124 */ 125 static String checkNotNull(@Nullable Object val) { 126 return val instanceof String ? (String) val : EmptyString; 127 } 128 129 /** 130 Get an attribute value by key. 131 @param key the (case-sensitive) attribute key 132 @return the attribute value if set; or empty string if not set (or a boolean attribute). 133 @see #hasKey(String) 134 */ 135 public String get(String key) { 136 int i = indexOfKey(key); 137 return i == NotFound ? EmptyString : checkNotNull(vals[i]); 138 } 139 140 /** 141 Get an Attribute by key. The Attribute will remain connected to these Attributes, so changes made via 142 {@link Attribute#setKey(String)}, {@link Attribute#setValue(String)} etc will cascade back to these Attributes and 143 their owning Element. 144 @param key the (case-sensitive) attribute key 145 @return the Attribute for this key, or null if not present. 146 @since 1.17.2 147 */ 148 @Nullable public Attribute attribute(String key) { 149 int i = indexOfKey(key); 150 return i == NotFound ? null : new Attribute(key, checkNotNull(vals[i]), this); 151 } 152 153 /** 154 * Get an attribute's value by case-insensitive key 155 * @param key the attribute name 156 * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute). 157 */ 158 public String getIgnoreCase(String key) { 159 int i = indexOfKeyIgnoreCase(key); 160 return i == NotFound ? EmptyString : checkNotNull(vals[i]); 161 } 162 163 /** 164 * Adds a new attribute. Will produce duplicates if the key already exists. 165 * @see Attributes#put(String, String) 166 */ 167 public Attributes add(String key, @Nullable String value) { 168 addObject(key, value); 169 return this; 170 } 171 172 private void addObject(String key, @Nullable Object value) { 173 checkCapacity(size + 1); 174 keys[size] = key; 175 vals[size] = value; 176 size++; 177 } 178 179 /** 180 * Set a new attribute, or replace an existing one by key. 181 * @param key case sensitive attribute key (not null) 182 * @param value attribute value (which can be null, to set a true boolean attribute) 183 * @return these attributes, for chaining 184 */ 185 public Attributes put(String key, @Nullable String value) { 186 Validate.notNull(key); 187 int i = indexOfKey(key); 188 if (i != NotFound) 189 vals[i] = value; 190 else 191 addObject(key, value); 192 return this; 193 } 194 195 /** 196 Get the map holding any user-data associated with these Attributes. Will be created empty on first use. Held as 197 an internal attribute, not a field member, to reduce the memory footprint of Attributes when not used. Can hold 198 arbitrary objects; use for connecting W3C nodes to Elements, etc. 199 * @return the map holding user-data 200 */ 201 @SuppressWarnings("unchecked") 202 Map<String, Object> userData() { 203 final Map<String, Object> userData; 204 int i = indexOfKey(SharedConstants.UserDataKey); 205 if (i == NotFound) { 206 userData = new HashMap<>(); 207 addObject(SharedConstants.UserDataKey, userData); 208 } else { 209 userData = (Map<String, Object>) vals[i]; 210 } 211 assert userData != null; 212 return userData; 213 } 214 215 /** 216 Check if these attributes have any user data associated with them. 217 */ 218 boolean hasUserData() { 219 return hasKey(SharedConstants.UserDataKey); 220 } 221 222 /** 223 Get an arbitrary user-data object by key. 224 * @param key case-sensitive key to the object. 225 * @return the object associated to this key, or {@code null} if not found. 226 * @see #userData(String key, Object val) 227 * @since 1.17.1 228 */ 229 @Nullable 230 public Object userData(String key) { 231 Validate.notNull(key); 232 if (!hasUserData()) return null; // no user data exists 233 Map<String, Object> userData = userData(); 234 return userData.get(key); 235 } 236 237 /** 238 Set an arbitrary user-data object by key. Will be treated as an internal attribute, so will not be emitted in HTML. 239 * @param key case-sensitive key 240 * @param value object value. Providing a {@code null} value has the effect of removing the key from the userData map. 241 * @return these attributes 242 * @see #userData(String key) 243 * @since 1.17.1 244 */ 245 public Attributes userData(String key, @Nullable Object value) { 246 Validate.notNull(key); 247 if (value == null && !hasKey(SharedConstants.UserDataKey)) return this; // no user data exists, so short-circuit 248 Map<String, Object> userData = userData(); 249 if (value == null) userData.remove(key); 250 else userData.put(key, value); 251 return this; 252 } 253 254 /** 255 Gets the range spans, if source tracking was used. 256 */ 257 Range.@Nullable Spans spans() { 258 int i = indexOfKey(SharedConstants.RangeSpansKey); 259 return i == NotFound ? null : (Range.Spans) vals[i]; 260 } 261 262 /** 263 Gets or creates the range spans for this attributes object. 264 */ 265 Range.Spans ensureSpans() { 266 Range.Spans rangeSpans = spans(); 267 if (rangeSpans == null) { 268 rangeSpans = new Range.Spans(); 269 addObject(SharedConstants.RangeSpansKey, rangeSpans); 270 } 271 return rangeSpans; 272 } 273 274 /** 275 Sets the range spans when expanding compact leaf storage. 276 */ 277 void putSpans(Range.Spans rangeSpans) { 278 int i = indexOfKey(SharedConstants.RangeSpansKey); 279 if (i == NotFound) 280 addObject(SharedConstants.RangeSpansKey, rangeSpans); 281 else 282 vals[i] = rangeSpans; 283 } 284 285 void putIgnoreCase(String key, @Nullable String value) { 286 int i = indexOfKeyIgnoreCase(key); 287 if (i != NotFound) { 288 vals[i] = value; 289 String old = keys[i]; 290 assert old != null; 291 if (!old.equals(key)) // case changed, update 292 keys[i] = key; 293 } 294 else 295 addObject(key, value); 296 } 297 298 /** 299 * Set a new boolean attribute. Removes the attribute if the value is false. 300 * @param key case <b>insensitive</b> attribute key 301 * @param value attribute value 302 * @return these attributes, for chaining 303 */ 304 public Attributes put(String key, boolean value) { 305 if (value) 306 putIgnoreCase(key, null); 307 else 308 remove(key); 309 return this; 310 } 311 312 /** 313 Set a new attribute, or replace an existing one by key. 314 @param attribute attribute with case-sensitive key 315 @return these attributes, for chaining 316 */ 317 public Attributes put(Attribute attribute) { 318 Validate.notNull(attribute); 319 put(attribute.getKey(), attribute.getValue()); 320 attribute.parent = this; 321 return this; 322 } 323 324 // removes and shifts up 325 @SuppressWarnings("AssignmentToNull") 326 private void remove(int index) { 327 Validate.isFalse(index >= size); 328 Range.Spans rangeSpans = spans(); 329 // Source ranges are stored by visible attribute index; internal metadata slots have no matching range record. 330 if (rangeSpans != null && !isInternalKey(keys[index])) 331 rangeSpans.removeAttributeRange(visibleIndex(index)); 332 333 int shifted = size - index - 1; 334 if (shifted > 0) { 335 System.arraycopy(keys, index + 1, keys, index, shifted); 336 System.arraycopy(vals, index + 1, vals, index, shifted); 337 } 338 size--; 339 keys[size] = null; // release hold 340 vals[size] = null; 341 } 342 343 /** 344 Remove an attribute by key. <b>Case sensitive.</b> 345 @param key attribute key to remove 346 */ 347 public void remove(String key) { 348 int i = indexOfKey(key); 349 if (i != NotFound) 350 remove(i); 351 } 352 353 /** 354 Remove an attribute by key. <b>Case insensitive.</b> 355 @param key attribute key to remove 356 */ 357 public void removeIgnoreCase(String key) { 358 int i = indexOfKeyIgnoreCase(key); 359 if (i != NotFound) 360 remove(i); 361 } 362 363 /** 364 Tests if these attributes contain an attribute with this key. 365 @param key case-sensitive key to check for 366 @return true if key exists, false otherwise 367 */ 368 public boolean hasKey(String key) { 369 return indexOfKey(key) != NotFound; 370 } 371 372 /** 373 Tests if these attributes contain an attribute with this key. 374 @param key key to check for 375 @return true if key exists, false otherwise 376 */ 377 public boolean hasKeyIgnoreCase(String key) { 378 return indexOfKeyIgnoreCase(key) != NotFound; 379 } 380 381 /** 382 * Check if these attributes contain an attribute with a value for this key. 383 * @param key key to check for 384 * @return true if key exists, and it has a value 385 */ 386 public boolean hasDeclaredValueForKey(String key) { 387 int i = indexOfKey(key); 388 return i != NotFound && vals[i] != null; 389 } 390 391 /** 392 * Check if these attributes contain an attribute with a value for this key. 393 * @param key case-insensitive key to check for 394 * @return true if key exists, and it has a value 395 */ 396 public boolean hasDeclaredValueForKeyIgnoreCase(String key) { 397 int i = indexOfKeyIgnoreCase(key); 398 return i != NotFound && vals[i] != null; 399 } 400 401 /** 402 Get the number of attributes in this set, excluding any internal-only attributes (e.g. user data). 403 <p>Internal attributes are excluded from the {@link #html()}, {@link #asList()}, and {@link #iterator()} 404 methods.</p> 405 406 @return size 407 */ 408 public int size() { 409 if (size == 0) return 0; 410 int count = 0; 411 for (int i = 0; i < size; i++) { 412 if (!isInternalKey(keys[i])) count++; 413 } 414 return count; 415 } 416 417 /** 418 Test if this Attributes list is empty. 419 <p>This does not include internal attributes, such as user data.</p> 420 */ 421 public boolean isEmpty() { 422 return size() == 0; 423 } 424 425 /** 426 Add all the attributes from the incoming set to this set. 427 @param incoming attributes to add to these attributes. 428 */ 429 public void addAll(Attributes incoming) { 430 int incomingSize = incoming.size(); // not adding internal 431 if (incomingSize == 0) return; 432 checkCapacity(size + incomingSize); 433 434 boolean needsPut = size != 0; // if this set is empty, no need to check existing set, so can add() vs put() 435 // (and save bashing on the indexOfKey() 436 for (Attribute attr : incoming) { 437 if (needsPut) 438 put(attr); 439 else 440 addObject(attr.getKey(), attr.getValue()); 441 } 442 } 443 444 /** 445 Get the source ranges (start to end position) in the original input source from which this attribute's <b>name</b> 446 and <b>value</b> were parsed. 447 <p>Position tracking must be enabled before parsing the content.</p> 448 @param key the attribute name 449 @return the ranges for the attribute's name and value, or {@code untracked} if the attribute does not exist or its range 450 was not tracked. 451 @see org.jsoup.parser.Parser#setTrackPosition(boolean) 452 @see Attribute#sourceRange() 453 @see Node#sourceRange() 454 @see Element#endSourceRange() 455 @since 1.17.1 456 */ 457 public Range.AttributeRange sourceRange(String key) { 458 int index = visibleIndexOfKey(key); 459 if (index == NotFound) return UntrackedAttr; 460 Range.Spans rangeSpans = spans(); 461 return rangeSpans != null ? rangeSpans.attributeRange(index) : UntrackedAttr; 462 } 463 464 /** 465 Deprecated parser-internal source range setup method, retained for source compatibility. Source ranges are normally 466 produced by enabling parser position tracking before parsing. 467 @param key the attribute name 468 @param range the range for the attribute's name and value 469 @return these attributes, for chaining 470 @since 1.18.2 471 @deprecated Use parser position tracking instead. Will be removed in jsoup 1.24.1. 472 */ 473 @Deprecated 474 public Attributes sourceRange(String key, Range.AttributeRange range) { 475 Validate.notNull(key); 476 Validate.notNull(range); 477 NodeInternals.attributeRange(this, key, range); 478 return this; 479 } 480 481 482 @Override 483 public Iterator<Attribute> iterator() { 484 //noinspection ReturnOfInnerClass 485 return new Iterator<Attribute>() { 486 int expectedSize = size; 487 int i = 0; 488 489 @Override 490 public boolean hasNext() { 491 checkModified(); 492 while (i < size) { 493 String key = keys[i]; 494 assert key != null; 495 if (isInternalKey(key)) // skip over internal keys 496 i++; 497 else 498 break; 499 } 500 501 return i < size; 502 } 503 504 @Override 505 public Attribute next() { 506 checkModified(); 507 if (i >= size) throw new NoSuchElementException(); 508 String key = keys[i]; 509 assert key != null; 510 final Attribute attr = new Attribute(key, (String) vals[i], Attributes.this); 511 i++; 512 return attr; 513 } 514 515 private void checkModified() { 516 if (size != expectedSize) throw new ConcurrentModificationException("Use Iterator#remove() instead to remove attributes while iterating."); 517 } 518 519 @Override 520 public void remove() { 521 Attributes.this.remove(--i); // next() advanced, so rewind 522 expectedSize--; 523 } 524 }; 525 } 526 527 /** 528 Get the attributes as a List, for iteration. 529 @return a view of the attributes as an unmodifiable List. 530 */ 531 public List<Attribute> asList() { 532 ArrayList<Attribute> list = new ArrayList<>(size); 533 for (int i = 0; i < size; i++) { 534 String key = keys[i]; 535 assert key != null; 536 if (isInternalKey(key)) 537 continue; // skip internal keys 538 Attribute attr = new Attribute(key, (String) vals[i], Attributes.this); 539 list.add(attr); 540 } 541 return Collections.unmodifiableList(list); 542 } 543 544 /** 545 * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys 546 * starting with {@code data-}. 547 * @return map of custom data attributes. 548 */ 549 public Map<String, String> dataset() { 550 return new Dataset(this); 551 } 552 553 /** 554 Get the HTML representation of these attributes. 555 @return HTML 556 */ 557 public String html() { 558 StringBuilder sb = StringUtil.borrowBuilder(); 559 html(QuietAppendable.wrap(sb), new Document.OutputSettings()); // output settings a bit funky, but this html() seldom used 560 return StringUtil.releaseBuilder(sb); 561 } 562 563 final void html(final QuietAppendable accum, final Document.OutputSettings out) { 564 final int sz = size; 565 for (int i = 0; i < sz; i++) { 566 String key = keys[i]; 567 assert key != null; 568 if (isInternalKey(key)) 569 continue; 570 final String validated = Attribute.getValidKey(key, out.syntax()); 571 if (validated != null) 572 Attribute.htmlNoValidate(validated, (String) vals[i], accum.append(' '), out); 573 } 574 } 575 576 @Override 577 public String toString() { 578 return html(); 579 } 580 581 /** 582 * Checks if these attributes are equal to another set of attributes, by comparing the two sets. Note that the order 583 * of the attributes does not impact this equality (as per the Map interface equals()). 584 * @param o attributes to compare with 585 * @return if both sets of attributes have the same content 586 */ 587 @Override 588 public boolean equals(@Nullable Object o) { 589 if (this == o) return true; 590 if (o == null || getClass() != o.getClass()) return false; 591 592 Attributes that = (Attributes) o; 593 if (size != that.size) return false; 594 for (int i = 0; i < size; i++) { 595 String key = keys[i]; 596 assert key != null; 597 int thatI = that.indexOfKey(key); 598 if (thatI == NotFound || !Objects.equals(vals[i], that.vals[thatI])) 599 return false; 600 } 601 return true; 602 } 603 604 /** 605 * Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes. 606 * @return calculated hashcode 607 */ 608 @Override 609 public int hashCode() { 610 int result = size; 611 result = 31 * result + Arrays.hashCode(keys); 612 result = 31 * result + Arrays.hashCode(vals); 613 return result; 614 } 615 616 @Override 617 @SuppressWarnings("unchecked") 618 public Attributes clone() { 619 Attributes clone; 620 try { 621 clone = (Attributes) super.clone(); 622 } catch (CloneNotSupportedException e) { 623 throw new RuntimeException(e); 624 } 625 clone.size = size; 626 clone.keys = Arrays.copyOf(keys, size); 627 clone.vals = Arrays.copyOf(vals, size); 628 629 // make a copy of the user data map. (Contents are shallow). 630 int i = indexOfKey(SharedConstants.UserDataKey); 631 if (i != NotFound) { 632 clone.vals[i] = new HashMap<>((Map<String, Object>) vals[i]); 633 } 634 635 // make a copy of the range spans, if present. 636 i = indexOfKey(SharedConstants.RangeSpansKey); 637 if (i != NotFound) { 638 clone.vals[i] = ((Range.Spans) vals[i]).copy(); 639 } 640 641 return clone; 642 } 643 644 /** 645 * Internal method. Lowercases all (non-internal) keys. 646 */ 647 public void normalize() { 648 for (int i = 0; i < size; i++) { 649 assert keys[i] != null; 650 String key = keys[i]; 651 assert key != null; 652 if (!isInternalKey(key)) 653 keys[i] = lowerCase(key); 654 } 655 } 656 657 /** 658 * Internal method. Removes duplicate attribute by name. Settings for case sensitivity of key names. 659 * @param settings case sensitivity 660 * @return number of removed dupes 661 */ 662 public int deduplicate(ParseSettings settings) { 663 if (size == 0) return 0; 664 boolean preserve = settings.preserveAttributeCase(); 665 int dupes = 0; 666 for (int i = 0; i < size; i++) { 667 String keyI = keys[i]; 668 assert keyI != null; 669 for (int j = i + 1; j < size; j++) { 670 if ((preserve && keyI.equals(keys[j])) || (!preserve && keyI.equalsIgnoreCase(keys[j]))) { 671 dupes++; 672 remove(j); 673 j--; 674 } 675 } 676 } 677 return dupes; 678 } 679 680 private static class Dataset extends AbstractMap<String, String> { 681 private final Attributes attributes; 682 683 private Dataset(Attributes attributes) { 684 this.attributes = attributes; 685 } 686 687 @Override 688 public Set<Entry<String, String>> entrySet() { 689 return new EntrySet(); 690 } 691 692 @Override 693 public String put(String key, String value) { 694 String dataKey = dataKey(key); 695 String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null; 696 attributes.put(dataKey, value); 697 return oldValue; 698 } 699 700 private class EntrySet extends AbstractSet<Map.Entry<String, String>> { 701 702 @Override 703 public Iterator<Map.Entry<String, String>> iterator() { 704 return new DatasetIterator(); 705 } 706 707 @Override 708 public int size() { 709 int count = 0; 710 Iterator<Entry<String, String>> iter = new DatasetIterator(); 711 while (iter.hasNext()) 712 count++; 713 return count; 714 } 715 } 716 717 private class DatasetIterator implements Iterator<Map.Entry<String, String>> { 718 private final Iterator<Attribute> attrIter = attributes.iterator(); 719 private Attribute attr; 720 @Override public boolean hasNext() { 721 while (attrIter.hasNext()) { 722 attr = attrIter.next(); 723 if (attr.isDataAttribute()) return true; 724 } 725 return false; 726 } 727 728 @Override public Entry<String, String> next() { 729 return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue()); 730 } 731 732 @Override public void remove() { 733 attributes.remove(attr.getKey()); 734 } 735 } 736 } 737 738 private static String dataKey(String key) { 739 return dataPrefix + key; 740 } 741 742 static String internalKey(String key) { 743 return InternalPrefix + key; 744 } 745 746 static boolean isInternalKey(String key) { 747 return key.length() > 1 && key.charAt(0) == InternalPrefix; 748 } 749}