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