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