001package org.jsoup.nodes;
002
003import org.jsoup.SerializationException;
004import org.jsoup.helper.Validate;
005
006import java.io.IOException;
007import java.util.AbstractMap;
008import java.util.AbstractSet;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.Collections;
012import java.util.Iterator;
013import java.util.List;
014import java.util.Map;
015import java.util.Set;
016
017import static org.jsoup.internal.Normalizer.lowerCase;
018
019/**
020 * The attributes of an Element.
021 * <p>
022 * Attributes are treated as a map: there can be only one value associated with an attribute key/name.
023 * </p>
024 * <p>
025 * Attribute name and value comparisons are  generally <b>case sensitive</b>. By default for HTML, attribute names are
026 * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by
027 * name.
028 * </p>
029 *
030 * @author Jonathan Hedley, jonathan@hedley.net
031 */
032public class Attributes implements Iterable<Attribute>, Cloneable {
033    protected static final String dataPrefix = "data-";
034    private static final int InitialCapacity = 4; // todo - analyze Alexa 1MM sites, determine best setting
035
036    // manages the key/val arrays
037    private static final int GrowthFactor = 2;
038    private static final String[] Empty = {};
039    static final int NotFound = -1;
040    private static final String EmptyString = "";
041
042    private int size = 0; // number of slots used (not capacity, which is keys.length
043    String[] keys = Empty;
044    String[] vals = Empty;
045
046    // check there's room for more
047    private void checkCapacity(int minNewSize) {
048        Validate.isTrue(minNewSize >= size);
049        int curSize = keys.length;
050        if (curSize >= minNewSize)
051            return;
052
053        int newSize = curSize >= InitialCapacity ? size * GrowthFactor : InitialCapacity;
054        if (minNewSize > newSize)
055            newSize = minNewSize;
056
057        keys = copyOf(keys, newSize);
058        vals = copyOf(vals, newSize);
059    }
060
061    // simple implementation of Arrays.copy, for support of Android API 8.
062    private static String[] copyOf(String[] orig, int size) {
063        final String[] copy = new String[size];
064        System.arraycopy(orig, 0, copy, 0,
065                Math.min(orig.length, size));
066        return copy;
067    }
068
069    int indexOfKey(String key) {
070        Validate.notNull(key);
071        for (int i = 0; i < size; i++) {
072            if (key.equals(keys[i]))
073                return i;
074        }
075        return NotFound;
076    }
077
078    private int indexOfKeyIgnoreCase(String key) {
079        Validate.notNull(key);
080        for (int i = 0; i < size; i++) {
081            if (key.equalsIgnoreCase(keys[i]))
082                return i;
083        }
084        return NotFound;
085    }
086
087    // we track boolean attributes as null in values - they're just keys. so returns empty for consumers
088    static String checkNotNull(String val) {
089        return val == null ? EmptyString : val;
090    }
091
092    /**
093     Get an attribute value by key.
094     @param key the (case-sensitive) attribute key
095     @return the attribute value if set; or empty string if not set (or a boolean attribute).
096     @see #hasKey(String)
097     */
098    public String get(String key) {
099        int i = indexOfKey(key);
100        return i == NotFound ? EmptyString : checkNotNull(vals[i]);
101    }
102
103    /**
104     * Get an attribute's value by case-insensitive key
105     * @param key the attribute name
106     * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute).
107     */
108    public String getIgnoreCase(String key) {
109        int i = indexOfKeyIgnoreCase(key);
110        return i == NotFound ? EmptyString : checkNotNull(vals[i]);
111    }
112
113    // adds without checking if this key exists
114    private void add(String key, String value) {
115        checkCapacity(size + 1);
116        keys[size] = key;
117        vals[size] = value;
118        size++;
119    }
120
121    /**
122     * Set a new attribute, or replace an existing one by key.
123     * @param key case sensitive attribute key
124     * @param value attribute value
125     * @return these attributes, for chaining
126     */
127    public Attributes put(String key, String value) {
128        int i = indexOfKey(key);
129        if (i != NotFound)
130            vals[i] = value;
131        else
132            add(key, value);
133        return this;
134    }
135
136    void putIgnoreCase(String key, String value) {
137        int i = indexOfKeyIgnoreCase(key);
138        if (i != NotFound) {
139            vals[i] = value;
140            if (!keys[i].equals(key)) // case changed, update
141                keys[i] = key;
142        }
143        else
144            add(key, value);
145    }
146
147    /**
148     * Set a new boolean attribute, remove attribute if value is false.
149     * @param key case <b>insensitive</b> attribute key
150     * @param value attribute value
151     * @return these attributes, for chaining
152     */
153    public Attributes put(String key, boolean value) {
154        if (value)
155            putIgnoreCase(key, null);
156        else
157            remove(key);
158        return this;
159    }
160
161    /**
162     Set a new attribute, or replace an existing one by key.
163     @param attribute attribute with case sensitive key
164     @return these attributes, for chaining
165     */
166    public Attributes put(Attribute attribute) {
167        Validate.notNull(attribute);
168        put(attribute.getKey(), attribute.getValue());
169        attribute.parent = this;
170        return this;
171    }
172
173    // removes and shifts up
174    private void remove(int index) {
175        Validate.isFalse(index >= size);
176        int shifted = size - index - 1;
177        if (shifted > 0) {
178            System.arraycopy(keys, index + 1, keys, index, shifted);
179            System.arraycopy(vals, index + 1, vals, index, shifted);
180        }
181        size--;
182        keys[size] = null; // release hold
183        vals[size] = null;
184    }
185
186    /**
187     Remove an attribute by key. <b>Case sensitive.</b>
188     @param key attribute key to remove
189     */
190    public void remove(String key) {
191        int i = indexOfKey(key);
192        if (i != NotFound)
193            remove(i);
194    }
195
196    /**
197     Remove an attribute by key. <b>Case insensitive.</b>
198     @param key attribute key to remove
199     */
200    public void removeIgnoreCase(String key) {
201        int i = indexOfKeyIgnoreCase(key);
202        if (i != NotFound)
203            remove(i);
204    }
205
206    /**
207     Tests if these attributes contain an attribute with this key.
208     @param key case-sensitive key to check for
209     @return true if key exists, false otherwise
210     */
211    public boolean hasKey(String key) {
212        return indexOfKey(key) != NotFound;
213    }
214
215    /**
216     Tests if these attributes contain an attribute with this key.
217     @param key key to check for
218     @return true if key exists, false otherwise
219     */
220    public boolean hasKeyIgnoreCase(String key) {
221        return indexOfKeyIgnoreCase(key) != NotFound;
222    }
223
224    /**
225     Get the number of attributes in this set.
226     @return size
227     */
228    public int size() {
229        return size;
230    }
231
232    /**
233     Add all the attributes from the incoming set to this set.
234     @param incoming attributes to add to these attributes.
235     */
236    public void addAll(Attributes incoming) {
237        if (incoming.size() == 0)
238            return;
239        checkCapacity(size + incoming.size);
240
241        for (Attribute attr : incoming) {
242            // todo - should this be case insensitive?
243            put(attr);
244        }
245
246    }
247
248    public Iterator<Attribute> iterator() {
249        return new Iterator<Attribute>() {
250            int i = 0;
251
252            @Override
253            public boolean hasNext() {
254                return i < size;
255            }
256
257            @Override
258            public Attribute next() {
259                final Attribute attr = new Attribute(keys[i], vals[i], Attributes.this);
260                i++;
261                return attr;
262            }
263
264            @Override
265            public void remove() {
266                Attributes.this.remove(--i); // next() advanced, so rewind
267            }
268        };
269    }
270
271    /**
272     Get the attributes as a List, for iteration.
273     @return an view of the attributes as an unmodifialbe List.
274     */
275    public List<Attribute> asList() {
276        ArrayList<Attribute> list = new ArrayList<>(size);
277        for (int i = 0; i < size; i++) {
278            Attribute attr = vals[i] == null ?
279                new BooleanAttribute(keys[i]) : // deprecated class, but maybe someone still wants it
280                new Attribute(keys[i], vals[i], Attributes.this);
281            list.add(attr);
282        }
283        return Collections.unmodifiableList(list);
284    }
285
286    /**
287     * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys
288     * starting with {@code data-}.
289     * @return map of custom data attributes.
290     */
291    public Map<String, String> dataset() {
292        return new Dataset(this);
293    }
294
295    /**
296     Get the HTML representation of these attributes.
297     @return HTML
298     @throws SerializationException if the HTML representation of the attributes cannot be constructed.
299     */
300    public String html() {
301        StringBuilder accum = new StringBuilder();
302        try {
303            html(accum, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used
304        } catch (IOException e) { // ought never happen
305            throw new SerializationException(e);
306        }
307        return accum.toString();
308    }
309
310    final void html(final Appendable accum, final Document.OutputSettings out) throws IOException {
311        final int sz = size;
312        for (int i = 0; i < sz; i++) {
313            // inlined from Attribute.html()
314            final String key = keys[i];
315            final String val = vals[i];
316            accum.append(' ').append(key);
317
318            // collapse checked=null, checked="", checked=checked; write out others
319            if (!(out.syntax() == Document.OutputSettings.Syntax.html
320                && (val == null || val.equals(key) && Attribute.isBooleanAttribute(key)))) {
321
322                accum.append("=\"");
323                Entities.escape(accum, val == null ? EmptyString : val, out, true, false, false);
324                accum.append('"');
325            }
326        }
327    }
328
329    @Override
330    public String toString() {
331        return html();
332    }
333
334    /**
335     * Checks if these attributes are equal to another set of attributes, by comparing the two sets
336     * @param o attributes to compare with
337     * @return if both sets of attributes have the same content
338     */
339    @Override
340    public boolean equals(Object o) {
341        if (this == o) return true;
342        if (o == null || getClass() != o.getClass()) return false;
343
344        Attributes that = (Attributes) o;
345
346        if (size != that.size) return false;
347        if (!Arrays.equals(keys, that.keys)) return false;
348        return Arrays.equals(vals, that.vals);
349    }
350
351    /**
352     * Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes.
353     * @return calculated hashcode
354     */
355    @Override
356    public int hashCode() {
357        int result = size;
358        result = 31 * result + Arrays.hashCode(keys);
359        result = 31 * result + Arrays.hashCode(vals);
360        return result;
361    }
362
363    @Override
364    public Attributes clone() {
365        Attributes clone;
366        try {
367            clone = (Attributes) super.clone();
368        } catch (CloneNotSupportedException e) {
369            throw new RuntimeException(e);
370        }
371        clone.size = size;
372        keys = copyOf(keys, size);
373        vals = copyOf(vals, size);
374        return clone;
375    }
376
377    /**
378     * Internal method. Lowercases all keys.
379     */
380    public void normalize() {
381        for (int i = 0; i < size; i++) {
382            keys[i] = lowerCase(keys[i]);
383        }
384    }
385
386    private static class Dataset extends AbstractMap<String, String> {
387        private final Attributes attributes;
388
389        private Dataset(Attributes attributes) {
390            this.attributes = attributes;
391        }
392
393        @Override
394        public Set<Entry<String, String>> entrySet() {
395            return new EntrySet();
396        }
397
398        @Override
399        public String put(String key, String value) {
400            String dataKey = dataKey(key);
401            String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null;
402            attributes.put(dataKey, value);
403            return oldValue;
404        }
405
406        private class EntrySet extends AbstractSet<Map.Entry<String, String>> {
407
408            @Override
409            public Iterator<Map.Entry<String, String>> iterator() {
410                return new DatasetIterator();
411            }
412
413            @Override
414            public int size() {
415                int count = 0;
416                Iterator iter = new DatasetIterator();
417                while (iter.hasNext())
418                    count++;
419                return count;
420            }
421        }
422
423        private class DatasetIterator implements Iterator<Map.Entry<String, String>> {
424            private Iterator<Attribute> attrIter = attributes.iterator();
425            private Attribute attr;
426            public boolean hasNext() {
427                while (attrIter.hasNext()) {
428                    attr = attrIter.next();
429                    if (attr.isDataAttribute()) return true;
430                }
431                return false;
432            }
433
434            public Entry<String, String> next() {
435                return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue());
436            }
437
438            public void remove() {
439                attributes.remove(attr.getKey());
440            }
441        }
442    }
443
444    private static String dataKey(String key) {
445        return dataPrefix + key;
446    }
447}