68
|
1 /*
|
|
2 Copyright (c) 2008 Franklin Schmidt <fschmidt@gmail.com>
|
|
3
|
|
4 Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5 of this software and associated documentation files (the "Software"), to deal
|
|
6 in the Software without restriction, including without limitation the rights
|
|
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8 copies of the Software, and to permit persons to whom the Software is
|
|
9 furnished to do so, subject to the following conditions:
|
|
10
|
|
11 The above copyright notice and this permission notice shall be included in
|
|
12 all copies or substantial portions of the Software.
|
|
13
|
|
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
20 THE SOFTWARE.
|
|
21 */
|
|
22
|
|
23
|
|
24 package fschmidt.html;
|
|
25
|
|
26 import java.util.List;
|
|
27 import java.util.ArrayList;
|
|
28 import java.util.Map;
|
|
29 import java.util.HashMap;
|
|
30 import java.util.LinkedHashMap;
|
|
31 import java.util.Iterator;
|
|
32 import java.util.Collection;
|
|
33 import java.util.Collections;
|
|
34
|
|
35
|
|
36 public class HtmlTag {
|
|
37
|
|
38 static final class BadTag extends RuntimeException {
|
|
39 private BadTag(String msg) {
|
|
40 super(msg);
|
|
41 }
|
|
42 }
|
|
43
|
|
44 public static final class Attribute {
|
|
45 private final String spaceBeforeName;
|
|
46 private final String name;
|
|
47 private final String spaceAfterName;
|
|
48 private final String spaceBeforeValue;
|
|
49 private final String value;
|
|
50
|
|
51 protected Attribute(String spaceBeforeName,String name,String spaceAfterName,String spaceBeforeValue,String value) {
|
|
52 this.spaceBeforeName = spaceBeforeName;
|
|
53 this.name = name;
|
|
54 this.spaceAfterName = spaceAfterName;
|
|
55 this.spaceBeforeValue = spaceBeforeValue;
|
|
56 this.value = value;
|
|
57 }
|
|
58
|
|
59 public String getSpaceBeforeName() {
|
|
60 return spaceBeforeName;
|
|
61 }
|
|
62
|
|
63 public String getName() {
|
|
64 return name;
|
|
65 }
|
|
66
|
|
67 public String getSpaceAfterName() {
|
|
68 return spaceAfterName;
|
|
69 }
|
|
70
|
|
71 public String getSpaceBeforeValue() {
|
|
72 return spaceBeforeValue;
|
|
73 }
|
|
74
|
|
75 public String getValue() {
|
|
76 return value;
|
|
77 }
|
|
78
|
|
79 @Override public String toString() {
|
|
80 StringBuilder buf = new StringBuilder();
|
|
81 buf.append(name);
|
|
82 if( value != null ) {
|
|
83 buf.append(spaceAfterName);
|
|
84 buf.append('=');
|
|
85 buf.append(spaceBeforeValue);
|
|
86 buf.append(value);
|
|
87 }
|
|
88 return buf.toString();
|
|
89 }
|
|
90 }
|
|
91
|
|
92 private String name;
|
|
93 private final Map<String,Attribute> attrMap;
|
|
94 private boolean isEmpty;
|
|
95 int lineNumber = -1;
|
|
96 private final String spaceAtEnd;
|
|
97
|
|
98 private static int toEndName(String text,int i,int len) {
|
|
99 if( i==len )
|
|
100 return i;
|
|
101 char c = text.charAt(i);
|
|
102 switch(c) {
|
|
103 case '"':
|
|
104 case '\'':
|
|
105 i = text.indexOf(c,i+1);
|
|
106 return i==-1 ? len : i+1;
|
|
107 default:
|
|
108 if( Character.isWhitespace(c) ) {
|
|
109 throw new RuntimeException("text="+text+" i="+i);
|
|
110 }
|
|
111 do {
|
|
112 i++;
|
|
113 } while( i<len && (c=text.charAt(i))!='=' && !Character.isWhitespace(c) );
|
|
114 return i;
|
|
115 }
|
|
116 }
|
|
117
|
|
118 private static int toEndValue(String text,int i,int len) {
|
|
119 if( i==len )
|
|
120 return i;
|
|
121 char c = text.charAt(i);
|
|
122 switch(c) {
|
|
123 case '"':
|
|
124 case '\'':
|
|
125 i = text.indexOf(c,i+1);
|
|
126 return i==-1 ? len : i+1;
|
|
127 default:
|
|
128 if( Character.isWhitespace(c) ) {
|
|
129 throw new RuntimeException("text="+text+" i="+i);
|
|
130 }
|
|
131 do {
|
|
132 i++;
|
|
133 } while( i<len && !Character.isWhitespace(text.charAt(i)) );
|
|
134 return i;
|
|
135 }
|
|
136 }
|
|
137
|
|
138 public HtmlTag(String text) throws BadTag {
|
|
139 attrMap = new LinkedHashMap<String,Attribute>();
|
|
140 if( text.endsWith("/") ) {
|
|
141 text = text.substring(0,text.length()-1);
|
|
142 isEmpty = true;
|
|
143 } else {
|
|
144 isEmpty = false;
|
|
145 }
|
|
146 int len = text.length();
|
|
147 int i = 0;
|
|
148 int i2 = i;
|
|
149 if( i2<len && text.charAt(i2)=='/' )
|
|
150 i2++;
|
|
151 while( i2<len ) {
|
|
152 char c = text.charAt(i2);
|
|
153 if( Character.isWhitespace(c) )
|
|
154 break;
|
|
155 if( !( Character.isLetterOrDigit(c) || c=='_' || c=='.' || c=='-' || c==':' ) )
|
|
156 throw new BadTag("invalid tag name for <"+text+">");
|
|
157 i2++;
|
|
158 }
|
|
159 name = text.substring(i,i2);
|
|
160 i = i2;
|
|
161 while( i<len && Character.isWhitespace(text.charAt(i)) ) i++;
|
|
162 while( i<len ) {
|
|
163 String attrSpaceBeforeName = text.substring(i2,i);
|
|
164 i2 = toEndName(text,i,len);
|
|
165 String attrName = text.substring(i,i2);
|
|
166 i = i2;
|
|
167 while( i<len && Character.isWhitespace(text.charAt(i)) ) i++;
|
|
168 String attrSpaceAfterName = "";
|
|
169 String attrSpaceBeforeValue = "";
|
|
170 String attrValue = null;
|
|
171 if( i<len && text.charAt(i) == '=' ) {
|
|
172 attrSpaceAfterName = text.substring(i2,i);
|
|
173 i++;
|
|
174 i2 = i;
|
|
175 while( i<len && Character.isWhitespace(text.charAt(i)) ) i++;
|
|
176 attrSpaceBeforeValue = text.substring(i2,i);
|
|
177 i2 = toEndValue(text,i,len);
|
|
178 attrValue = text.substring(i,i2);
|
|
179 if( attrValue.indexOf('<') != -1 || attrValue.indexOf('>') != -1 )
|
|
180 throw new BadTag("invalid attribute value: "+attrValue);
|
|
181 i = i2;
|
|
182 while( i<len && Character.isWhitespace(text.charAt(i)) ) i++;
|
|
183 }
|
|
184 if( setAttribute(attrSpaceBeforeName,attrName,attrSpaceAfterName,attrSpaceBeforeValue,attrValue) != null )
|
|
185 throw new BadTag("duplicate attribute: "+attrName);
|
|
186 }
|
|
187 spaceAtEnd = text.substring(i2,i);
|
|
188 }
|
|
189
|
|
190 public HtmlTag(HtmlTag tag) {
|
|
191 this.name = tag.name;
|
|
192 this.attrMap = new LinkedHashMap<String,Attribute>(tag.attrMap);
|
|
193 this.isEmpty = tag.isEmpty;
|
|
194 this.spaceAtEnd = tag.spaceAtEnd;
|
|
195 }
|
|
196
|
|
197 public HtmlTag cloneTag() {
|
|
198 HtmlTag tag = new HtmlTag(this);
|
|
199 tag.lineNumber = lineNumber;
|
|
200 return tag;
|
|
201 }
|
|
202
|
|
203 public String getName() {
|
|
204 return name;
|
|
205 }
|
|
206
|
|
207 public void setName(String name) {
|
|
208 this.name = name;
|
|
209 }
|
|
210
|
|
211 public boolean isEmpty() {
|
|
212 return isEmpty;
|
|
213 }
|
|
214
|
|
215 public void setEmpty(boolean isEmpty) {
|
|
216 this.isEmpty = isEmpty;
|
|
217 }
|
|
218
|
|
219 public String getAttributeName(String name) {
|
|
220 String key = unquote(name).toLowerCase();
|
|
221 Attribute attr = attrMap.get(key);
|
|
222 return attr==null ? null : attr.name;
|
|
223 }
|
|
224
|
|
225 public String getAttributeValue(String name) {
|
|
226 String key = unquote(name).toLowerCase();
|
|
227 Attribute attr = attrMap.get(key);
|
|
228 return attr==null ? null : attr.value;
|
|
229 }
|
|
230
|
|
231 public boolean hasAttribute(String name) {
|
|
232 String key = unquote(name).toLowerCase();
|
|
233 return attrMap.containsKey(key);
|
|
234 }
|
|
235
|
|
236 public void setAttribute(String name,String value) {
|
|
237 setAttribute(" ",name,"","",value);
|
|
238 }
|
|
239
|
|
240 protected Attribute setAttribute(String spaceBeforeName,String name,String spaceAfterName,String spaceBeforeValue,String value) {
|
|
241 String key = unquote(name).toLowerCase();
|
|
242 Attribute attr = new Attribute(spaceBeforeName,name,spaceAfterName,spaceBeforeValue,value);
|
|
243 return attrMap.put(key,attr);
|
|
244 }
|
|
245
|
|
246 public void removeAttribute(String name) {
|
|
247 String key = unquote(name).toLowerCase();
|
|
248 attrMap.remove(key);
|
|
249 }
|
|
250
|
|
251 public String[] getAttributeNames() {
|
|
252 String[] a = new String[attrMap.size()];
|
|
253 int i = 0;
|
|
254 for( Attribute attr : attrMap.values() ) {
|
|
255 a[i++] = attr.name;
|
|
256 }
|
|
257 return a;
|
|
258 }
|
|
259
|
|
260 @Override public String toString() {
|
|
261 StringBuilder buf = new StringBuilder();
|
|
262 buf.append('<');
|
|
263 buf.append(name);
|
|
264 for( Attribute attr : attrMap.values() ) {
|
|
265 buf.append(attr.spaceBeforeName);
|
|
266 buf.append(attr);
|
|
267 }
|
|
268 buf.append(spaceAtEnd);
|
|
269 if(isEmpty)
|
|
270 buf.append('/');
|
|
271 buf.append('>');
|
|
272 return buf.toString();
|
|
273 }
|
|
274
|
|
275 public static String unquote(String s) {
|
|
276 if( s==null || s.length()<=1 )
|
|
277 return s;
|
|
278 char c = s.charAt(0);
|
|
279 return (c=='"' || c=='\'') && s.charAt(s.length()-1)==c
|
|
280 ? s.substring(1,s.length()-1) : s;
|
|
281 }
|
|
282
|
|
283 public static String quote(String s) {
|
|
284 if( s==null )
|
|
285 return null;
|
|
286 StringBuilder buf = new StringBuilder();
|
|
287 buf.append('"');
|
|
288 int i = 0;
|
|
289 while(true) {
|
|
290 int i2 = s.indexOf('"',i);
|
|
291 if( i2 == -1 ) {
|
|
292 buf.append(s.substring(i));
|
|
293 break;
|
|
294 } else {
|
|
295 buf.append(s.substring(i,i2));
|
|
296 buf.append(""");
|
|
297 i = i2 + 1;
|
|
298 }
|
|
299 }
|
|
300 buf.append('"');
|
|
301 return buf.toString();
|
|
302 }
|
|
303
|
|
304 @Override public boolean equals(Object obj) {
|
|
305 if( obj == this )
|
|
306 return true;
|
|
307 if( !(obj instanceof HtmlTag) )
|
|
308 return false;
|
|
309 HtmlTag tag = (HtmlTag)obj;
|
|
310 return getName().equalsIgnoreCase(tag.getName())
|
|
311 && isEmpty() == tag.isEmpty()
|
|
312 && getAttributeMap().equals(tag.getAttributeMap())
|
|
313 ;
|
|
314 }
|
|
315
|
|
316 public Map<String,String> getAttributeMap() {
|
|
317 Map<String,String> map = new HashMap<String,String>();
|
|
318 for( Attribute attr : attrMap.values() ) {
|
|
319 map.put(unquote(attr.name),unquote(attr.value));
|
|
320 }
|
|
321 return map;
|
|
322 }
|
|
323
|
|
324 public Collection<Attribute> getAttributes() {
|
|
325 return Collections.unmodifiableCollection(attrMap.values());
|
|
326 }
|
|
327
|
|
328 public int getLineNumber() {
|
|
329 return lineNumber;
|
|
330 }
|
|
331
|
|
332 public String getSpaceAtEnd() {
|
|
333 return spaceAtEnd;
|
|
334 }
|
|
335 }
|