001package org.jsoup.nodes;
002
003import org.jsoup.SerializationException;
004import org.jsoup.helper.Validate;
005
006import java.io.IOException;
007import java.util.Arrays;
008import java.util.Map;
009
010/**
011 A single key + value attribute. (Only used for presentation.)
012 */
013public class Attribute implements Map.Entry<String, String>, Cloneable  {
014    private static final String[] booleanAttributes = {
015            "allowfullscreen", "async", "autofocus", "checked", "compact", "declare", "default", "defer", "disabled",
016            "formnovalidate", "hidden", "inert", "ismap", "itemscope", "multiple", "muted", "nohref", "noresize",
017            "noshade", "novalidate", "nowrap", "open", "readonly", "required", "reversed", "seamless", "selected",
018            "sortable", "truespeed", "typemustmatch"
019    };
020
021    private String key;
022    private String val;
023    Attributes parent; // used to update the holding Attributes when the key / value is changed via this interface
024
025    /**
026     * Create a new attribute from unencoded (raw) key and value.
027     * @param key attribute key; case is preserved.
028     * @param value attribute value
029     * @see #createFromEncoded
030     */
031    public Attribute(String key, String value) {
032        this(key, value, null);
033    }
034
035    /**
036     * Create a new attribute from unencoded (raw) key and value.
037     * @param key attribute key; case is preserved.
038     * @param val attribute value
039     * @param parent the containing Attributes (this Attribute is not automatically added to said Attributes)
040     * @see #createFromEncoded*/
041    public Attribute(String key, String val, Attributes parent) {
042        Validate.notNull(key);
043        this.key = key.trim();
044        Validate.notEmpty(key); // trimming could potentially make empty, so validate here
045        this.val = val;
046        this.parent = parent;
047    }
048
049    /**
050     Get the attribute key.
051     @return the attribute key
052     */
053    public String getKey() {
054        return key;
055    }
056
057    /**
058     Set the attribute key; case is preserved.
059     @param key the new key; must not be null
060     */
061    public void setKey(String key) {
062        Validate.notNull(key);
063        key = key.trim();
064        Validate.notEmpty(key); // trimming could potentially make empty, so validate here
065        if (parent != null) {
066            int i = parent.indexOfKey(this.key);
067            if (i != Attributes.NotFound)
068                parent.keys[i] = key;
069        }
070        this.key = key;
071    }
072
073    /**
074     Get the attribute value.
075     @return the attribute value
076     */
077    public String getValue() {
078        return val;
079    }
080
081    /**
082     Set the attribute value.
083     @param val the new attribute value; must not be null
084     */
085    public String setValue(String val) {
086        String oldVal = parent.get(this.key);
087        if (parent != null) {
088            int i = parent.indexOfKey(this.key);
089            if (i != Attributes.NotFound)
090                parent.vals[i] = val;
091        }
092        this.val = val;
093        return oldVal;
094    }
095
096    /**
097     Get the HTML representation of this attribute; e.g. {@code href="index.html"}.
098     @return HTML
099     */
100    public String html() {
101        StringBuilder accum = new StringBuilder();
102        
103        try {
104                html(accum, (new Document("")).outputSettings());
105        } catch(IOException exception) {
106                throw new SerializationException(exception);
107        }
108        return accum.toString();
109    }
110
111    protected static void html(String key, String val, Appendable accum, Document.OutputSettings out) throws IOException {
112        accum.append(key);
113        if (!shouldCollapseAttribute(key, val, out)) {
114            accum.append("=\"");
115            Entities.escape(accum, Attributes.checkNotNull(val) , out, true, false, false);
116            accum.append('"');
117        }
118    }
119    
120    protected void html(Appendable accum, Document.OutputSettings out) throws IOException {
121        html(key, val, accum, out);
122    }
123
124    /**
125     Get the string representation of this attribute, implemented as {@link #html()}.
126     @return string
127     */
128    @Override
129    public String toString() {
130        return html();
131    }
132
133    /**
134     * Create a new Attribute from an unencoded key and a HTML attribute encoded value.
135     * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars.
136     * @param encodedValue HTML attribute encoded value
137     * @return attribute
138     */
139    public static Attribute createFromEncoded(String unencodedKey, String encodedValue) {
140        String value = Entities.unescape(encodedValue, true);
141        return new Attribute(unencodedKey, value, null); // parent will get set when Put
142    }
143
144    protected boolean isDataAttribute() {
145        return isDataAttribute(key);
146    }
147
148    protected static boolean isDataAttribute(String key) {
149        return key.startsWith(Attributes.dataPrefix) && key.length() > Attributes.dataPrefix.length();
150    }
151
152    /**
153     * Collapsible if it's a boolean attribute and value is empty or same as name
154     * 
155     * @param out output settings
156     * @return  Returns whether collapsible or not
157     */
158    protected final boolean shouldCollapseAttribute(Document.OutputSettings out) {
159        return shouldCollapseAttribute(key, val, out);
160    }
161
162    protected static boolean shouldCollapseAttribute(String key, String val, Document.OutputSettings out) {
163        // todo: optimize
164        return (val == null || "".equals(val) || val.equalsIgnoreCase(key))
165            && out.syntax() == Document.OutputSettings.Syntax.html
166            && isBooleanAttribute(key);
167    }
168
169    /**
170     * @deprecated
171     */
172    protected boolean isBooleanAttribute() {
173        return Arrays.binarySearch(booleanAttributes, key) >= 0 || val == null;
174    }
175
176    /**
177     * Checks if this attribute name is defined as a boolean attribute in HTML5
178     */
179    protected static boolean isBooleanAttribute(final String key) {
180        return Arrays.binarySearch(booleanAttributes, key) >= 0;
181    }
182
183    @Override
184    public boolean equals(Object o) { // note parent not considered
185        if (this == o) return true;
186        if (o == null || getClass() != o.getClass()) return false;
187        Attribute attribute = (Attribute) o;
188        if (key != null ? !key.equals(attribute.key) : attribute.key != null) return false;
189        return val != null ? val.equals(attribute.val) : attribute.val == null;
190    }
191
192    @Override
193    public int hashCode() { // note parent not considered
194        int result = key != null ? key.hashCode() : 0;
195        result = 31 * result + (val != null ? val.hashCode() : 0);
196        return result;
197    }
198
199    @Override
200    public Attribute clone() {
201        try {
202            return (Attribute) super.clone();
203        } catch (CloneNotSupportedException e) {
204            throw new RuntimeException(e);
205        }
206    }
207}