|
1 /* |
|
2 * Copyright (c) 2007 Henri Sivonen |
|
3 * Copyright (c) 2008-2011 Mozilla Foundation |
|
4 * |
|
5 * Permission is hereby granted, free of charge, to any person obtaining a |
|
6 * copy of this software and associated documentation files (the "Software"), |
|
7 * to deal in the Software without restriction, including without limitation |
|
8 * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|
9 * and/or sell copies of the Software, and to permit persons to whom the |
|
10 * Software is furnished to do so, subject to the following conditions: |
|
11 * |
|
12 * The above copyright notice and this permission notice shall be included in |
|
13 * all copies or substantial portions of the Software. |
|
14 * |
|
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
|
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
21 * DEALINGS IN THE SOFTWARE. |
|
22 */ |
|
23 |
|
24 package nu.validator.htmlparser.impl; |
|
25 |
|
26 import nu.validator.htmlparser.annotation.Auto; |
|
27 import nu.validator.htmlparser.annotation.IdType; |
|
28 import nu.validator.htmlparser.annotation.Local; |
|
29 import nu.validator.htmlparser.annotation.NsUri; |
|
30 import nu.validator.htmlparser.annotation.Prefix; |
|
31 import nu.validator.htmlparser.annotation.QName; |
|
32 import nu.validator.htmlparser.common.Interner; |
|
33 import nu.validator.htmlparser.common.XmlViolationPolicy; |
|
34 |
|
35 import org.xml.sax.Attributes; |
|
36 import org.xml.sax.SAXException; |
|
37 |
|
38 /** |
|
39 * Be careful with this class. QName is the name in from HTML tokenization. |
|
40 * Otherwise, please refer to the interface doc. |
|
41 * |
|
42 * @version $Id: AttributesImpl.java 206 2008-03-20 14:09:29Z hsivonen $ |
|
43 * @author hsivonen |
|
44 */ |
|
45 public final class HtmlAttributes implements Attributes { |
|
46 |
|
47 // [NOCPP[ |
|
48 |
|
49 private static final AttributeName[] EMPTY_ATTRIBUTENAMES = new AttributeName[0]; |
|
50 |
|
51 private static final String[] EMPTY_STRINGS = new String[0]; |
|
52 |
|
53 // ]NOCPP] |
|
54 |
|
55 public static final HtmlAttributes EMPTY_ATTRIBUTES = new HtmlAttributes( |
|
56 AttributeName.HTML); |
|
57 |
|
58 private int mode; |
|
59 |
|
60 private int length; |
|
61 |
|
62 private @Auto AttributeName[] names; |
|
63 |
|
64 private @Auto String[] values; // XXX perhaps make this @NoLength? |
|
65 |
|
66 // [NOCPP[ |
|
67 |
|
68 private String idValue; |
|
69 |
|
70 private int xmlnsLength; |
|
71 |
|
72 private AttributeName[] xmlnsNames; |
|
73 |
|
74 private String[] xmlnsValues; |
|
75 |
|
76 // ]NOCPP] |
|
77 |
|
78 public HtmlAttributes(int mode) { |
|
79 this.mode = mode; |
|
80 this.length = 0; |
|
81 /* |
|
82 * The length of 5 covers covers 98.3% of elements |
|
83 * according to Hixie |
|
84 */ |
|
85 this.names = new AttributeName[5]; |
|
86 this.values = new String[5]; |
|
87 |
|
88 // [NOCPP[ |
|
89 |
|
90 this.idValue = null; |
|
91 |
|
92 this.xmlnsLength = 0; |
|
93 |
|
94 this.xmlnsNames = HtmlAttributes.EMPTY_ATTRIBUTENAMES; |
|
95 |
|
96 this.xmlnsValues = HtmlAttributes.EMPTY_STRINGS; |
|
97 |
|
98 // ]NOCPP] |
|
99 } |
|
100 /* |
|
101 public HtmlAttributes(HtmlAttributes other) { |
|
102 this.mode = other.mode; |
|
103 this.length = other.length; |
|
104 this.names = new AttributeName[other.length]; |
|
105 this.values = new String[other.length]; |
|
106 // [NOCPP[ |
|
107 this.idValue = other.idValue; |
|
108 this.xmlnsLength = other.xmlnsLength; |
|
109 this.xmlnsNames = new AttributeName[other.xmlnsLength]; |
|
110 this.xmlnsValues = new String[other.xmlnsLength]; |
|
111 // ]NOCPP] |
|
112 } |
|
113 */ |
|
114 |
|
115 void destructor() { |
|
116 clear(0); |
|
117 } |
|
118 |
|
119 /** |
|
120 * Only use with a static argument |
|
121 * |
|
122 * @param name |
|
123 * @return |
|
124 */ |
|
125 public int getIndex(AttributeName name) { |
|
126 for (int i = 0; i < length; i++) { |
|
127 if (names[i] == name) { |
|
128 return i; |
|
129 } |
|
130 } |
|
131 return -1; |
|
132 } |
|
133 |
|
134 /** |
|
135 * Only use with static argument. |
|
136 * |
|
137 * @see org.xml.sax.Attributes#getValue(java.lang.String) |
|
138 */ |
|
139 public String getValue(AttributeName name) { |
|
140 int index = getIndex(name); |
|
141 if (index == -1) { |
|
142 return null; |
|
143 } else { |
|
144 return getValueNoBoundsCheck(index); |
|
145 } |
|
146 } |
|
147 |
|
148 public int getLength() { |
|
149 return length; |
|
150 } |
|
151 |
|
152 /** |
|
153 * Variant of <code>getLocalName(int index)</code> without bounds check. |
|
154 * @param index a valid attribute index |
|
155 * @return the local name at index |
|
156 */ |
|
157 public @Local String getLocalNameNoBoundsCheck(int index) { |
|
158 // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; |
|
159 return names[index].getLocal(mode); |
|
160 } |
|
161 |
|
162 /** |
|
163 * Variant of <code>getURI(int index)</code> without bounds check. |
|
164 * @param index a valid attribute index |
|
165 * @return the namespace URI at index |
|
166 */ |
|
167 public @NsUri String getURINoBoundsCheck(int index) { |
|
168 // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; |
|
169 return names[index].getUri(mode); |
|
170 } |
|
171 |
|
172 /** |
|
173 * Variant of <code>getPrefix(int index)</code> without bounds check. |
|
174 * @param index a valid attribute index |
|
175 * @return the namespace prefix at index |
|
176 */ |
|
177 public @Prefix String getPrefixNoBoundsCheck(int index) { |
|
178 // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; |
|
179 return names[index].getPrefix(mode); |
|
180 } |
|
181 |
|
182 /** |
|
183 * Variant of <code>getValue(int index)</code> without bounds check. |
|
184 * @param index a valid attribute index |
|
185 * @return the attribute value at index |
|
186 */ |
|
187 public String getValueNoBoundsCheck(int index) { |
|
188 // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; |
|
189 return values[index]; |
|
190 } |
|
191 |
|
192 /** |
|
193 * Variant of <code>getAttributeName(int index)</code> without bounds check. |
|
194 * @param index a valid attribute index |
|
195 * @return the attribute name at index |
|
196 */ |
|
197 public AttributeName getAttributeNameNoBoundsCheck(int index) { |
|
198 // CPPONLY: assert index < length && index >= 0: "Index out of bounds"; |
|
199 return names[index]; |
|
200 } |
|
201 |
|
202 // [NOCPP[ |
|
203 |
|
204 /** |
|
205 * Variant of <code>getQName(int index)</code> without bounds check. |
|
206 * @param index a valid attribute index |
|
207 * @return the QName at index |
|
208 */ |
|
209 public @QName String getQNameNoBoundsCheck(int index) { |
|
210 return names[index].getQName(mode); |
|
211 } |
|
212 |
|
213 /** |
|
214 * Variant of <code>getType(int index)</code> without bounds check. |
|
215 * @param index a valid attribute index |
|
216 * @return the attribute type at index |
|
217 */ |
|
218 public @IdType String getTypeNoBoundsCheck(int index) { |
|
219 return (names[index] == AttributeName.ID) ? "ID" : "CDATA"; |
|
220 } |
|
221 |
|
222 public int getIndex(String qName) { |
|
223 for (int i = 0; i < length; i++) { |
|
224 if (names[i].getQName(mode).equals(qName)) { |
|
225 return i; |
|
226 } |
|
227 } |
|
228 return -1; |
|
229 } |
|
230 |
|
231 public int getIndex(String uri, String localName) { |
|
232 for (int i = 0; i < length; i++) { |
|
233 if (names[i].getLocal(mode).equals(localName) |
|
234 && names[i].getUri(mode).equals(uri)) { |
|
235 return i; |
|
236 } |
|
237 } |
|
238 return -1; |
|
239 } |
|
240 |
|
241 public @IdType String getType(String qName) { |
|
242 int index = getIndex(qName); |
|
243 if (index == -1) { |
|
244 return null; |
|
245 } else { |
|
246 return getType(index); |
|
247 } |
|
248 } |
|
249 |
|
250 public @IdType String getType(String uri, String localName) { |
|
251 int index = getIndex(uri, localName); |
|
252 if (index == -1) { |
|
253 return null; |
|
254 } else { |
|
255 return getType(index); |
|
256 } |
|
257 } |
|
258 |
|
259 public String getValue(String qName) { |
|
260 int index = getIndex(qName); |
|
261 if (index == -1) { |
|
262 return null; |
|
263 } else { |
|
264 return getValue(index); |
|
265 } |
|
266 } |
|
267 |
|
268 public String getValue(String uri, String localName) { |
|
269 int index = getIndex(uri, localName); |
|
270 if (index == -1) { |
|
271 return null; |
|
272 } else { |
|
273 return getValue(index); |
|
274 } |
|
275 } |
|
276 |
|
277 public @Local String getLocalName(int index) { |
|
278 if (index < length && index >= 0) { |
|
279 return names[index].getLocal(mode); |
|
280 } else { |
|
281 return null; |
|
282 } |
|
283 } |
|
284 |
|
285 public @QName String getQName(int index) { |
|
286 if (index < length && index >= 0) { |
|
287 return names[index].getQName(mode); |
|
288 } else { |
|
289 return null; |
|
290 } |
|
291 } |
|
292 |
|
293 public @IdType String getType(int index) { |
|
294 if (index < length && index >= 0) { |
|
295 return (names[index] == AttributeName.ID) ? "ID" : "CDATA"; |
|
296 } else { |
|
297 return null; |
|
298 } |
|
299 } |
|
300 |
|
301 public AttributeName getAttributeName(int index) { |
|
302 if (index < length && index >= 0) { |
|
303 return names[index]; |
|
304 } else { |
|
305 return null; |
|
306 } |
|
307 } |
|
308 |
|
309 public @NsUri String getURI(int index) { |
|
310 if (index < length && index >= 0) { |
|
311 return names[index].getUri(mode); |
|
312 } else { |
|
313 return null; |
|
314 } |
|
315 } |
|
316 |
|
317 public @Prefix String getPrefix(int index) { |
|
318 if (index < length && index >= 0) { |
|
319 return names[index].getPrefix(mode); |
|
320 } else { |
|
321 return null; |
|
322 } |
|
323 } |
|
324 |
|
325 public String getValue(int index) { |
|
326 if (index < length && index >= 0) { |
|
327 return values[index]; |
|
328 } else { |
|
329 return null; |
|
330 } |
|
331 } |
|
332 |
|
333 public String getId() { |
|
334 return idValue; |
|
335 } |
|
336 |
|
337 public int getXmlnsLength() { |
|
338 return xmlnsLength; |
|
339 } |
|
340 |
|
341 public @Local String getXmlnsLocalName(int index) { |
|
342 if (index < xmlnsLength && index >= 0) { |
|
343 return xmlnsNames[index].getLocal(mode); |
|
344 } else { |
|
345 return null; |
|
346 } |
|
347 } |
|
348 |
|
349 public @NsUri String getXmlnsURI(int index) { |
|
350 if (index < xmlnsLength && index >= 0) { |
|
351 return xmlnsNames[index].getUri(mode); |
|
352 } else { |
|
353 return null; |
|
354 } |
|
355 } |
|
356 |
|
357 public String getXmlnsValue(int index) { |
|
358 if (index < xmlnsLength && index >= 0) { |
|
359 return xmlnsValues[index]; |
|
360 } else { |
|
361 return null; |
|
362 } |
|
363 } |
|
364 |
|
365 public int getXmlnsIndex(AttributeName name) { |
|
366 for (int i = 0; i < xmlnsLength; i++) { |
|
367 if (xmlnsNames[i] == name) { |
|
368 return i; |
|
369 } |
|
370 } |
|
371 return -1; |
|
372 } |
|
373 |
|
374 public String getXmlnsValue(AttributeName name) { |
|
375 int index = getXmlnsIndex(name); |
|
376 if (index == -1) { |
|
377 return null; |
|
378 } else { |
|
379 return getXmlnsValue(index); |
|
380 } |
|
381 } |
|
382 |
|
383 public AttributeName getXmlnsAttributeName(int index) { |
|
384 if (index < xmlnsLength && index >= 0) { |
|
385 return xmlnsNames[index]; |
|
386 } else { |
|
387 return null; |
|
388 } |
|
389 } |
|
390 |
|
391 // ]NOCPP] |
|
392 |
|
393 void addAttribute(AttributeName name, String value |
|
394 // [NOCPP[ |
|
395 , XmlViolationPolicy xmlnsPolicy |
|
396 // ]NOCPP] |
|
397 ) throws SAXException { |
|
398 // [NOCPP[ |
|
399 if (name == AttributeName.ID) { |
|
400 idValue = value; |
|
401 } |
|
402 |
|
403 if (name.isXmlns()) { |
|
404 if (xmlnsNames.length == xmlnsLength) { |
|
405 int newLen = xmlnsLength == 0 ? 2 : xmlnsLength << 1; |
|
406 AttributeName[] newNames = new AttributeName[newLen]; |
|
407 System.arraycopy(xmlnsNames, 0, newNames, 0, xmlnsNames.length); |
|
408 xmlnsNames = newNames; |
|
409 String[] newValues = new String[newLen]; |
|
410 System.arraycopy(xmlnsValues, 0, newValues, 0, xmlnsValues.length); |
|
411 xmlnsValues = newValues; |
|
412 } |
|
413 xmlnsNames[xmlnsLength] = name; |
|
414 xmlnsValues[xmlnsLength] = value; |
|
415 xmlnsLength++; |
|
416 switch (xmlnsPolicy) { |
|
417 case FATAL: |
|
418 // this is ugly |
|
419 throw new SAXException("Saw an xmlns attribute."); |
|
420 case ALTER_INFOSET: |
|
421 return; |
|
422 case ALLOW: |
|
423 // fall through |
|
424 } |
|
425 } |
|
426 |
|
427 // ]NOCPP] |
|
428 |
|
429 if (names.length == length) { |
|
430 int newLen = length << 1; // The first growth covers virtually |
|
431 // 100% of elements according to |
|
432 // Hixie |
|
433 AttributeName[] newNames = new AttributeName[newLen]; |
|
434 System.arraycopy(names, 0, newNames, 0, names.length); |
|
435 names = newNames; |
|
436 String[] newValues = new String[newLen]; |
|
437 System.arraycopy(values, 0, newValues, 0, values.length); |
|
438 values = newValues; |
|
439 } |
|
440 names[length] = name; |
|
441 values[length] = value; |
|
442 length++; |
|
443 } |
|
444 |
|
445 void clear(int m) { |
|
446 for (int i = 0; i < length; i++) { |
|
447 names[i].release(); |
|
448 names[i] = null; |
|
449 Portability.releaseString(values[i]); |
|
450 values[i] = null; |
|
451 } |
|
452 length = 0; |
|
453 mode = m; |
|
454 // [NOCPP[ |
|
455 idValue = null; |
|
456 for (int i = 0; i < xmlnsLength; i++) { |
|
457 xmlnsNames[i] = null; |
|
458 xmlnsValues[i] = null; |
|
459 } |
|
460 xmlnsLength = 0; |
|
461 // ]NOCPP] |
|
462 } |
|
463 |
|
464 /** |
|
465 * This is used in C++ to release special <code>isindex</code> |
|
466 * attribute values whose ownership is not transferred. |
|
467 */ |
|
468 void releaseValue(int i) { |
|
469 Portability.releaseString(values[i]); |
|
470 } |
|
471 |
|
472 /** |
|
473 * This is only used for <code>AttributeName</code> ownership transfer |
|
474 * in the isindex case to avoid freeing custom names twice in C++. |
|
475 */ |
|
476 void clearWithoutReleasingContents() { |
|
477 for (int i = 0; i < length; i++) { |
|
478 names[i] = null; |
|
479 values[i] = null; |
|
480 } |
|
481 length = 0; |
|
482 } |
|
483 |
|
484 boolean contains(AttributeName name) { |
|
485 for (int i = 0; i < length; i++) { |
|
486 if (name.equalsAnother(names[i])) { |
|
487 return true; |
|
488 } |
|
489 } |
|
490 // [NOCPP[ |
|
491 for (int i = 0; i < xmlnsLength; i++) { |
|
492 if (name.equalsAnother(xmlnsNames[i])) { |
|
493 return true; |
|
494 } |
|
495 } |
|
496 // ]NOCPP] |
|
497 return false; |
|
498 } |
|
499 |
|
500 public void adjustForMath() { |
|
501 mode = AttributeName.MATHML; |
|
502 } |
|
503 |
|
504 public void adjustForSvg() { |
|
505 mode = AttributeName.SVG; |
|
506 } |
|
507 |
|
508 public HtmlAttributes cloneAttributes(Interner interner) |
|
509 throws SAXException { |
|
510 assert (length == 0 |
|
511 // [NOCPP[ |
|
512 && xmlnsLength == 0 |
|
513 // ]NOCPP] |
|
514 ) |
|
515 || mode == 0 || mode == 3; |
|
516 HtmlAttributes clone = new HtmlAttributes(0); |
|
517 for (int i = 0; i < length; i++) { |
|
518 clone.addAttribute(names[i].cloneAttributeName(interner), |
|
519 Portability.newStringFromString(values[i]) |
|
520 // [NOCPP[ |
|
521 , XmlViolationPolicy.ALLOW |
|
522 // ]NOCPP] |
|
523 ); |
|
524 } |
|
525 // [NOCPP[ |
|
526 for (int i = 0; i < xmlnsLength; i++) { |
|
527 clone.addAttribute(xmlnsNames[i], xmlnsValues[i], |
|
528 XmlViolationPolicy.ALLOW); |
|
529 } |
|
530 // ]NOCPP] |
|
531 return clone; // XXX!!! |
|
532 } |
|
533 |
|
534 public boolean equalsAnother(HtmlAttributes other) { |
|
535 assert mode == 0 || mode == 3 : "Trying to compare attributes in foreign content."; |
|
536 int otherLength = other.getLength(); |
|
537 if (length != otherLength) { |
|
538 return false; |
|
539 } |
|
540 for (int i = 0; i < length; i++) { |
|
541 // Work around the limitations of C++ |
|
542 boolean found = false; |
|
543 // The comparing just the local names is OK, since these attribute |
|
544 // holders are both supposed to belong to HTML formatting elements |
|
545 @Local String ownLocal = names[i].getLocal(AttributeName.HTML); |
|
546 for (int j = 0; j < otherLength; j++) { |
|
547 if (ownLocal == other.names[j].getLocal(AttributeName.HTML)) { |
|
548 found = true; |
|
549 if (!Portability.stringEqualsString(values[i], other.values[j])) { |
|
550 return false; |
|
551 } |
|
552 } |
|
553 } |
|
554 if (!found) { |
|
555 return false; |
|
556 } |
|
557 } |
|
558 return true; |
|
559 } |
|
560 |
|
561 // [NOCPP[ |
|
562 |
|
563 void processNonNcNames(TreeBuilder<?> treeBuilder, XmlViolationPolicy namePolicy) throws SAXException { |
|
564 for (int i = 0; i < length; i++) { |
|
565 AttributeName attName = names[i]; |
|
566 if (!attName.isNcName(mode)) { |
|
567 String name = attName.getLocal(mode); |
|
568 switch (namePolicy) { |
|
569 case ALTER_INFOSET: |
|
570 names[i] = AttributeName.create(NCName.escapeName(name)); |
|
571 // fall through |
|
572 case ALLOW: |
|
573 if (attName != AttributeName.XML_LANG) { |
|
574 treeBuilder.warn("Attribute \u201C" + name + "\u201D is not serializable as XML 1.0."); |
|
575 } |
|
576 break; |
|
577 case FATAL: |
|
578 treeBuilder.fatal("Attribute \u201C" + name + "\u201D is not serializable as XML 1.0."); |
|
579 break; |
|
580 } |
|
581 } |
|
582 } |
|
583 } |
|
584 |
|
585 public void merge(HtmlAttributes attributes) throws SAXException { |
|
586 int len = attributes.getLength(); |
|
587 for (int i = 0; i < len; i++) { |
|
588 AttributeName name = attributes.getAttributeNameNoBoundsCheck(i); |
|
589 if (!contains(name)) { |
|
590 addAttribute(name, attributes.getValueNoBoundsCheck(i), XmlViolationPolicy.ALLOW); |
|
591 } |
|
592 } |
|
593 } |
|
594 |
|
595 |
|
596 // ]NOCPP] |
|
597 |
|
598 } |