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