Source code: nanoxml/kXMLElement.java
1 /*
2 * This file is part of NanoXML.
3 *
4 * $Revision: 1.2 $
5 * $Date: 2002/06/05 01:27:16 $
6 * $Name: build_20030502 $
7 *
8 * Copyright (C) 2000 Marc De Scheemaecker, All Rights Reserved.
9 *
10 * This software is provided 'as-is', without any express or implied warranty.
11 * In no event will the authors be held liable for any damages arising from the
12 * use of this software.
13 *
14 * Permission is granted to anyone to use this software for any purpose,
15 * including commercial applications, and to alter it and redistribute it
16 * freely, subject to the following restrictions:
17 *
18 * 1. The origin of this software must not be misrepresented; you must not
19 * claim that you wrote the original software. If you use this software in
20 * a product, an acknowledgment in the product documentation would be
21 * appreciated but is not required.
22 *
23 * 2. Altered source versions must be plainly marked as such, and must not be
24 * misrepresented as being the original software.
25 *
26 * 3. This notice may not be removed or altered from any source distribution.
27 */
28
29 package nanoxml;
30
31 import java.io.IOException;
32 import java.io.Reader;
33 import java.io.Writer;
34 import java.util.Enumeration;
35 import java.util.Hashtable;
36 import java.util.Vector;
37
38
39 /**
40 * kXMLElement is a representation of an XML object. The object is able to
41 * parse XML code. <P>
42 *
43 * Note that NanoXML is not 100% XML 1.0 compliant:
44 * <UL>
45 * <LI> The parser is non-validating.
46 * <LI> The DTD is fully ignored, including <CODE><!ENTITY...></CODE>.
47 *
48 * <LI> There is no support for mixed content (elements containing both
49 * subelements and CDATA elements)
50 * </UL>
51 * <P>
52 *
53 * You can opt to use a SAX compatible API, by including both <CODE>nanoxml.jar</CODE>
54 * and <CODE>nanoxml-sax.jar</CODE> in your classpath and setting the property
55 * <CODE>org.xml.sax.parser</CODE> to <CODE>nanoxml.sax.SAXParser</CODE> <P>
56 *
57 * $Revision: 1.2 $<BR>
58 * $Date: 2002/06/05 01:27:16 $<P>
59 *
60 *
61 *
62 * @author Marc De Scheemaecker <<A HREF="mailto:Marc.DeScheemaecker@advalvas.be"
63 * >Marc.DeScheemaecker@advalvas.be</A> >
64 * @created May 28, 2002
65 * @see nanoxml.kXMLParseException
66 * @version 1.6
67 */
68 public class kXMLElement {
69 /**
70 * Major version of NanoXML.
71 */
72 public final static int NANOXML_MAJOR_VERSION = 1;
73
74 /**
75 * Minor version of NanoXML.
76 */
77 public final static int NANOXML_MINOR_VERSION = 6;
78
79 /**
80 * The attributes given to the object.
81 */
82 private Hashtable attributes;
83
84 /**
85 * Parent of the object.
86 */
87 private kXMLElement parent;
88
89 /**
90 * Subobjects of the object. The subobjects are of class kXMLElement
91 * themselves.
92 */
93 private Vector children;
94
95 /**
96 * The class of the object (the name indicated in the tag).
97 */
98 private String tagName;
99
100 /**
101 * The #PCDATA content of the object. If there is no such content, this
102 * field is null.
103 */
104 private String contents;
105
106 /**
107 * Conversion table for &...; tags.
108 */
109 private Hashtable conversionTable;
110
111 /**
112 * Whether to skip leading whitespace in CDATA.
113 */
114 private boolean skipLeadingWhitespace;
115
116 /**
117 * The line number where the element starts.
118 */
119 private int lineNr;
120
121 /**
122 * Whether the parsing is case sensitive.
123 */
124 private boolean ignoreCase;
125
126
127 /**
128 * Creates a new XML element. The following settings are used:
129 * <DL>
130 * <DT> Conversion table</DT>
131 * <DD> Minimal XML conversions: <CODE>&amp; &lt; &gt;
132 * &apos; &quot;</CODE></DD>
133 * <DT> Skip whitespace in contents</DT>
134 * <DD> <CODE>false</CODE></DD>
135 * <DT> Ignore Case</DT>
136 * <DD> <CODE>true</CODE></DD>
137 * </DL>
138 *
139 *
140 * @see nanoxml.kXMLElement#kXMLElement(java.util.Hashtable)
141 * @see nanoxml.kXMLElement#kXMLElement(boolean)
142 * @see nanoxml.kXMLElement#kXMLElement(java.util.Hashtable,boolean)
143 */
144 public kXMLElement() {
145 this(new Hashtable(), false, true, true);
146 }
147
148
149 /**
150 * Creates a new XML element. The following settings are used:
151 * <DL>
152 * <DT> Conversion table</DT>
153 * <DD> <I>conversionTable</I> combined with the minimal XML
154 * conversions: <CODE>&amp; &lt; &gt;
155 * &apos; &quot;</CODE></DD>
156 * <DT> Skip whitespace in contents</DT>
157 * <DD> <CODE>false</CODE></DD>
158 * <DT> Ignore Case</DT>
159 * <DD> <CODE>true</CODE></DD>
160 * </DL>
161 *
162 *
163 * @param conversionTable Description of the Parameter
164 * @see nanoxml.kXMLElement#kXMLElement()
165 * @see nanoxml.kXMLElement#kXMLElement(boolean)
166 * @see nanoxml.kXMLElement#kXMLElement(java.util.Hashtable,boolean)
167 */
168 public kXMLElement(Hashtable conversionTable) {
169 this(conversionTable, false, true, true);
170 }
171
172
173 /**
174 * Creates a new XML element. The following settings are used:
175 * <DL>
176 * <DT> Conversion table</DT>
177 * <DD> Minimal XML conversions: <CODE>&amp; &lt; &gt;
178 * &apos; &quot;</CODE></DD>
179 * <DT> Skip whitespace in contents</DT>
180 * <DD> <I>skipLeadingWhitespace</I> </DD>
181 * <DT> Ignore Case</DT>
182 * <DD> <CODE>true</CODE></DD>
183 * </DL>
184 *
185 *
186 * @param skipLeadingWhitespace Description of the Parameter
187 * @see nanoxml.kXMLElement#kXMLElement()
188 * @see nanoxml.kXMLElement#kXMLElement(java.util.Hashtable)
189 * @see nanoxml.kXMLElement#kXMLElement(java.util.Hashtable,boolean)
190 */
191 public kXMLElement(boolean skipLeadingWhitespace) {
192 this(new Hashtable(), skipLeadingWhitespace, true, true);
193 }
194
195
196 /**
197 * Creates a new XML element. The following settings are used:
198 * <DL>
199 * <DT> Conversion table</DT>
200 * <DD> <I>conversionTable</I> combined with the minimal XML
201 * conversions: <CODE>&amp; &lt; &gt;
202 * &apos; &quot;</CODE></DD>
203 * <DT> Skip whitespace in contents</DT>
204 * <DD> <I>skipLeadingWhitespace</I> </DD>
205 * <DT> Ignore Case</DT>
206 * <DD> <CODE>true</CODE></DD>
207 * </DL>
208 *
209 *
210 * @param conversionTable Description of the Parameter
211 * @param skipLeadingWhitespace Description of the Parameter
212 * @see nanoxml.kXMLElement#kXMLElement()
213 * @see nanoxml.kXMLElement#kXMLElement(boolean)
214 * @see nanoxml.kXMLElement#kXMLElement(java.util.Hashtable)
215 */
216 public kXMLElement(Hashtable conversionTable,
217 boolean skipLeadingWhitespace) {
218 this(conversionTable, skipLeadingWhitespace, true, true);
219 }
220
221
222 /**
223 * Creates a new XML element. The following settings are used:
224 * <DL>
225 * <DT> Conversion table</DT>
226 * <DD> <I>conversionTable</I> , eventually combined with the minimal
227 * XML conversions: <CODE>&amp; &lt; &gt;
228 * &apos; &quot;</CODE> (depending on <I>
229 * fillBasicConversionTable</I> )</DD>
230 * <DT> Skip whitespace in contents</DT>
231 * <DD> <I>skipLeadingWhitespace</I> </DD>
232 * <DT> Ignore Case</DT>
233 * <DD> <I>ignoreCase</I> </DD>
234 * </DL>
235 * <P>
236 *
237 * This constructor should <I>only</I> be called from kXMLElement itself
238 * to create child elements.
239 *
240 * @param conversionTable Description of the Parameter
241 * @param skipLeadingWhitespace Description of the Parameter
242 * @param ignoreCase Description of the Parameter
243 * @see nanoxml.kXMLElement#kXMLElement()
244 * @see nanoxml.kXMLElement#XMLElement(boolean)
245 * @see nanoxml.kXMLElement#XMLElement(java.util.Hashtable)
246 * @see nanoxml.kXMLElement#XMLElement(java.util.Hashtable,boolean)
247 */
248 public kXMLElement(Hashtable conversionTable,
249 boolean skipLeadingWhitespace,
250 boolean ignoreCase) {
251 this(conversionTable, skipLeadingWhitespace, true, ignoreCase);
252 }
253
254
255 /**
256 * Creates a new XML element. The following settings are used:
257 * <DL>
258 * <DT> Conversion table</DT>
259 * <DD> <I>conversionTable</I> , eventually combined with the minimal
260 * XML conversions: <CODE>&amp; &lt; &gt;
261 * &apos; &quot;</CODE> (depending on <I>
262 * fillBasicConversionTable</I> )</DD>
263 * <DT> Skip whitespace in contents</DT>
264 * <DD> <I>skipLeadingWhitespace</I> </DD>
265 * <DT> Ignore Case</DT>
266 * <DD> <I>ignoreCase</I> </DD>
267 * </DL>
268 * <P>
269 *
270 * This constructor should <I>only</I> be called from kXMLElement itself
271 * to create child elements.
272 *
273 * @param conversionTable Description of the Parameter
274 * @param skipLeadingWhitespace Description of the Parameter
275 * @param fillBasicConversionTable Description of the Parameter
276 * @param ignoreCase Description of the Parameter
277 * @see nanoxml.kXMLElement#XMLElement()
278 * @see nanoxml.kXMLElement#XMLElement(boolean)
279 * @see nanoxml.kXMLElement#XMLElement(java.util.Hashtable)
280 * @see nanoxml.kXMLElement#XMLElement(java.util.Hashtable,boolean)
281 */
282 protected kXMLElement(Hashtable conversionTable,
283 boolean skipLeadingWhitespace,
284 boolean fillBasicConversionTable,
285 boolean ignoreCase) {
286 this.ignoreCase = ignoreCase;
287 this.skipLeadingWhitespace = skipLeadingWhitespace;
288 this.tagName = null;
289 this.contents = "";
290 this.attributes = null;
291 this.children = null;
292 this.conversionTable = conversionTable;
293 this.lineNr = 0;
294
295 if (fillBasicConversionTable) {
296 this.conversionTable.put("lt", "<");
297 this.conversionTable.put("gt", ">");
298 this.conversionTable.put("quot", "\"");
299 this.conversionTable.put("apos", "'");
300 this.conversionTable.put("amp", "&");
301 }
302 }
303
304
305 /**
306 * Gets its parent.
307 *
308 * @return The parent value
309 */
310 public kXMLElement getParent() {
311 return parent;
312 }
313
314
315 /**
316 * Sets the parent of this instance.
317 *
318 * @param parent The new parent value
319 */
320 private void setParent(kXMLElement parent) {
321 this.parent = parent;
322 }
323
324
325 /**
326 * Adds a subobject.
327 *
328 * @param child The feature to be added to the Child attribute
329 */
330 public void addChild(kXMLElement child) {
331 child.setParent(this);
332 getChildren().addElement(child);
333 }
334
335
336 /**
337 * Gets the attributes attribute of the kXMLElement object
338 *
339 * @return The attributes value
340 */
341 private Hashtable getAttributes() {
342 if (attributes == null) {
343 attributes = new Hashtable();
344 }
345 return attributes;
346 }
347
348
349 /**
350 * Adds a property. If the element is case insensitive, the property name
351 * is capitalized.
352 *
353 * @param key The feature to be added to the Property attribute
354 * @param value The feature to be added to the Property attribute
355 */
356 public void addProperty(String key,
357 Object value) {
358 if (ignoreCase) {
359 key = key.toUpperCase();
360 }
361
362 getAttributes().put(key, value.toString());
363 }
364
365
366 /**
367 * Adds a property. If the element is case insensitive, the property name
368 * is capitalized.
369 *
370 * @param key The feature to be added to the Property attribute
371 * @param value The feature to be added to the Property attribute
372 */
373 public void addProperty(String key,
374 int value) {
375 if (ignoreCase) {
376 key = key.toUpperCase();
377 }
378
379 getAttributes().put(key, Integer.toString(value));
380 }
381
382
383 /**
384 * Returns the number of subobjects of the object.
385 *
386 * @return Description of the Return Value
387 */
388 public int countChildren() {
389 return (children != null) ? children.size() : 0;
390 }
391
392
393 /**
394 * Enumerates the attribute names.
395 *
396 * @return Description of the Return Value
397 */
398 public Enumeration enumeratePropertyNames() {
399 return getAttributes().keys();
400 }
401
402
403 /**
404 * Enumerates the subobjects of the object.
405 *
406 * @return Description of the Return Value
407 */
408 public Enumeration enumerateChildren() {
409 return getChildren().elements();
410 }
411
412
413 /**
414 * Returns the subobjects of the object.
415 *
416 * @return The children value
417 */
418 public Vector getChildren() {
419 if (children == null) {
420 children = new Vector();
421 }
422 return children;
423 }
424
425
426 /**
427 * Returns the #PCDATA content of the object. If there is no such content,
428 * <CODE>null</CODE> is returned.
429 *
430 * @return The contents value
431 */
432 public String getContents() {
433 return contents;
434 }
435
436
437 /**
438 * Returns the line nr on which the element is found.
439 *
440 * @return The lineNr value
441 */
442 public int getLineNr() {
443 return lineNr;
444 }
445
446
447 /**
448 * Returns a property by looking up a key in a hashtable. If the property
449 * doesn't exist, the value corresponding to defaultValue is returned.
450 *
451 * @param key Description of the Parameter
452 * @param valueSet Description of the Parameter
453 * @param defaultValue Description of the Parameter
454 * @return The intProperty value
455 */
456 public int getIntProperty(String key,
457 Hashtable valueSet,
458 String defaultValue) {
459 String val = getProperty(attributes, key);
460 Integer result;
461
462 if (ignoreCase) {
463 key = key.toUpperCase();
464 }
465
466 if (val == null) {
467 val = defaultValue;
468 }
469
470 try {
471 result = (Integer) (valueSet.get(val));
472 } catch (ClassCastException e) {
473 throw invalidValueSet(key);
474 }
475
476 if (result == null) {
477 throw invalidValue(key, val, lineNr);
478 }
479
480 return result.intValue();
481 }
482
483
484 /**
485 * @param h Description of the Parameter
486 * @param key Description of the Parameter
487 * @return The property value
488 */
489
490 public static String getProperty(Hashtable h, String key) {
491 if (h == null) {
492 return null;
493 }
494 return (String) h.get(key);
495 }
496
497
498 /**
499 * @param h Description of the Parameter
500 * @param key Description of the Parameter
501 * @param def Description of the Parameter
502 * @return The property value
503 */
504
505 public static String getProperty(Hashtable h, String key, String def) {
506 if (h == null) {
507 return def;
508 }
509 String val = (String) h.get(key);
510 return (val != null) ? val : def;
511 }
512
513
514 /**
515 * Returns a property of the object. If there is no such property, this
516 * method returns <CODE>null</CODE>.
517 *
518 * @param key Description of the Parameter
519 * @return The property value
520 */
521 public String getProperty(String key) {
522 if (ignoreCase) {
523 key = key.toUpperCase();
524 }
525
526 return getProperty(attributes, key);
527 }
528
529
530 /**
531 * Returns a property of the object. If the property doesn't exist, <I>
532 * defaultValue</I> is returned.
533 *
534 * @param key Description of the Parameter
535 * @param defaultValue Description of the Parameter
536 * @return The property value
537 */
538 public String getProperty(String key,
539 String defaultValue) {
540 if (ignoreCase) {
541 key = key.toUpperCase();
542 }
543
544 return getProperty(attributes, key, defaultValue);
545 }
546
547
548 /**
549 * Returns an integer property of the object. If the property doesn't
550 * exist, <I>defaultValue</I> is returned.
551 *
552 * @param key Description of the Parameter
553 * @param defaultValue Description of the Parameter
554 * @return The property value
555 */
556 public int getProperty(String key,
557 int defaultValue) {
558 if (ignoreCase) {
559 key = key.toUpperCase();
560 }
561
562 String val = getProperty(attributes, key);
563
564 if (val == null) {
565 return defaultValue;
566 } else {
567 try {
568 return Integer.parseInt(val);
569 } catch (NumberFormatException e) {
570 throw invalidValue(key, val, lineNr);
571 }
572 }
573 }
574
575
576 /**
577 * Returns a boolean property of the object. If the property is missing,
578 * <I>defaultValue</I> is returned.
579 *
580 * @param key Description of the Parameter
581 * @param trueValue Description of the Parameter
582 * @param falseValue Description of the Parameter
583 * @param defaultValue Description of the Parameter
584 * @return The property value
585 */
586 public boolean getProperty(String key,
587 String trueValue,
588 String falseValue,
589 boolean defaultValue) {
590 if (ignoreCase) {
591 key = key.toUpperCase();
592 }
593
594 String val = getProperty(attributes, key);
595
596 if (val == null) {
597 return defaultValue;
598 } else if (val.equals(trueValue)) {
599 return true;
600 } else if (val.equals(falseValue)) {
601 return false;
602 } else {
603 throw invalidValue(key, val, lineNr);
604 }
605 }
606
607
608 /**
609 * Returns a property by looking up a key in the hashtable <I>valueSet</I>
610 * If the property doesn't exist, the value corresponding to <I>
611 * defaultValue</I> is returned.
612 *
613 * @param key Description of the Parameter
614 * @param valueSet Description of the Parameter
615 * @param defaultValue Description of the Parameter
616 * @return The property value
617 */
618 public Object getProperty(String key,
619 Hashtable valueSet,
620 String defaultValue) {
621 if (ignoreCase) {
622 key = key.toUpperCase();
623 }
624
625 String val = getProperty(attributes, key);
626
627 if (val == null) {
628 val = defaultValue;
629 }
630
631 Object result = valueSet.get(val);
632
633 if (result == null) {
634 throw invalidValue(key, val, lineNr);
635 }
636
637 return result;
638 }
639
640
641 /**
642 * Returns a property by looking up a key in the hashtable <I>valueSet</I>
643 * . If the property doesn't exist, the value corresponding to <I>
644 * defaultValue</I> is returned.
645 *
646 * @param key Description of the Parameter
647 * @param valueSet Description of the Parameter
648 * @param defaultValue Description of the Parameter
649 * @return The stringProperty value
650 */
651 public String getStringProperty(String key,
652 Hashtable valueSet,
653 String defaultValue) {
654 if (ignoreCase) {
655 key = key.toUpperCase();
656 }
657
658 String val = getProperty(attributes, key);
659 String result;
660
661 if (val == null) {
662 val = defaultValue;
663 }
664
665 try {
666 result = (String) (valueSet.get(val));
667 } catch (ClassCastException e) {
668 throw invalidValueSet(key);
669 }
670
671 if (result == null) {
672 throw invalidValue(key, val, lineNr);
673 }
674
675 return result;
676 }
677
678
679 /**
680 * Returns a property by looking up a key in the hashtable <I>valueSet</I>
681 * . If the value is not defined in the hashtable, the value is considered
682 * to be an integer. If the property doesn't exist, the value
683 * corresponding to <I>defaultValue</I> is returned.
684 *
685 * @param key Description of the Parameter
686 * @param valueSet Description of the Parameter
687 * @param defaultValue Description of the Parameter
688 * @return The specialIntProperty value
689 */
690 public int getSpecialIntProperty(String key,
691 Hashtable valueSet,
692 String defaultValue) {
693 if (ignoreCase) {
694 key = key.toUpperCase();
695 }
696
697 String val = getProperty(attributes, key);
698 Integer result;
699
700 if (val == null) {
701 val = defaultValue;
702 }
703
704 try {
705 result = (Integer) (valueSet.get(val));
706 } catch (ClassCastException e) {
707 throw invalidValueSet(key);
708 }
709
710 if (result == null) {
711 try {
712 return Integer.parseInt(val);
713 } catch (NumberFormatException e) {
714 throw invalidValue(key, val, lineNr);
715 }
716 }
717
718 return result.intValue();
719 }
720
721
722 /**
723 * Returns the class (i.e. the name indicated in the tag) of the object.
724 *
725 * @return The tagName value
726 */
727 public String getTagName() {
728 return tagName;
729 }
730
731
732 /**
733 * Checks whether a character may be part of an identifier.
734 *
735 * @param ch Description of the Parameter
736 * @return The identifierChar value
737 */
738 private boolean isIdentifierChar(char ch) {
739 return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z'))
740 || ((ch >= '0') && (ch <= '9')) || (".-_:".indexOf(ch) >= 0));
741 }
742
743
744 /**
745 * Reads an XML definition from a java.io.Reader and parses it.
746 *
747 * @param reader Description of the Parameter
748 * @exception IOException Description of the Exception
749 * @exception kXMLParseException Description of the Exception
750 */
751 public void parseFromReader(Reader reader)
752 throws IOException, kXMLParseException {
753 parseFromReader(reader, 1);
754 }
755
756
757 /**
758 * Reads an XML definition from a java.io.Reader and parses it.
759 *
760 * @param reader Description of the Parameter
761 * @param startingLineNr Description of the Parameter
762 * @exception IOException Description of the Exception
763 * @exception kXMLParseException Description of the Exception
764 */
765 public void parseFromReader(Reader reader,
766 int startingLineNr)
767 throws IOException, kXMLParseException {
768 int blockSize = 4096;
769 char[] input = null;
770 int size = 0;
771
772 for (; ; ) {
773 if (input == null) {
774 input = new char[blockSize];
775 } else {
776 char[] oldInput = input;
777 input = new char[input.length + blockSize];
778 System.arraycopy(oldInput, 0, input, 0, oldInput.length);
779 }
780
781 int charsRead = reader.read(input, size, blockSize);
782
783 if (charsRead < 0) {
784 break;
785 }
786
787 size += charsRead;
788 }
789
790 parseCharArray(input, 0, size, startingLineNr);
791 }
792
793
794 /**
795 * Parses an XML definition.
796 *
797 * @param string Description of the Parameter
798 * @exception kXMLParseException Description of the Exception
799 */
800 public void parseString(String string)
801 throws kXMLParseException {
802 parseCharArray(string.toCharArray(), 0, string.length(), 1);
803 }
804
805
806 /**
807 * Parses an XML definition starting at <I>offset</I> .
808 *
809 * @param string Description of the Parameter
810 * @param offset Description of the Parameter
811 * @return the offset of the string
812 * following the XML data
813 * @exception kXMLParseException Description of the Exception
814 */
815 public int parseString(String string,
816 int offset)
817 throws kXMLParseException {
818 return parseCharArray(string.toCharArray(), offset,
819 string.length(), 1);
820 }
821
822
823 /**
824 * Parses an XML definition starting at <I>offset</I> .
825 *
826 * @param string Description of the Parameter
827 * @param offset Description of the Parameter
828 * @param end Description of the Parameter
829 * @return the offset of the string
830 * following the XML data (<= end)
831 * @exception kXMLParseException Description of the Exception
832 */
833 public int parseString(String string,
834 int offset,
835 int end)
836 throws kXMLParseException {
837 return parseCharArray(string.toCharArray(), offset, end, 1);
838 }
839
840
841 /**
842 * Parses an XML definition starting at <I>offset</I> .
843 *
844 * @param string Description of the Parameter
845 * @param offset Description of the Parameter
846 * @param end Description of the Parameter
847 * @param startingLineNr Description of the Parameter
848 * @return the offset of the string
849 * following the XML data (<= end)
850 * @exception kXMLParseException Description of the Exception
851 */
852 public int parseString(String string,
853 int offset,
854 int end,
855 int startingLineNr)
856 throws kXMLParseException {
857 return parseCharArray(string.toCharArray(), offset, end,
858 startingLineNr);
859 }
860
861
862 /**
863 * Parses an XML definition starting at <I>offset</I> .
864 *
865 * @param input Description of the Parameter
866 * @param offset Description of the Parameter
867 * @param end Description of the Parameter
868 * @return the offset of the array
869 * following the XML data (<= end)
870 * @exception kXMLParseException Description of the Exception
871 */
872 public int parseCharArray(char[] input,
873 int offset,
874 int end)
875 throws kXMLParseException {
876 return parseCharArray(input, offset, end, 1);
877 }
878
879
880 /**
881 * Parses an XML definition starting at <I>offset</I> .
882 *
883 * @param input Description of the Parameter
884 * @param offset Description of the Parameter
885 * @param end Description of the Parameter
886 * @param startingLineNr Description of the Parameter
887 * @return the offset of the array
888 * following the XML data (<= end)
889 * @exception kXMLParseException Description of the Exception
890 */
891 public int parseCharArray(char[] input,
892 int offset,
893 int end,
894 int startingLineNr)
895 throws kXMLParseException {
896 int[] lineNr = new int[1];
897 lineNr[0] = startingLineNr;
898 return parseCharArray(input, offset, end, lineNr);
899 }
900
901
902 /**
903 * Parses an XML definition starting at <I>offset</I> .
904 *
905 * @param input Description of the Parameter
906 * @param offset Description of the Parameter
907 * @param end Description of the Parameter
908 * @param currentLineNr Description of the Parameter
909 * @return the offset of the array
910 * following the XML data (<= end)
911 * @exception kXMLParseException Description of the Exception
912 */
913 private int parseCharArray(char[] input,
914 int offset,
915 int end,
916 int[] currentLineNr)
917 throws kXMLParseException {
918 this.lineNr = currentLineNr[0];
919 this.tagName = null;
920 this.contents = null;
921 this.attributes = null;
922 this.children = null;
923
924 try {
925 offset = this.skipWhitespace(input, offset, end, currentLineNr);
926 } catch (kXMLParseException e) {
927 return offset;
928 }
929
930 offset = skipPreamble(input, offset, end, currentLineNr);
931 offset = scanTagName(input, offset, end, currentLineNr);
932 lineNr = currentLineNr[0];
933 offset = scanAttributes(input, offset, end, currentLineNr);
934 int[] contentOffset = new int[1];
935 int[] contentSize = new int[1];
936 int contentLineNr = currentLineNr[0];
937 offset = scanContent(input, offset, end,
938 contentOffset, contentSize, currentLineNr);
939
940 if (contentSize[0] > 0) {
941 scanChildren(input, contentOffset[0], contentSize[0],
942 contentLineNr);
943
944 if (children != null && children.size() > 0) {
945 contents = null;
946 } else {
947 processContents(input, contentOffset[0],
948 contentSize[0], contentLineNr);
949
950 for (int i = 0; i < contents.length(); i++) {
951 if (contents.charAt(i) > ' ') {
952 return offset;
953 }
954 }
955
956 contents = null;
957 }
958 }
959
960 return offset;
961 }
962
963
964 /**
965 * Decodes the entities in the contents and, if skipLeadingWhitespace is
966 * <CODE>true</CODE>, removes extraneous whitespaces after newlines and
967 * convert those newlines into spaces.
968 *
969 * @param input Description of the Parameter
970 * @param contentOffset Description of the Parameter
971 * @param contentSize Description of the Parameter
972 * @param contentLineNr Description of the Parameter
973 * @exception kXMLParseException Description of the Exception
974 * @see nanoxml.kXMLElement#decodeString
975 */
976 private void processContents(char[] input,
977 int contentOffset,
978 int contentSize,
979 int contentLineNr)
980 throws kXMLParseException {
981 int[] lineNr = new int[1];
982 lineNr[0] = contentLineNr;
983
984 if (!skipLeadingWhitespace) {
985 String str = new String(input, contentOffset, contentSize);
986 contents = decodeString(str, lineNr[0]);
987 return;
988 }
989
990 StringBuffer result = new StringBuffer(contentSize);
991 int end = contentSize + contentOffset;
992
993 for (int i = contentOffset; i < end; i++) {
994 char ch = input[i];
995
996 // The end of the contents is always a < character, so there's
997 // no danger for bounds violation
998 while ((ch == '\r') || (ch == '\n')) {
999 lineNr[0]++;
1000 result.append(ch);
1001
1002 i++;
1003 ch = input[i];
1004
1005 if (ch != '\n') {
1006 result.append(ch);
1007 }
1008
1009 do {
1010 i++;
1011 ch = input[i];
1012 } while ((ch == ' ') || (ch == '\t'));
1013 }
1014
1015 if (i < end) {
1016 result.append(input[i]);
1017 }
1018 }
1019
1020 contents = decodeString(result.toString(), lineNr[0]);
1021 }
1022
1023
1024 /**
1025 * Removes a child object. If the object is not a child, nothing happens.
1026 *
1027 * @param child Description of the Parameter
1028 */
1029 public void removeChild(kXMLElement child) {
1030 if (children != null) {
1031 children.removeElement(child);
1032 }
1033 }
1034
1035
1036 /**
1037 * Removes an attribute.
1038 *
1039 * @param key Description of the Parameter
1040 */
1041 public void removeChild(String key) {
1042 if (attributes != null) {
1043 if (ignoreCase) {
1044 key = key.toUpperCase();
1045 }
1046
1047 attributes.remove(key);
1048 }
1049 }
1050
1051
1052 /**
1053 * Scans the attributes of the object.
1054 *
1055 * @param input Description of the Parameter
1056 * @param offset Description of the Parameter
1057 * @param end Description of the Parameter
1058 * @param lineNr Description of the Parameter
1059 * @return the offset in the string
1060 * following the attributes, so that input[offset] in { '/', '>' }
1061 * @exception kXMLParseException Description of the Exception
1062 * @see nanoxml.kXMLElement#scanOneAttribute
1063 */
1064 private int scanAttributes(char[] input,
1065 int offset,
1066 int end,
1067 int[] lineNr)
1068 throws kXMLParseException {
1069 String key;
1070 String value;
1071
1072 for (; ; ) {
1073 offset = skipWhitespace(input, offset, end, lineNr);
1074
1075 char ch = input[offset];
1076
1077 if ((ch == '/') || (ch == '>')) {
1078 break;
1079 }
1080
1081 offset = scanOneAttribute(input, offset, end, lineNr);
1082 }
1083
1084 return offset;
1085 }
1086
1087
1088 /**
1089 * !!! Searches the content for child objects. If such objects exist, the
1090 * content is reduced to <CODE>null</CODE>.
1091 *
1092 * @param input Description of the Parameter
1093 * @param contentOffset Description of the Parameter
1094 * @param contentSize Description of the Parameter
1095 * @param contentLineNr Description of the Parameter
1096 * @exception kXMLParseException Description of the Exception
1097 * @see nanoxml.kXMLElement#parseCharArray
1098 */
1099 protected void scanChildren(char[] input,
1100 int contentOffset,
1101 int contentSize,
1102 int contentLineNr)
1103 throws kXMLParseException {
1104 int end = contentOffset + contentSize;
1105 int offset = contentOffset;
1106 int lineNr[] = new int[1];
1107 lineNr[0] = contentLineNr;
1108
1109 while (offset < end) {
1110 try {
1111 offset = skipWhitespace(input, offset, end, lineNr);
1112 } catch (kXMLParseException e) {
1113 return;
1114 }
1115
1116 if ((input[offset] != '<')
1117 || ((input[offset + 1] == '!') && (input[offset + 2] == '['))) {
1118 return;
1119 }
1120
1121 kXMLElement child = createAnotherElement();
1122 offset = child.parseCharArray(input, offset, end, lineNr);
1123 addChild(child);
1124 }
1125 }
1126
1127
1128 /**
1129 * Creates a new XML element.
1130 *
1131 * @return Description of the Return Value
1132 */
1133 protected kXMLElement createAnotherElement() {
1134 return new kXMLElement(conversionTable,
1135 skipLeadingWhitespace,
1136 false,
1137 ignoreCase);
1138 }
1139
1140
1141 /**
1142 * Scans the content of the object.
1143 *
1144 * @param input Description of the Parameter
1145 * @param offset Description of the Parameter
1146 * @param end Description of the Parameter
1147 * @param contentOffset Description of the Parameter
1148 * @param contentSize Description of the Parameter
1149 * @param lineNr Description of the Parameter
1150 * @return the offset after the XML
1151 * element; contentOffset points to the start of the content section;
1152 * contentSize is the size of the content section
1153 * @exception kXMLParseException Description of the Exception
1154 */
1155 private int scanContent(char[] input,
1156 int offset,
1157 int end,
1158 int[] contentOffset,
1159 int[] contentSize,
1160 int[] lineNr)
1161 throws kXMLParseException {
1162 if (input[offset] == '/') {
1163 contentSize[0] = 0;
1164
1165 if (input[offset + 1] != '>') {
1166 throw expectedInput("'>'", lineNr[0]);
1167 }
1168
1169 return offset + 2;
1170 }
1171
1172 if (input[offset] != '>') {
1173 throw expectedInput("'>'", lineNr[0]);
1174 }
1175
1176 if (skipLeadingWhitespace) {
1177 offset = skipWhitespace(input, offset + 1, end, lineNr);
1178 } else {
1179 offset++;
1180 }
1181
1182 int begin = offset;
1183 contentOffset[0] = offset;
1184 int level = 0;
1185 char[] tag = tagName.toCharArray();
1186 end -= (tag.length + 2);
1187
1188 while ((offset < end) && (level >= 0)) {
1189 if (input[offset] == '<') {
1190 boolean ok = true;
1191
1192 if ((offset < (end - 3)) && (input[offset + 1] == '!')
1193 && (input[offset + 2] == '-')
1194 && (input[offset + 3] == '-')) {
1195 offset += 3;
1196
1197 while ((offset < end)
1198 && ((input[offset - 2] != '-')
1199 || (input[offset - 1] != '-')
1200 || (input[offset - 0] != '>'))) {
1201 offset++;
1202 }
1203
1204 offset++;
1205 continue;
1206 }
1207
1208 if ((offset < (end - 1)) && (input[offset + 1] == '!')
1209 && (input[offset + 2] == '[')) {
1210 offset++;
1211 continue;
1212 }
1213
1214 for (int i = 0; ok && (i < tag.length); i++) {
1215 ok &= (input[offset + (i + 1)] == tag[i]);
1216 }
1217
1218 ok &= !isIdentifierChar(input[offset + tag.length + 1]);
1219
1220 if (ok) {
1221 while ((offset < end) && (input[offset] != '>')) {
1222 offset++;
1223 }
1224
1225 if (input[offset - 1] != '/') {
1226 level++;
1227 }
1228
1229 continue;
1230 } else if (input[offset + 1] == '/') {
1231 ok = true;
1232
1233 for (int i = 0; ok && (i < tag.length); i++) {
1234 ok &= (input[offset + (i + 2)] == tag[i]);
1235 }
1236
1237 if (ok) {
1238 contentSize[0] = offset - contentOffset[0];
1239 offset += tag.length + 2;
1240
1241 try {
1242 offset = skipWhitespace(input, offset,
1243 end + tag.length
1244 + 2,
1245 lineNr);
1246 } catch (kXMLParseException e) {
1247 // ignore
1248 }
1249
1250 if (input[offset] == '>') {
1251 level--;
1252 offset++;
1253 }
1254
1255 continue;
1256 }
1257 }
1258 }
1259
1260 if (input[offset] == '\r') {
1261 lineNr[0]++;
1262
1263 if ((offset != end) && (input[offset + 1] == '\n')) {
1264 offset++;
1265 }
1266 } else if (input[offset] == '\n') {
1267 lineNr[0]++;
1268 }
1269
1270 offset++;
1271 }
1272
1273 if (level >= 0) {
1274 throw unexpectedEndOfData(lineNr[0]);
1275 }
1276
1277 if (skipLeadingWhitespace) {
1278 int i = contentOffset[0] + contentSize[0] - 1;
1279
1280 while ((contentSize[0] >= 0) && (input[i] <= ' ')) {
1281 i--;
1282 contentSize[0]--;
1283 }
1284 }
1285
1286 return offset;
1287 }
1288
1289
1290 /**
1291 * Scans an identifier.
1292 *
1293 * @param input Description of the Parameter
1294 * @param offset Description of the Parameter
1295 * @param end Description of the Parameter
1296 * @return the identifier, or <CODE>null</CODE> if offset doesn't
1297 * point to an identifier
1298 */
1299 private String scanIdentifier(char[] input,
1300 int offset,
1301 int end) {
1302 int begin = offset;
1303
1304 while ((offset < end) && (isIdentifierChar(input[offset]))) {
1305 offset++;
1306 }
1307
1308 if ((offset == end) || (offset == begin)) {
1309 return null;
1310 } else {
1311 return new String(input, begin, offset - begin);
1312 }
1313 }
1314
1315
1316 /**
1317 * Scans one attribute of an object.
1318 *
1319 * @param input Description of the Parameter
1320 * @param offset Description of the Parameter
1321 * @param end Description of the Parameter
1322 * @param lineNr Description of the Parameter
1323 * @return the offset after the attribute
1324 * @exception kXMLParseException Description of the Exception
1325 */
1326 private int scanOneAttribute(char[] input,
1327 int offset,
1328 int end,
1329 int[] lineNr)
1330 throws kXMLParseException {
1331 String key;
1332 String value;
1333
1334 key = scanIdentifier(input, offset, end);
1335
1336 if (key == null) {
1337 throw syntaxError("an attribute key", lineNr[0]);
1338 }
1339
1340 offset = skipWhitespace(input, offset + key.length(), end, lineNr);
1341
1342 if (ignoreCase) {
1343 key = key.toUpperCase();
1344 }
1345
1346 if (input[offset] != '=') {
1347 throw valueMissingForAttribute(key, lineNr[0]);
1348 }
1349
1350 offset = skipWhitespace(input, offset + 1, end, lineNr);
1351
1352 value = scanString(input, offset, end, lineNr);
1353
1354 if (value == null) {
1355 throw syntaxError("an attribute value", lineNr[0]);
1356 }
1357
1358 if ((value.charAt(0) == '"') || (value.charAt(0) == '\'')) {
1359 value = value.substring(1, (value.length() - 1));
1360 offset += 2;
1361 }
1362
1363 getAttributes().put(key, decodeString(value, lineNr[0]));
1364 return offset + value.length();
1365 }
1366
1367
1368 /**
1369 * Scans a string. Strings are either identifiers, or text delimited by
1370 * double quotes.
1371 *
1372 * @param input Description of the Parameter
1373 * @param offset Description of the Parameter
1374 * @param end Description of the Parameter
1375 * @param lineNr Description of the Parameter
1376 * @return the string found, without
1377 * delimiting double quotes; or null if offset didn't point to a
1378 * valid string
1379 * @exception kXMLParseException Description of the Exception
1380 * @see nanoxml.kXMLElement#scanIdentifier
1381 */
1382 private String scanString(char[] input,
1383 int offset,
1384 int end,
1385 int[] lineNr)
1386 throws kXMLParseException {
1387 char delim = input[offset];
1388
1389 if ((delim == '"') || (delim == '\'')) {
1390 int begin = offset;
1391 offset++;
1392
1393 while ((offset < end) && (input[offset] != delim)) {
1394 if (input[offset] == '\r') {
1395 lineNr[0]++;
1396
1397 if ((offset != end) && (input[offset + 1] == '\n')) {
1398 offset++;
1399 }
1400 } else if (input[offset] == '\n') {
1401 lineNr[0]++;
1402 }
1403
1404 offset++;
1405 }
1406
1407 if (offset == end) {
1408 return null;
1409 } else {
1410 return new String(input, begin, offset - begin + 1);
1411 }
1412 } else {
1413 return scanIdentifier(input, offset, end);
1414 }
1415 }
1416
1417
1418 /**
1419 * Scans the class (tag) name of the object.
1420 *
1421 * @param input Description of the Parameter
1422 * @param offset Description of the Parameter
1423 * @param end Description of the Parameter
1424 * @param lineNr Description of the Parameter
1425 * @return the position after the tag name
1426 * @exception kXMLParseException Description of the Exception
1427 */
1428 private int scanTagName(char[] input,
1429 int offset,
1430 int end,
1431 int[] lineNr)
1432 throws kXMLParseException {
1433 tagName = scanIdentifier(input, offset, end);
1434
1435 if (tagName == null) {
1436 throw syntaxError("a tag name", lineNr[0]);
1437 }
1438
1439 return offset + tagName.length();
1440 }
1441
1442
1443 /**
1444 * Changes the content string.
1445 *
1446 * @param content The new content string.
1447 */
1448 public void setContent(String content) {
1449 this.contents = content;
1450 }
1451
1452
1453 /**
1454 * Changes the tag name.
1455 *
1456 * @param tagName The new tag name.
1457 */
1458 public void setTagName(String tagName) {
1459 this.tagName = tagName;
1460 }
1461
1462
1463 /**
1464 * Skips a tag that don't contain any useful data: <?...?>,
1465 * <!...> and comments.
1466 *
1467 * @param input Description of the Parameter
1468 * @param offset Description of the Parameter
1469 * @param end Description of the Parameter
1470 * @param lineNr Description of the Parameter
1471 * @return the position after the tag
1472 */
1473 protected int skipBogusTag(char[] input,
1474 int offset,
1475 int end,
1476 int[] lineNr) {
1477 int level = 1;
1478
1479 while (offset < end) {
1480 char ch = input[offset++];
1481
1482 switch (ch) {
1483 case '\r':
1484 if ((offset < end) && (input[offset] == '\n')) {
1485 offset++;
1486 }
1487
1488 lineNr[0]++;
1489 break;
1490 case '\n':
1491 lineNr[0]++;
1492 break;
1493 case '<':
1494 level++;
1495 break;
1496 case '>':
1497 level--;
1498
1499 if (level == 0) {
1500 return offset;
1501 }
1502
1503 break;
1504 default:
1505 }
1506 }
1507
1508 throw unexpectedEndOfData(lineNr[0]);
1509 }
1510
1511
1512 /**
1513 * Skips a tag that don't contain any useful data: <?...?>,
1514 * <!...> and comments.
1515 *
1516 * @param input Description of the Parameter
1517 * @param offset Description of the Parameter
1518 * @param end Description of the Parameter
1519 * @param lineNr Description of the Parameter
1520 * @return the position after the tag
1521 * @exception kXMLParseException Description of the Exception
1522 */
1523 private int skipPreamble(char[] input,
1524 int offset,
1525 int end,
1526 int[] lineNr)
1527 throws kXMLParseException {
1528 char ch;
1529
1530 do {
1531 offset = skipWhitespace(input, offset, end, lineNr);
1532
1533 if (input[offset] != '<') {
1534 expectedInput("'<'", lineNr[0]);
1535 }
1536
1537 offset++;
1538
1539 if (offset >= end) {
1540 throw unexpectedEndOfData(lineNr[0]);
1541 }
1542
1543 ch = input[offset];
1544
1545 if ((ch == '!') || (ch == '?')) {
1546 offset = skipBogusTag(input, offset, end, lineNr);
1547 }
1548 } while (!isIdentifierChar(ch));
1549
1550 return offset;
1551 }
1552
1553
1554 /**
1555 * Skips whitespace characters.
1556 *
1557 * @param input Description of the Parameter
1558 * @param offset Description of the Parameter
1559 * @param end Description of the Parameter
1560 * @param lineNr Description of the Parameter
1561 * @return the position after the
1562 * whitespace
1563 */
1564 private int skipWhitespace(char[] input,
1565 int offset,
1566 int end,
1567 int[] lineNr) {
1568 int startLine = lineNr[0];
1569
1570 while (offset < end) {
1571 if (((offset + 6) < end) && (input[offset + 3] == '-')
1572 && (input[offset + 2] == '-') && (input[offset + 1] == '!')
1573 && (input[offset] == '<')) {
1574 offset += 4;
1575
1576 while ((input[offset] != '-') || (input[offset + 1] != '-')
1577 || (input[offset + 2] != '>')) {
1578 if ((offset + 2) >= end) {
1579 throw unexpectedEndOfData(startLine);
1580 }
1581
1582 offset++;
1583 }
1584
1585 offset += 2;
1586 } else if (input[offset] == '\r') {
1587 lineNr[0]++;
1588
1589 if ((offset != end) && (input[offset + 1] == '\n')) {
1590 offset++;
1591 }
1592 } else if (input[offset] == '\n') {
1593 lineNr[0]++;
1594 } else if (input[offset] > ' ') {
1595 break;
1596 }
1597
1598 offset++;
1599 }
1600
1601 if (offset == end) {
1602 throw unexpectedEndOfData(startLine);
1603 }
1604
1605 return offset;
1606 }
1607
1608
1609 /**
1610 * Converts &...; sequences to "normal" chars.
1611 *
1612 * @param s Description of the Parameter
1613 * @param lineNr Description of the Parameter
1614 * @return Description of the Return Value
1615 */
1616 protected String decodeString(