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}