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}