All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.styledxmlparser.jsoup.nodes.Attributes Maven / Gradle / Ivy

There is a newer version: 9.0.0
Show newest version
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2024 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
 */
package com.itextpdf.styledxmlparser.jsoup.nodes;

import com.itextpdf.styledxmlparser.jsoup.SerializationException;
import com.itextpdf.styledxmlparser.jsoup.helper.Validate;
import com.itextpdf.styledxmlparser.jsoup.internal.Normalizer;
import com.itextpdf.styledxmlparser.jsoup.internal.StringUtil;
import com.itextpdf.styledxmlparser.jsoup.parser.ParseSettings;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * The attributes of an Element.
 * 

* Attributes are treated as a map: there can be only one value associated with an attribute key/name. *

* Attribute name and value comparisons are generally case sensitive. By default for HTML, attribute names are * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by * name. * * @author Jonathan Hedley, [email protected] */ public class Attributes implements Iterable, Cloneable { // The Attributes object is only created on the first use of an attribute; the Element will just have a null // Attribute slot otherwise protected static final String dataPrefix = "data-"; // Indicates a jsoup internal key. Can't be set via HTML. (It could be set via accessor, but not too worried about // that. Suppressed from list, iter. static final char InternalPrefix = '/'; 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. // manages the key/val arrays private static final int GrowthFactor = 2; static final int NotFound = -1; private static final String EmptyString = ""; private int size = 0; // number of slots used (not total capacity, which is keys.length) String[] keys = new String[InitialCapacity]; String[] vals = new String[InitialCapacity]; // check there's room for more private void checkCapacity(int minNewSize) { Validate.isTrue(minNewSize >= size); int curCap = keys.length; if (curCap >= minNewSize) return; int newCap = curCap >= InitialCapacity ? size * GrowthFactor : InitialCapacity; if (minNewSize > newCap) newCap = minNewSize; keys = Arrays.copyOf(keys, newCap); vals = Arrays.copyOf(vals, newCap); } int indexOfKey(String key) { Validate.notNull(key); for (int i = 0; i < size; i++) { if (key.equals(keys[i])) return i; } return NotFound; } private int indexOfKeyIgnoreCase(String key) { Validate.notNull(key); for (int i = 0; i < size; i++) { if (key.equalsIgnoreCase(keys[i])) return i; } return NotFound; } // we track boolean attributes as null in values - they're just keys. so returns empty for consumers static String checkNotNull(String val) { return val == null ? EmptyString : val; } /** Get an attribute value by key. @param key the (case-sensitive) attribute key @return the attribute value if set; or empty string if not set (or a boolean attribute). @see #hasKey(String) */ public String get(String key) { int i = indexOfKey(key); return i == NotFound ? EmptyString : checkNotNull(vals[i]); } /** * Get an attribute's value by case-insensitive key * @param key the attribute name * @return the first matching attribute value if set; or empty string if not set (ora boolean attribute). */ public String getIgnoreCase(String key) { int i = indexOfKeyIgnoreCase(key); return i == NotFound ? EmptyString : checkNotNull(vals[i]); } /** * Adds a new attribute. Will produce duplicates if the key already exists. * @see Attributes#put(String, String) */ public Attributes add(String key, String value) { checkCapacity(size + 1); keys[size] = key; vals[size] = value; size++; return this; } /** * Set a new attribute, or replace an existing one by key. * @param key case sensitive attribute key (not null) * @param value attribute value (may be null, to set a boolean attribute) * @return these attributes, for chaining */ public Attributes put(String key, String value) { Validate.notNull(key); int i = indexOfKey(key); if (i != NotFound) vals[i] = value; else add(key, value); return this; } void putIgnoreCase(String key, String value) { int i = indexOfKeyIgnoreCase(key); if (i != NotFound) { vals[i] = value; if (!keys[i].equals(key)) // case changed, update keys[i] = key; } else add(key, value); } /** * Set a new boolean attribute, remove attribute if value is false. * @param key case insensitive attribute key * @param value attribute value * @return these attributes, for chaining */ public Attributes put(String key, boolean value) { if (value) putIgnoreCase(key, null); else remove(key); return this; } /** Set a new attribute, or replace an existing one by key. @param attribute attribute with case sensitive key @return these attributes, for chaining */ public Attributes put(Attribute attribute) { Validate.notNull(attribute); put(attribute.getKey(), attribute.getValue()); attribute.parent = this; return this; } // removes and shifts up @SuppressWarnings("AssignmentToNull") private void remove(int index) { Validate.isFalse(index >= size); int shifted = size - index - 1; if (shifted > 0) { System.arraycopy(keys, index + 1, keys, index, shifted); System.arraycopy(vals, index + 1, vals, index, shifted); } size--; keys[size] = null; // release hold vals[size] = null; } /** Remove an attribute by key. Case sensitive. @param key attribute key to remove */ public void remove(String key) { int i = indexOfKey(key); if (i != NotFound) remove(i); } /** Remove an attribute by key. Case insensitive. @param key attribute key to remove */ public void removeIgnoreCase(String key) { int i = indexOfKeyIgnoreCase(key); if (i != NotFound) remove(i); } /** Tests if these attributes contain an attribute with this key. @param key case-sensitive key to check for @return true if key exists, false otherwise */ public boolean hasKey(String key) { return indexOfKey(key) != NotFound; } /** Tests if these attributes contain an attribute with this key. @param key key to check for @return true if key exists, false otherwise */ public boolean hasKeyIgnoreCase(String key) { return indexOfKeyIgnoreCase(key) != NotFound; } /** * Check if these attributes contain an attribute with a value for this key. * @param key key to check for * @return true if key exists, and it has a value */ public boolean hasDeclaredValueForKey(String key) { int i = indexOfKey(key); return i != NotFound && vals[i] != null; } /** * Check if these attributes contain an attribute with a value for this key. * @param key case-insensitive key to check for * @return true if key exists, and it has a value */ public boolean hasDeclaredValueForKeyIgnoreCase(String key) { int i = indexOfKeyIgnoreCase(key); return i != NotFound && vals[i] != null; } /** Get the number of attributes in this set. @return size */ public int size() { int s = 0; for (int i = 0; i < size; i++) { if (!isInternalKey(keys[i])) s++; } return s; } /** * Test if this Attributes list is empty (size==0). */ public boolean isEmpty() { return size == 0; } /** Add all the attributes from the incoming set to this set. @param incoming attributes to add to these attributes. */ public void addAll(Attributes incoming) { if (incoming.size() == 0) return; checkCapacity(size + incoming.size); for (Attribute attr : incoming) { put(attr); } } public Iterator iterator() { return new Iterator() { int i = 0; @Override public boolean hasNext() { while (i < size) { if (isInternalKey(keys[i])) // skip over internal keys i++; else break; } return i < size; } @Override public Attribute next() { final Attribute attr = new Attribute(keys[i], vals[i], Attributes.this); i++; return attr; } }; } /** Get the attributes as a List, for iteration. @return an view of the attributes as an unmodifiable List. */ public List asList() { ArrayList list = new ArrayList<>(size); for (int i = 0; i < size; i++) { if (isInternalKey(keys[i])) continue; // skip internal keys Attribute attr = new Attribute(keys[i], vals[i], Attributes.this); list.add(attr); } return Collections.unmodifiableList(list); } /** * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys * starting with {@code data-}. * @return map of custom data attributes. */ public Map dataset() { return new Dataset(this); } /** Get the HTML representation of these attributes. @return HTML */ public String html() { StringBuilder sb = StringUtil.borrowBuilder(); try { html(sb, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used } catch (IOException e) { // ought never happen throw new SerializationException(e); } return StringUtil.releaseBuilder(sb); } final void html(final Appendable accum, final Document.OutputSettings out) throws IOException { final int sz = size; for (int i = 0; i < sz; i++) { if (isInternalKey(keys[i])) continue; // inlined from Attribute.html() final String key = keys[i]; final String val = vals[i]; accum.append(' ').append(key); // collapse checked=null, checked="", checked=checked; write out others if (!Attribute.shouldCollapseAttribute(key, val, out)) { accum.append("=\""); Entities.escape(accum, val == null ? EmptyString : val, out, true, false, false); accum.append('"'); } } } @Override public String toString() { return html(); } /** * Checks if these attributes are equal to another set of attributes, by comparing the two sets * @param o attributes to compare with * @return if both sets of attributes have the same content */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Attributes that = (Attributes) o; if (size != that.size) return false; if (!Arrays.equals(keys, that.keys)) return false; return Arrays.equals(vals, that.vals); } /** * Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes. * @return calculated hashcode */ @Override public int hashCode() { int result = size; result = 31 * result + Arrays.hashCode(keys); result = 31 * result + Arrays.hashCode(vals); return result; } @Override public Object clone() { Attributes clone; try { clone = (Attributes) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } clone.size = size; keys = Arrays.copyOf(keys, size); vals = Arrays.copyOf(vals, size); return clone; } /** * Internal method. Lowercases all keys. */ public void normalize() { for (int i = 0; i < size; i++) { keys[i] = Normalizer.lowerCase(keys[i]); } } /** * Internal method. Removes duplicate attribute by name. Settings for case sensitivity of key names. * @param settings case sensitivity * @return number of removed dupes */ public int deduplicate(ParseSettings settings) { if (isEmpty()) return 0; boolean preserve = settings.preserveAttributeCase(); int dupes = 0; OUTER: for (int i = 0; i < keys.length; i++) { for (int j = i + 1; j < keys.length; j++) { if (keys[j] == null) continue OUTER; // keys.length doesn't shrink when removing, so re-test if ((preserve && keys[i].equals(keys[j])) || (!preserve && keys[i].equalsIgnoreCase(keys[j]))) { dupes++; remove(j); j--; } } } return dupes; } private static class Dataset extends AbstractMap { private final Attributes attributes; Dataset(Attributes attributes) { this.attributes = attributes; } @Override public Set> entrySet() { return new EntrySet(); } @Override public String put(String key, String value) { String dataKey = dataKey(key); String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null; attributes.put(dataKey, value); return oldValue; } private class EntrySet extends AbstractSet> { @Override public Iterator> iterator() { return new DatasetIterator(); } @Override public int size() { int count = 0; Iterator iter = new DatasetIterator(); while (iter.hasNext()) count++; return count; } } private class DatasetIterator implements Iterator> { private Iterator attrIter = attributes.iterator(); private Attribute attr; public boolean hasNext() { while (attrIter.hasNext()) { attr = attrIter.next(); if (attr.isDataAttribute()) return true; } return false; } public Entry next() { return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue()); } public void remove() { attributes.remove(attr.getKey()); } } } private static String dataKey(String key) { return dataPrefix + key; } static String internalKey(String key) { return InternalPrefix + key; } private boolean isInternalKey(String key) { return key != null && key.length() > 1 && key.charAt(0) == InternalPrefix; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy