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}