]> sjero.net Git - linphone/blob - p2pproxy/dependencies-src/jxse-src-2.5/impl/src/net/jxta/impl/document/LiteXMLElement.java
remove mediastreamer2 and add it as a submodule instead.
[linphone] / p2pproxy / dependencies-src / jxse-src-2.5 / impl / src / net / jxta / impl / document / LiteXMLElement.java
1 /*
2  * Copyright (c) 2001-2007 Sun Microsystems, Inc.  All rights reserved.
3  *  
4  *  The Sun Project JXTA(TM) Software License
5  *  
6  *  Redistribution and use in source and binary forms, with or without 
7  *  modification, are permitted provided that the following conditions are met:
8  *  
9  *  1. Redistributions of source code must retain the above copyright notice,
10  *     this list of conditions and the following disclaimer.
11  *  
12  *  2. Redistributions in binary form must reproduce the above copyright notice, 
13  *     this list of conditions and the following disclaimer in the documentation 
14  *     and/or other materials provided with the distribution.
15  *  
16  *  3. The end-user documentation included with the redistribution, if any, must 
17  *     include the following acknowledgment: "This product includes software 
18  *     developed by Sun Microsystems, Inc. for JXTA(TM) technology." 
19  *     Alternately, this acknowledgment may appear in the software itself, if 
20  *     and wherever such third-party acknowledgments normally appear.
21  *  
22  *  4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must 
23  *     not be used to endorse or promote products derived from this software 
24  *     without prior written permission. For written permission, please contact 
25  *     Project JXTA at http://www.jxta.org.
26  *  
27  *  5. Products derived from this software may not be called "JXTA", nor may 
28  *     "JXTA" appear in their name, without prior written permission of Sun.
29  *  
30  *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
31  *  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
32  *  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN 
33  *  MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
34  *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
35  *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
36  *  OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
37  *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
38  *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
39  *  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40  *  
41  *  JXTA is a registered trademark of Sun Microsystems, Inc. in the United 
42  *  States and other countries.
43  *  
44  *  Please see the license information page at :
45  *  <http://www.jxta.org/project/www/license.html> for instructions on use of 
46  *  the license in source files.
47  *  
48  *  ====================================================================
49  *  
50  *  This software consists of voluntary contributions made by many individuals 
51  *  on behalf of Project JXTA. For more information on Project JXTA, please see 
52  *  http://www.jxta.org.
53  *  
54  *  This license is based on the BSD license adopted by the Apache Foundation. 
55  */
56
57 package net.jxta.impl.document;
58
59
60 import java.io.Writer;
61
62 import java.util.ArrayList;
63 import java.util.Collections;
64 import java.util.Enumeration;
65 import java.util.Iterator;
66 import java.util.List;
67 import java.util.logging.Level;
68 import java.util.logging.Logger;
69
70 import java.io.IOException;
71
72 import net.jxta.document.Attribute;
73 import net.jxta.document.XMLElement;
74 import net.jxta.logging.Logging;
75
76
77 /**
78  * An element of a <CODE>StructuredDocument</CODE>. <CODE>StructuredDocument</CODE>s
79  * are made up of hierarchies of elements. LiteXMLElement is part of an implementation
80  * while makes use of XML-style document conventions, but without the overhead of a
81  * full parser.
82  */
83 public class LiteXMLElement implements XMLElement<LiteXMLElement> {
84
85     /**
86      * Defines a range of characters, probably within a string. The range is
87      * deemed to be invalid if 'start' is -1.  A zero length range is, by
88      * convention, described by an 'end' value of 'start' - 1.
89      */
90     protected static class charRange implements Comparable<charRange> {
91
92         /**
93          * Contains the start position of this range.
94          */
95         public int start;
96
97         /**
98          * Contains the end position of this range. one weird thing: if end == start -1,
99          * then the item is of zero length beginning at start.
100          */
101         public int end;
102
103         /**
104          * Constructor for a null charRange.
105          */
106         public charRange() {
107             start = -1;
108             end = -1;
109         }
110
111         /**
112          * Constructor for which the bounds are specified.
113          */
114         public charRange(int start, int end) {
115             this.start = start;
116             this.end = end;
117         }
118
119         /**
120          * {@inheritDoc}
121          */
122         @Override
123         public boolean equals(Object aRange) {
124             if (this == aRange) {
125                 return true;
126             }
127
128             if (!(aRange instanceof charRange)) {
129                 return false;
130             }
131
132             charRange someRange = (charRange) aRange;
133
134             return (start == someRange.start) && (end == someRange.end);
135         }
136
137         /**
138          * {@inheritDoc}
139          */
140         public int compareTo(charRange someRange) {
141             if (this == someRange) {
142                 return 0;
143             }
144
145             if (start < someRange.start) {
146                 return -1;
147             }
148
149             if (start > someRange.start) {
150                 return 1;
151             }
152
153             if (end < someRange.end) {
154                 return -1;
155             }
156
157             if (end > someRange.end) {
158                 return 1;
159             }
160
161             return 0;
162         }
163
164         /**
165          * {@inheritDoc}
166          */
167         @Override
168         public String toString() {
169             return "[" + start + "," + end + "]";
170         }
171
172         /**
173          * Returns true if the <CODE>charRange</CODE> specified by someRange is
174          * contained within this range.
175          *
176          * @param someRange The range which must be contained within this range.
177          * @return true if the specified range is contained with this range otherwise false.
178          */
179         public boolean contains(charRange someRange) {
180             return (isValid() && someRange.isValid() && (start <= someRange.start) && (end >= someRange.end));
181         }
182
183         /**
184          * Returns true if the <CODE>tagRange</CODE> specified by someRange is
185          * contained within this range.
186          *
187          * @param someRange The range which must be contained within this range.
188          * @return true if the specified range is contained with this range otherwise false.
189          */
190         public boolean contains(tagRange someRange) {
191             return (isValid() && someRange.isValid() && (start <= someRange.startTag.start) && (end >= someRange.endTag.end));
192         }
193
194         /**
195          * Returns true if the location specified is contained in this range.
196          *
197          * @param someLoc the location which is to be tested.
198          * @return true if the location is in this range, otherwise false.
199          */
200         public boolean contains(int someLoc) {
201             return (isValid() && (someLoc >= 0) && (start <= someLoc) && (end >= someLoc));
202         }
203
204         /**
205          * Returns true if the range is both non-null and has a length of greater
206          * than or equal to zero.
207          *
208          * @return true if the range is a valid one, otherwise false.
209          */
210         public boolean isValid() {
211             return length() >= 0;
212         }
213
214         /**
215          * Returns the length of this range.
216          *
217          * @return The length of the range or -1 if the range is null.
218          */
219         public int length() {
220             if ((-1 == start) || (-1 == end)) {
221                 return -1;
222             }
223
224             return (end - start + 1);
225         }
226     }
227
228
229     /**
230      * A tagRange is a collection of char ranges useful for describing XML
231      * structures.
232      * <p/>
233      * <p/><dl>
234      * <dt><code>startTag</code></dt>
235      * <dd>The range of the opening tag, ie. &lt;tag></dd>
236      * <dt><code>body</code></dt>
237      * <dd>Everything between <code>startTag</code> and <code>endTag</code>.</dd>
238      * <dt><code>endTag</code></dt>
239      * <dd>The range of the terminating tag, ie. &lt;/tag>.</dd>
240      * </dl>
241      * <p/>
242      * <p/>For empty-element tags the <code>startTag</code>, <code>body</code>
243      * and <code>endTag</code> will be equal.
244      */
245     protected static class tagRange implements Comparable<tagRange> {
246         public charRange startTag;
247         public charRange body;
248         public charRange endTag;
249
250         public tagRange() {
251             startTag = new charRange();
252             body = new charRange();
253             endTag = new charRange();
254         }
255
256         public tagRange(charRange startTag, charRange body, charRange endTag) {
257             this.startTag = startTag;
258             this.body = body;
259             this.endTag = endTag;
260         }
261
262         /**
263          * {@inheritDoc}
264          */
265         @Override
266         public boolean equals(Object aRange) {
267             if (this == aRange) {
268                 return true;
269             }
270
271             if (!(aRange instanceof tagRange)) {
272                 return false;
273             }
274
275             tagRange likeMe = (tagRange) aRange;
276
277             return startTag.equals(likeMe.startTag) && body.equals(likeMe.body) && endTag.equals(likeMe.endTag);
278         }
279
280         /**
281          * {@inheritDoc}
282          */
283         public int compareTo(tagRange someRange) {
284             if (this == someRange) {
285                 return 0;
286             }
287
288             int compared = startTag.compareTo(someRange.startTag);
289
290             if (0 != compared) {
291                 return compared;
292             }
293
294             return endTag.compareTo(someRange.endTag);
295         }
296
297         /**
298          * {@inheritDoc}
299          */
300         @Override
301         public String toString() {
302             return startTag + ":" + body + ":" + endTag;
303         }
304
305         /**
306          * Returns true if the <CODE>tagRange</CODE> specified by someRange is
307          * contained within the body portion of this range.
308          *
309          * @param someRange The range which must be contained within this range.
310          * @return true if the specified range is contained with this range
311          *         otherwise false.
312          */
313         public boolean contains(tagRange someRange) {
314             return (isValid() && someRange.isValid() && (body.start <= someRange.startTag.start)
315                     && (body.end >= someRange.endTag.end));
316         }
317
318         /**
319          * Returns true if the <CODE>charRange</CODE> specified by someRange is
320          * contained within the body portion of this range.
321          *
322          * @param someRange The range which must be contained within this range.
323          * @return true if the specified range is contained with this range
324          *         otherwise false.
325          */
326         public boolean contains(charRange someRange) {
327             return (isValid() && someRange.isValid() && (body.start <= someRange.start) && (body.end >= someRange.end));
328         }
329
330         /**
331          * @return <code>true</code> if this tagRange represents and empty
332          *         element.
333          */
334         public boolean isEmptyElement() {
335             return isValid() && startTag.equals(body) && startTag.equals(endTag);
336         }
337
338         /**
339          * @return true if valid
340          */
341         public boolean isValid() {
342             return (null != startTag) && (null != body) && (null != endTag) && startTag.isValid() && body.isValid()
343                     && endTag.isValid();
344         }
345     }
346
347     /**
348      * Log4J Logger
349      */
350     private final static transient Logger LOG = Logger.getLogger(LiteXMLElement.class.getName());
351
352     /**
353      * If true then every operation which modifies the state of the document will
354      * perform a consistency check. This is a deadly performance killer but
355      * helps a lot in isolating bugs.
356      */
357     protected final static transient boolean paranoidConsistencyChecking = false;
358
359     /**
360      * The document associated with this Element.
361      */
362     protected final transient LiteXMLDocument doc;
363
364     /**
365      * Identifies the element which is the parent of this element. If <code>
366      * this.parent == this</code> then this element is the root of the document.
367      * If <code>null == parent</code> then this element has not yet been
368      * inserted into the document.
369      */
370     protected transient LiteXMLElement parent;
371
372     /**
373      * The portion of the source XML associated with this node
374      */
375     protected transient tagRange loc;
376
377     /**
378      * If this node has yet to be inserted into the document then will contain
379      * the String value of this node, otherwise null.
380      */
381     private transient StringBuilder uninserted = null;
382
383     /**
384      * The child elements associated with this element
385      */
386     private transient List<LiteXMLElement> children;
387
388     /**
389      * Creates new LiteXMLElement
390      *
391      * @param loc The location of the element within the document.
392      * @param doc The {@link LiteXMLDocument} which is the root of the document.
393      */
394     protected LiteXMLElement(LiteXMLDocument doc, tagRange loc) {
395         this.doc = doc;
396         this.loc = loc;
397     }
398
399     /**
400      * Creates new LiteElement
401      *
402      * @param doc  The {@link LiteXMLDocument} which is the root of the document.
403      * @param name The name of the element being created.
404      * @param val  The value of the element being created or null if there is no
405      *             content to the element.
406      */
407     public LiteXMLElement(LiteXMLDocument doc, final String name, final String val) {
408         this(doc, new tagRange());
409
410         for (int eachChar = name.length() - 1; eachChar >= 0; eachChar--) {
411             if (Character.isWhitespace(name.charAt(eachChar))) {
412                 throw new IllegalArgumentException("Element names may not contain spaces.");
413             }
414         }
415
416         if ((null == val) || (0 == val.length())) {
417             uninserted = new StringBuilder("<" + name + "/>");
418         } else {
419             uninserted = new StringBuilder(val);
420             encodeEscaped(uninserted);
421             uninserted.insert(0, "<" + name + ">");
422             uninserted.append("</").append(name).append(">");
423         }
424     }
425
426     /**
427      * {@inheritDoc}
428      */
429     @Override
430     public boolean equals(Object element) {
431         if (this == element) {
432             return true;
433         }
434
435         if (!(element instanceof LiteXMLElement)) {
436             return false;
437         }
438
439         LiteXMLElement liteElement = (LiteXMLElement) element;
440
441         if (getDocument() != liteElement.getDocument()) {
442             return false;
443         }
444
445         if (!getName().equals(liteElement.getName())) {
446             return false;
447         }
448
449         String val1;
450
451         if (null != uninserted) {
452             val1 = uninserted.toString();
453         } else {
454             val1 = getTextValue();
455         }
456
457         String val2 = liteElement.getTextValue();
458
459         if ((null == val1) && (null == val2)) {
460             return true;
461         }
462
463         return null != val1 && null != val2 && val1.equals(val2);
464
465     }
466
467     /**
468      * {@inheritDoc}
469      * <p/>
470      * <p/>A toString implementation for debugging purposes.
471      */
472     @Override
473     public String toString() {
474         if (paranoidConsistencyChecking) {
475             checkConsistency();
476         }
477
478         String name = getName();
479
480         if (name == null) {
481             name = "<<null name>>";
482         }
483         String value = getTextValue();
484
485         if (value == null) {
486             value = "<<null value>>";
487         }
488
489         if ((value.length() + name.length()) >= 60) {
490             int len = Math.max(20, 60 - name.length());
491
492             value = value.substring(0, Math.min(len, value.length()));
493         }
494
495         // FIXME 20021125 bondolo@jxta.org should remove carriage control.
496
497         return super.toString() + " / " + name + " = " + value;
498     }
499
500     /**
501      * {@inheritDoc}
502      */
503     public LiteXMLDocument getRoot() {
504         return getDocument();
505     }
506
507     /**
508      * {@inheritDoc}
509      */
510     public LiteXMLElement getParent() {
511         return parent;
512     }
513
514     /**
515      * {@inheritDoc}
516      */
517     public Enumeration<LiteXMLElement> getChildren() {
518         if (null != uninserted) {
519             throw new IllegalStateException("This element has not been added.");
520         }
521
522         if (null == children) {
523             List<LiteXMLElement> empty = Collections.emptyList();
524
525             return Collections.enumeration(empty);
526         } else {
527             return Collections.enumeration(children);
528         }
529     }
530
531     /**
532      * {@inheritDoc}
533      */
534     public String getName() {
535         if (null != uninserted) {
536             throw new IllegalStateException("This element has not been added.");
537         }
538
539         if (paranoidConsistencyChecking) {
540             checkConsistency();
541         }
542
543         int current = loc.startTag.start + 1;
544
545         while (current <= loc.startTag.end) {
546             char inTagName = getDocument().docContent.charAt(current);
547
548             if (Character.isWhitespace(inTagName) || ('/' == inTagName) || ('>' == inTagName)) {
549                 break;
550             }
551
552             current++;
553         }
554
555         return getDocument().docContent.substring(loc.startTag.start + 1, current);
556     }
557
558     /**
559      * Get the name associated with an element.
560      *
561      * @return A string containing the key of this element.
562      */
563     public String getKey() {
564         return getName();
565     }
566
567     /**
568      * Get the value (if any) associated with an element.
569      *
570      * @return A string containing the value of this element, if any, otherwise null.
571      */
572     public String getValue() {
573         return getTextValue();
574     }
575
576     /**
577      * {@inheritDoc}
578      */
579     public void appendChild(LiteXMLElement element) {
580         if (element.getDocument() != getDocument()) {
581             throw new IllegalArgumentException("Wrong document");
582         }
583
584         if (null != element.parent) {
585             throw new IllegalArgumentException("New element is already in document");
586         }
587
588         if (null != uninserted) {
589             throw new IllegalStateException("This element has not been added.");
590         }
591
592         if (paranoidConsistencyChecking) {
593             checkConsistency();
594         }
595
596         // If uninserted then this new element contains content which needs to
597         // be added to the document. If uninserted is null then the child
598         // element's content is already in the document, but merely needs to
599         // be recognized as a child.
600         if (null != element.uninserted) {
601             if (loc.startTag.equals(loc.endTag)) {
602                 getDocument().docContent.deleteCharAt(loc.endTag.end - 1); // delete the /
603                 loc.startTag.end -= 1;
604
605                 // skip past the name portion
606                 int current = loc.startTag.start + 1;
607
608                 while (current <= loc.startTag.end) {
609                     char inTagName = getDocument().docContent.charAt(current);
610
611                     if (Character.isWhitespace(inTagName) || ('>' == inTagName)) {
612                         break;
613                     }
614
615                     current++;
616                 }
617
618                 String tagName = getDocument().docContent.substring(loc.startTag.start + 1, current);
619
620                 getDocument().docContent.insert(loc.startTag.end + 1, "</" + tagName + ">");
621                 getDocument().adjustLocations(loc.startTag.end + 1, tagName.length() + 2);
622                 loc.endTag = new charRange(loc.startTag.end + 1, loc.startTag.end + 3 + tagName.length());
623                 loc.body = new charRange(loc.startTag.end + 1, loc.startTag.end);
624             }
625
626             getDocument().docContent.insert(loc.endTag.start, element.uninserted);
627
628             element.loc.startTag.start = loc.endTag.start;
629             element.loc.startTag.end = getDocument().docContent.indexOf(">", element.loc.startTag.start);
630
631             if ('/' != element.uninserted.charAt(element.uninserted.length() - 2)) {
632                 element.loc.body.start = element.loc.startTag.end + 1;
633
634                 element.loc.endTag.end = element.loc.startTag.start + element.uninserted.length() - 1;
635                 element.loc.endTag.start = getDocument().docContent.lastIndexOf("<", element.loc.endTag.end);
636
637                 element.loc.body.end = element.loc.endTag.start - 1;
638             } else {
639                 element.loc.body = new charRange(element.loc.startTag.start, element.loc.startTag.end);
640                 element.loc.endTag = new charRange(element.loc.startTag.start, element.loc.startTag.end);
641             }
642
643             if (0 != loc.body.length()) {
644                 getDocument().adjustLocations(loc.endTag.start, element.uninserted.length());
645             } else {
646                 loc.body.start--;
647                 getDocument().adjustLocations(loc.endTag.start, element.uninserted.length());
648                 loc.body.start++;
649             }
650
651             loc.body.end += element.uninserted.length();
652
653             element.uninserted = null;
654         }
655
656         element.parent = this;
657
658         if (null == children) {
659             children = new ArrayList<LiteXMLElement>();
660         }
661
662         children.add(element);
663
664         if (paranoidConsistencyChecking) {
665             checkConsistency();
666         }
667     }
668
669     /**
670      * Returns an enumeration of the immediate children of this element whose
671      * name match the specified string.
672      *
673      * @param key The key which will be matched against.
674      * @return enumeration containing all of the children of this element.
675      */
676     public Enumeration<LiteXMLElement> getChildren(Object key) {
677         if (key instanceof String)
678             return getChildren((String) key);
679         else
680             throw new ClassCastException(key.getClass().getName() + " not supported by getChildren.");
681     }
682
683     /**
684      * {@inheritDoc}
685      */
686     public Enumeration<LiteXMLElement> getChildren(String name) {
687         if (null != uninserted) {
688             throw new IllegalStateException("This element has not been added.");
689         }
690
691         if (paranoidConsistencyChecking) {
692             checkConsistency();
693         }
694
695         if (null == children) {
696             List<LiteXMLElement> empty = Collections.emptyList();
697
698             return Collections.enumeration(empty);
699         }
700
701         List<LiteXMLElement> result = new ArrayList<LiteXMLElement>();
702
703         for (LiteXMLElement aChild : children) {
704             if (name.equals(aChild.getName())) {
705                 result.add(aChild);
706             }
707         }
708
709         return Collections.enumeration(result);
710     }
711
712     /**
713      * {@inheritDoc}
714      */
715     public String getTextValue() {
716         return getTextValue(false, true);
717     }
718
719     /**
720      * Get the value (if any) associated with an element.
721      *
722      * @param getEncoded if true then the contents will be encoded such that
723      *                   the contents will not be interpreted as XML. see
724      *                   {@link <a href="http://www.w3.org/TR/REC-xml#syntax">W3C XML 1.0 Specification</a>}
725      *                   ie. < -> &lt; & -> &amp;
726      * @param trim       if true trims prefix and suffix white space
727      * @return A string containing the value of this element, if any, otherwise null.
728      */
729     protected String getTextValue(boolean getEncoded, boolean trim) {
730         if (null != uninserted) {
731             throw new IllegalStateException("This element has not been added.");
732         }
733
734         if (paranoidConsistencyChecking) {
735             checkConsistency();
736         }
737
738         StringBuilder building = new StringBuilder();
739
740         List<charRange> ranges = new ArrayList<charRange>();
741
742         /*
743          * insert the ranges of the children in order. insertion method is ok
744          * because the number of children is usually less than 10 or so.
745          */
746         for (Enumeration<LiteXMLElement> eachChild = getChildren(); eachChild.hasMoreElements();) {
747             LiteXMLElement aChild = eachChild.nextElement();
748             charRange childsRange = new charRange(aChild.loc.startTag.start, aChild.loc.endTag.end);
749
750             // find where to insert.
751             for (int eachRange = 0; eachRange < ranges.size(); eachRange++) {
752                 charRange rangeChild = ranges.get(eachRange);
753
754                 if (1 == rangeChild.compareTo(childsRange)) {
755                     ranges.set(eachRange, childsRange);
756                     childsRange = rangeChild;
757                 }
758             }
759             ranges.add(childsRange);
760         }
761
762         int current = loc.body.start;
763
764         // add all the text not part of some child
765         for (charRange aRange : ranges) {
766             building.append(getDocument().docContent.substring(current, aRange.start));
767
768             current = aRange.end + 1;
769         }
770
771         // Add the last bit.
772         building.append(getDocument().docContent.substring(current, loc.endTag.start));
773
774         if (!getEncoded) {
775             building = decodeEscaped(building);
776         }
777
778         // trim
779         int firstNonWhiteSpace = 0;
780         int lastNonWhiteSpace = building.length() - 1;
781
782         if (trim) {
783             while (firstNonWhiteSpace < building.length()) {
784                 char possibleSpace = building.charAt(firstNonWhiteSpace);
785
786                 if (!Character.isWhitespace(possibleSpace)) {
787                     break;
788                 }
789
790                 firstNonWhiteSpace++;
791             }
792
793             // did we find no non-whitespace?
794             if (firstNonWhiteSpace >= building.length()) {
795                 return null;
796             }
797
798             while (lastNonWhiteSpace >= firstNonWhiteSpace) {
799                 char possibleSpace = building.charAt(lastNonWhiteSpace);
800
801                 if (!Character.isWhitespace(possibleSpace)) {
802                     break;
803                 }
804
805                 lastNonWhiteSpace--;
806             }
807         }
808
809         String result = building.substring(firstNonWhiteSpace, lastNonWhiteSpace + 1);
810
811         return result;
812     }
813
814     /**
815      * Write the contents of this element and optionally its children. The
816      * writing is done to a provided <code>java.io.Writer</code>. The writing
817      * can optionally be indented.
818      *
819      * @param into    The java.io.Writer that the output will be sent to.
820      * @param indent  the number of tabs which will be inserted before each
821      *                line.
822      * @param recurse if true then also print the children of this element.
823      * @throws java.io.IOException if an io error occurs
824      */
825     protected void printNice(Writer into, int indent, boolean recurse) throws IOException {
826         if (null != uninserted) {
827             throw new IllegalStateException("This element has not been added.");
828         }
829
830         if (paranoidConsistencyChecking) {
831             checkConsistency();
832         }
833
834         // print start tag
835         StringBuilder start = new StringBuilder();
836
837         if (-1 != indent) {
838             // do indent
839             for (int eachTab = 0; eachTab < indent; eachTab++) {
840                 start.append('\t');
841             }
842         }
843
844         start.append(getDocument().docContent.substring(loc.startTag.start, loc.startTag.end + 1));
845
846         if (-1 != indent) {
847             start.append('\n');
848         }
849
850         into.write(start.toString());
851
852         // print the rest if this was not an empty element.
853         if (!loc.startTag.equals(loc.endTag)) {
854             String itsValue = getTextValue(true, (-1 != indent));
855
856             // print node value
857             if (null != itsValue) {
858                 if (-1 != indent) {
859                     // do indent
860                     for (int eachTab = 0; eachTab < indent + 1; eachTab++) {
861                         into.write("\t");
862                     }
863                 }
864
865                 into.write(itsValue);
866
867                 if (-1 != indent) {
868                     into.write('\n');
869                 }
870             }
871
872             // recurse as needed
873             if (recurse) {
874                 int childIndent;
875
876                 Enumeration<LiteXMLElement> childrens = getChildren();
877
878                 Attribute space = getAttribute("xml:space");
879
880                 if (null != space) {
881                     if ("preserve".equals(space.getValue())) {
882                         childIndent = -1;
883                     } else {
884                         childIndent = indent + 1;
885                     }
886                 } else {
887                     if (-1 != indent) {
888                         childIndent = indent + 1;
889                     } else {
890                         childIndent = -1;
891                     }
892                 }
893
894                 while (childrens.hasMoreElements()) {
895                     LiteXMLElement aChild = childrens.nextElement();
896
897                     aChild.printNice(into, childIndent, recurse);
898                 }
899             }
900
901             // print end tag
902             StringBuilder end = new StringBuilder();
903
904             if (-1 != indent) {
905                 // do indent
906                 for (int eachTab = 0; eachTab < indent; eachTab++) {
907                     end.append('\t');
908                 }
909             }
910
911             end.append(getDocument().docContent.substring(loc.endTag.start, loc.endTag.end + 1));
912
913             if (-1 != indent) {
914                 end.append('\n');
915             }
916
917             into.write(end.toString());
918         }
919     }
920
921     /**
922      * Given a source string, an optional tag and a range with in the source
923      * find either the tag specified or the next tag.
924      * <p/>
925      * The search consists of 4 phases :
926      * 0.  If no tag was specified, determine if a tag can be found and
927      * learn its name.
928      * 1.  Search for the start of the named tag.
929      * 2.  Search for the end tag. Each time we think we have found a tag
930      * which might be the end tag we make sure it is not the end tag
931      * of another element with the same name as our tag.
932      * 3.  Calculate the position of the body of the tag given the locations
933      * of the start and end.
934      *
935      * @param source the string to search
936      * @param tag    the tag to search for in the source string. If this tag is
937      *               empty or null then we will search for the next tag.
938      * @param range  describes the range of character locations in the source
939      *               string to which the search will be limited.
940      * @return tagRange containing the ranges of the found tag.
941      */
942
943     protected tagRange getTagRanges(final StringBuilder source, String tag, final charRange range) {
944
945         // FIXME   bondolo@jxta.org 20010327    Does not handle XML comments. ie.  <!-- -->
946         if (null != uninserted) {
947             throw new IllegalStateException("This element has not been added to the document.");
948         }
949
950         tagRange result = new tagRange();
951         int start = range.start;
952         int end = source.length() - 1;
953         int current;
954         boolean foundStartTag = false;
955         boolean foundEndTag = false;
956         boolean emptyTag = (null == tag) || (0 == tag.length());
957
958         // check for bogosity
959         if ((-1 == start) || (start >= end)) {
960             throw new IllegalArgumentException("Illegal start value");
961         }
962
963         // adjust end of range
964         if ((-1 != range.end) && (end > range.end)) {
965             end = range.end;
966         }
967
968         // check for empty tag and assign empty string
969         if (null == tag) {
970             tag = "";
971         }
972
973         if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
974             LOG.finer("Searching for \"" + tag + "\" in range [" + start + "," + end + "]");
975         }
976
977         current = start;
978
979         // Begin Phase 0 : Search for any tag.
980
981         if (emptyTag) {
982             int foundTagText = source.indexOf("<", current);
983
984             // was it not found? if not then quit
985             if (-1 == foundTagText) {
986                 if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
987                     LOG.finer("No Tags Found");
988                 }
989                 return result;
990             }
991
992             // this part is about setting the tag if necessary
993             foundTagText++;
994
995             int afterTagText = foundTagText;
996
997             while (afterTagText <= end) {
998                 char inTagName = source.charAt(afterTagText);
999
1000                 if (!Character.isWhitespace(inTagName) && ('/' != inTagName) && ('>' != inTagName)) {
1001                     afterTagText++;
1002                     continue;
1003                 }
1004
1005                 tag = source.substring(foundTagText, afterTagText);
1006                 emptyTag = (null == tag) || (0 == tag.length());
1007
1008                 break;
1009             }
1010
1011             // it better not be still empty
1012             if (emptyTag) {
1013                 if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1014                     LOG.finer("No tag found");
1015                 }
1016                 return result;
1017             }
1018         }
1019
1020         if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1021             LOG.finer("Search for \"" + tag + "\" [" + start + "," + end + "]");
1022         }
1023
1024         // Begin Phase 1: Search for the Start Tag
1025
1026         while (!foundStartTag && (current < end)) {
1027             int foundTagText = source.indexOf(tag, current + 1); // first loc is one past current location
1028             int foundTagTerminator;
1029             int foundNextTagStart;
1030             int afterTagText = foundTagText + tag.length();
1031
1032             // was it not found
1033             if ((-1 == foundTagText) || (afterTagText > end)) {
1034                 if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1035                     LOG.finer("Tag \"" + tag + "\" Not Found(1)");
1036                 }
1037                 return result;
1038             }
1039
1040             char checkChar = source.charAt(afterTagText);
1041
1042             // check to see if it is the start tag
1043             if (('<' != source.charAt(foundTagText - 1)) || // it has the open tag delimiter before it
1044                     (!Character.isWhitespace(checkChar) && ('/' != checkChar) && ('>' != checkChar))) { // is immediately followed by a delimiter
1045                 current = afterTagText;
1046                 continue;
1047             }
1048
1049             foundTagTerminator = source.indexOf(">", afterTagText);
1050             foundNextTagStart = source.indexOf("<", afterTagText + 1);
1051
1052             if ((-1 == foundTagTerminator) || // the tag has no terminator
1053                     (foundTagTerminator > end) || // it is past the valid range
1054                     ((-1 != foundNextTagStart) && // there is another tag start
1055                     (foundNextTagStart < foundTagTerminator))) { // and it is before the terminator we found. very bad
1056                 current = afterTagText;
1057                 continue;
1058             }
1059
1060             foundStartTag = true;
1061             result.startTag.start = foundTagText - 1;
1062             result.startTag.end = foundTagTerminator;
1063         }
1064
1065         if (!foundStartTag) {
1066             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1067                 LOG.finer("Tag \"" + tag + "\" Not Found(2)");
1068             }
1069             return result;
1070         }
1071
1072         // is this an empty element declaration?
1073         if ('/' == source.charAt(result.startTag.end - 1)) {
1074             // end is the start and there is no body
1075             result.body = new charRange(result.startTag.start, result.startTag.end);
1076             result.endTag = new charRange(result.startTag.start, result.startTag.end);
1077             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1078                 LOG.finer("Empty Element \"" + tag + "\" Start : " + result.startTag);
1079             }
1080             return result;
1081         }
1082
1083         current = result.startTag.end + 1;
1084
1085         // if current is past the end then our end tag is not found.
1086         if (current >= end) {
1087             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1088                 LOG.finer("End not found \"" + tag + "\" Start : " + result.startTag);
1089             }
1090             return result;
1091         }
1092
1093         // Begin Phase 2 :  Search for the end tag
1094
1095         String endTag = "</" + tag + ">";
1096         int searchFrom = result.startTag.end + 1;
1097
1098         while (!foundEndTag && (current < end) && (searchFrom < end)) {
1099             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1100                 LOG.finer("Searching for \"" + endTag + "\" in range [" + current + "," + end + "]");
1101             }
1102
1103             int foundTagText = source.indexOf(endTag, current);
1104
1105             // was it not found or not in bounds?
1106             if ((-1 == foundTagText) || ((foundTagText + endTag.length() - 1) > end)) {
1107                 break;
1108             } // it was not found
1109
1110             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1111                 LOG.finer(
1112                         "Prospective tag pair for \"" + tag + "\" " + result.startTag + ":[" + foundTagText + ","
1113                         + (foundTagText + endTag.length() - 1) + "]");
1114             }
1115
1116             // We recurse here in order to exclude the end tags of any sub elements with the same name
1117             charRange subRange = new charRange(searchFrom, foundTagText - 1);
1118
1119             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1120                 LOG.finer("Recursing to search for \"" + tag + "\" in " + subRange);
1121             }
1122
1123             tagRange subElement = getTagRanges(source, tag, subRange);
1124
1125             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1126                 LOG.finer("Recursion result \"" + tag + "\" " + subElement);
1127             }
1128
1129             // if there was an incomplete sub-tag with the same name, skip past it
1130             if (subElement.startTag.isValid()) {
1131                 if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1132                     LOG.finer("Found sub-tag \"" + tag + "\" at " + subElement + " within " + subRange);
1133                 }
1134
1135                 if (subElement.endTag.isValid()) {
1136                     if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1137                         LOG.finer("Complete sub-tag \"" + tag + "\" at " + subElement + " within " + subRange);
1138                     }
1139                     current = subElement.endTag.end + 1;
1140                     searchFrom = subElement.endTag.end + 1;
1141                 } else {
1142                     current = foundTagText + endTag.length();
1143                 }
1144
1145                 continue;
1146             }
1147
1148             foundEndTag = true;
1149             result.endTag.start = foundTagText;
1150             result.endTag.end = foundTagText + endTag.length() - 1;
1151
1152             if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1153                 LOG.finer("Prospective tag \"" + tag + "\" " + result.endTag + " is confirmed.");
1154             }
1155         }
1156
1157         // Begin Phase 3 :  Calculate the location of the body.
1158
1159         result.body.start = result.startTag.end + 1;
1160
1161         if (foundEndTag) {
1162             result.body.end = result.endTag.start - 1;
1163         } else {
1164             result.body.end = end;
1165         }
1166
1167         if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1168             LOG.finer("Found element : \"" + tag + "\" " + result);
1169         }
1170
1171         return result;
1172     }
1173
1174     /**
1175      * Parse a charRange and add any tags found as content as children of a
1176      * specified element. This process is repeated recursivly.
1177      *
1178      * @param scanRange the range to be parsed for sub-tags
1179      * @param addTo     the element to add any discovered children to.
1180      */
1181     protected void addChildTags(final charRange scanRange, LiteXMLElement addTo) {
1182         if (null != uninserted) {
1183             throw new IllegalStateException("This element has not been added to the document.");
1184         }
1185
1186         int current = scanRange.start;
1187
1188         if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1189             LOG.finer("Scanning for children in range " + scanRange);
1190         }
1191
1192         do {
1193             // scan for any tag.
1194             tagRange aSubtag = getTagRanges(getDocument().docContent, null, new charRange(current, scanRange.end));
1195
1196             // did we find one?
1197             if (aSubtag.isValid()) {
1198                 LiteXMLElement newChild = getDocument().createElement(aSubtag);
1199
1200                 if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
1201                     LOG.finer(
1202                             "Adding child tag \""
1203                                     + getDocument().docContent.substring(aSubtag.endTag.start + 2, aSubtag.endTag.end) + "\" "
1204                                     + aSubtag);
1205                 }
1206
1207                 addTo.appendChild(newChild);
1208
1209                 if (paranoidConsistencyChecking) {
1210                     checkConsistency();
1211                 }
1212
1213                 if (!aSubtag.startTag.equals(aSubtag.endTag)) {
1214                     addChildTags(aSubtag.body, newChild); // recurse into the new tag
1215                 }
1216
1217                 // all done this tag, move on
1218                 current = aSubtag.endTag.end + 1;
1219             } else {
1220                 current = -1; // all done!
1221             }
1222         } while ((-1 != current) && (current < scanRange.end));
1223
1224         if (paranoidConsistencyChecking) {
1225             checkConsistency();
1226         }
1227     }
1228
1229     /**
1230      * For this element and all its children adjust the location of its ranges
1231      * by the amount specified.
1232      *
1233      * @param beginningAt adjust all locations which are at or past this
1234      *                    location.
1235      * @param by          amount to adjust all matching locations.
1236      */
1237     protected void adjustLocations(final int beginningAt, final int by) {
1238         if (null != uninserted) {
1239             throw new IllegalStateException("This element has not been added.");
1240         }
1241
1242         // Check that this element is not entirely to the left of the shift
1243         // zone. NB: end can be < start if len is 0.
1244         if (loc.endTag.end < beginningAt && loc.endTag.start < beginningAt) {
1245             return;
1246         }
1247
1248         if ((loc.startTag.end >= beginningAt)
1249                 || ((loc.startTag.start >= beginningAt) && ((loc.startTag.end + 1) == loc.startTag.start))) {
1250             loc.startTag.end += by;
1251         }
1252
1253         if (loc.startTag.start >= beginningAt) {
1254             loc.startTag.start += by;
1255         }
1256
1257         if ((loc.body.end >= beginningAt) || ((loc.body.start >= beginningAt) && ((loc.body.end + 1) == loc.body.start))) {
1258             loc.body.end += by;
1259         }
1260
1261         if (loc.body.start >= beginningAt) {
1262             loc.body.start += by;
1263         }
1264
1265         if ((loc.endTag.end >= beginningAt) || ((loc.endTag.start >= beginningAt) && ((loc.endTag.end + 1) == loc.endTag.start))) {
1266             loc.endTag.end += by;
1267         }
1268
1269         if (loc.endTag.start >= beginningAt) {
1270             loc.endTag.start += by;
1271         }
1272
1273         for (Enumeration<LiteXMLElement> eachChild = getChildren(); eachChild.hasMoreElements();) {
1274             LiteXMLElement aChild = eachChild.nextElement();
1275
1276             aChild.adjustLocations(beginningAt, by);
1277         }
1278
1279         if (paranoidConsistencyChecking) {
1280             checkConsistency();
1281         }
1282     }
1283
1284     /**
1285      * Given a StringBuilder find all occurrences of escaped characters which
1286      * must be decoded and convert them back to their non-escaped equivalents.
1287      * <p/>
1288      * <p/>Also does end of line folding per: <a href="http://www.w3.org/TR/REC-xml#sec-line-ends"/>
1289      *
1290      * @param target The StringBuilder which will be decoded.
1291      * @return The decoded version of the StringBuilder.
1292      */
1293     protected StringBuilder decodeEscaped(StringBuilder target) {
1294
1295         int current = 0;
1296
1297         StringBuilder result = new StringBuilder(target.length());
1298
1299         while (current < target.length()) {
1300             // FIXME bondolo@jxta.org   20010422    Should process xml comments out here.
1301
1302             // fold 0x0D and 0x0D 0x0A to 0x0A
1303             if ('\r' == target.charAt(current)) {
1304                 result.append('\n');
1305                 current++;
1306                 if ((current < target.length()) && ('\n' == target.charAt(current))) {
1307                     current++;
1308                 }
1309                 continue;
1310             }
1311
1312             if ('&' != target.charAt(current)) {
1313                 result.append(target.charAt(current));
1314                 current++;
1315                 continue;
1316             }
1317
1318             int terminusAt = current + 1;
1319
1320             while ((terminusAt < target.length()) && // dont go past end
1321                     ((terminusAt - current) < 6) && // only look 6 chars away.
1322                     (';' != target.charAt(terminusAt))) { // must be a ;
1323                 terminusAt++;
1324             }
1325
1326             if ((terminusAt >= target.length()) || (';' != target.charAt(terminusAt))) {
1327                 // if we get here then we didnt find the terminal we needed
1328                 // so we just leave ampersand as it was, the document is
1329                 // ill-formed but why make things worse?
1330                 result.append(target.charAt(current));
1331                 current++;
1332                 continue;
1333             }
1334
1335             char[] sub = new char[terminusAt - current + 1];
1336
1337             target.getChars(current, terminusAt + 1, sub, 0);
1338             String escaped = new String(sub);
1339
1340             if ("&amp;".equals(escaped)) {
1341                 result.append('&');
1342                 current += 4;
1343             } else if ("&lt;".equals(escaped)) {
1344                 result.append('<');
1345                 current += 3;
1346             } else if ("&gt;".equals(escaped)) { // for compatibility with SGML. We dont encode these
1347                 result.append('>');
1348                 current += 3;
1349             } else if (escaped.startsWith("&#")) {
1350                 String numericChar = escaped.substring(2, escaped.length() - 1);
1351
1352                 // is it &#; ?
1353                 if (numericChar.length() < 1) {
1354                     result.append(target.charAt(current));
1355                     current++;
1356                     continue;
1357                 }
1358
1359                 // is it hex numeric
1360                 if (numericChar.charAt(0) == 'x') {
1361                     numericChar = numericChar.substring(1);
1362
1363                     // is it &#x; ?
1364                     if (numericChar.length() < 1) {
1365                         result.append(target.charAt(current));
1366                         current++;
1367                         continue;
1368                     }
1369
1370                     try {
1371                         char asChar = (char) Integer.parseInt(numericChar.toLowerCase(), 16);
1372
1373                         result.append(asChar);
1374                         current += escaped.length();
1375                     } catch (NumberFormatException badref) {
1376                         // it was bad, we will just skip it.
1377                         result.append(target.charAt(current));
1378                         current++;
1379                     }
1380                     continue;
1381                 }
1382
1383                 // its base 10
1384                 try {
1385                     char asChar = (char) Integer.parseInt(numericChar, 10);
1386
1387                     result.append(asChar);
1388                     current += escaped.length();
1389                 } catch (NumberFormatException badref) {
1390                     // it was bad, we will just skip it.
1391                     result.append(target.charAt(current));
1392                     current++;
1393                 }
1394                 continue;
1395             } else {
1396                 // if we get here then we didn't know what to do with the
1397                 // entity. so we just send it unchanged.
1398                 result.append(target.charAt(current));
1399                 current++;
1400                 continue;
1401             }
1402
1403             current++;
1404         }
1405
1406         return result;
1407     }
1408
1409     /**
1410      * Given a StringBuilder find all occurrences of characters which must be
1411      * escaped and convert them to their escaped equivalents.
1412      *
1413      * @param target The StringBuilder which will be encoded in place.
1414      */
1415     protected void encodeEscaped(StringBuilder target) {
1416
1417         int current = 0;
1418
1419         while (current < target.length()) {
1420             if ('&' == target.charAt(current)) {
1421                 target.insert(current + 1, "amp;");
1422                 current += 5;
1423             } else if ('<' == target.charAt(current)) {
1424                 target.setCharAt(current, '&');
1425                 target.insert(current + 1, "lt;");
1426                 current += 4;
1427             } else {
1428                 current++;
1429             }
1430         }
1431     }
1432
1433     /**
1434      * Returns an enumerations of the attributes associated with this object.
1435      * Each element is of type Attribute.
1436      *
1437      * @return Enumeration the attributes associated with this object.
1438      */
1439     public Enumeration<Attribute> getAttributes() {
1440         List<Attribute> results = new ArrayList<Attribute>();
1441
1442         if (null != uninserted) {
1443             throw new IllegalStateException("This element has not been added.");
1444         }
1445
1446         if (paranoidConsistencyChecking) {
1447             checkConsistency();
1448         }
1449
1450         // find the start of the first attribute
1451         int current = loc.startTag.start + 1;
1452
1453         while (current <= loc.startTag.end) {
1454             char inTagName = getDocument().docContent.charAt(current);
1455
1456             if (Character.isWhitespace(inTagName) || ('/' == inTagName) || ('>' == inTagName)) {
1457                 break;
1458             }
1459             current++;
1460         }
1461
1462         // loop and add attributes to the vector
1463         while (current < loc.startTag.end) {
1464             tagRange nextAttr = getAttributeLoc(null, new charRange(current, loc.startTag.end));
1465
1466             if (!nextAttr.isValid()) {
1467                 break;
1468             }
1469
1470             results.add(
1471                     new Attribute(this, getDocument().docContent.substring(nextAttr.startTag.start, nextAttr.startTag.end + 1)
1472                     ,
1473                     getDocument().docContent.substring(nextAttr.body.start, nextAttr.body.end + 1)));
1474
1475             current = nextAttr.endTag.end + 1;
1476         }
1477
1478         return Collections.enumeration(results);
1479     }
1480
1481     /**
1482      * Returns the tagRange of the next attribute contained in the range
1483      * provided. The tag range returned consists of the startTag indicating
1484      * the location of the name, body indicating the location of the value and
1485      * endTag indicating the location of the final quote delimiter.
1486      *
1487      * @param name    Name to match. null means match any name.
1488      * @param inRange the limits of the locations to scan.
1489      * @return tagRange containing the location of the next attribute
1490      */
1491     protected tagRange getAttributeLoc(String name, charRange inRange) {
1492         tagRange result = new tagRange();
1493         int current = inRange.start;
1494
1495         do {
1496             // skip the whitespace
1497
1498             while (current <= inRange.end) {
1499                 char inTagName = getDocument().docContent.charAt(current);
1500
1501                 if (!Character.isWhitespace(inTagName) && ('/' != inTagName) && ('>' != inTagName)) {
1502                     break;
1503                 }
1504                 current++;
1505             }
1506
1507             int equalsAt = getDocument().docContent.indexOf("=", current);
1508
1509             // make sure there is an equals
1510             if ((-1 == equalsAt) || (equalsAt >= inRange.end)) {
1511                 return result;
1512             }
1513
1514             // get the name
1515             result.startTag.start = current;
1516             result.startTag.end = equalsAt - 1;
1517
1518             // get the quote char we must match
1519             String requiredQuote = getDocument().docContent.substring(equalsAt + 1, equalsAt + 2);
1520
1521             // make sure its a valid quote
1522             if (('\'' != requiredQuote.charAt(0)) && ('\"' != requiredQuote.charAt(0))) {
1523                 return result;
1524             }
1525
1526             // find the next occurance of this quote
1527             int nextQuote = getDocument().docContent.indexOf(requiredQuote, equalsAt + 2);
1528
1529             // make sure the quote is in a good spot.
1530             if ((-1 == nextQuote) || (nextQuote >= inRange.end)) {
1531                 return result;
1532             }
1533
1534             result.body.start = equalsAt + 2;
1535             result.body.end = nextQuote - 1;
1536
1537             result.endTag.start = nextQuote;
1538             result.endTag.end = nextQuote;
1539
1540             // check if the name matches.
1541             if ((null != name) && !name.equals(getDocument().docContent.substring(result.startTag.start, result.startTag.end + 1))) {
1542                 result.startTag.start = -1;
1543             }
1544
1545             current = nextQuote + 1;
1546         } while ((current < inRange.end) && (!result.isValid()));
1547
1548         return result;
1549     }
1550
1551     /**
1552      * {@inheritDoc}
1553      */
1554     public String addAttribute(String name, String value) {
1555         if (null != uninserted) {
1556             throw new IllegalStateException("This element has not been added.");
1557         }
1558
1559         if (null == name) {
1560             throw new IllegalArgumentException("name must not be null");
1561         }
1562
1563         if (null == value) {
1564             throw new IllegalArgumentException("value must not be null");
1565         }
1566
1567         for (int eachChar = name.length() - 1; eachChar >= 0; eachChar--) {
1568             if (Character.isWhitespace(name.charAt(eachChar))) {
1569                 throw new IllegalArgumentException("Attribute names may not contain spaces.");
1570             }
1571         }
1572
1573         if (paranoidConsistencyChecking) {
1574             checkConsistency();
1575         }
1576
1577         // skip past the name portion
1578         int current = loc.startTag.start + 1;
1579
1580         while (current <= loc.startTag.end) {
1581             char inTagName = getDocument().docContent.charAt(current);
1582
1583             if (Character.isWhitespace(inTagName) || ('/' == inTagName) || ('>' == inTagName)) {
1584                 break;
1585             }
1586
1587             current++;
1588         }
1589
1590         // find out if there was a previous value for this name
1591         String oldValue = null;
1592         tagRange oldAttr = getAttributeLoc(name, new charRange(current, loc.startTag.end));
1593
1594         // choose which kind of quote to use
1595         char usingQuote = (-1 != value.indexOf('"')) ? '\'' : '\"';
1596
1597         // make sure we can use it.
1598         if (('\'' == usingQuote) && (-1 != value.indexOf('\''))) {
1599             throw new IllegalArgumentException("Value contains both \" and \'");
1600         }
1601
1602         // build the new attribute string
1603         StringBuilder newStuff = new StringBuilder(" ");
1604
1605         newStuff.append(name);
1606         newStuff.append("=");
1607         newStuff.append(usingQuote);
1608         newStuff.append(value);
1609         newStuff.append(usingQuote);
1610
1611         // add it in.
1612         if (!oldAttr.isValid()) {
1613             // we aren't replacing an existing value
1614             getDocument().docContent.insert(current, newStuff.toString());
1615
1616             // move all doc locations which follow this one based on how much we
1617             // inserted.
1618             getDocument().adjustLocations(current, newStuff.length());
1619         } else {
1620             // we are replacing an existing value
1621             oldValue = getDocument().docContent.substring(oldAttr.body.start, oldAttr.body.end + 1);
1622
1623             getDocument().docContent.delete(oldAttr.body.start, oldAttr.body.end + 1);
1624             getDocument().docContent.insert(oldAttr.body.start, value);
1625
1626             int delta = value.length() - (oldAttr.body.end - oldAttr.body.start + 1);
1627
1628             // move all doc locations which follow this one based on how much we
1629             // inserted or deleted.
1630             getDocument().adjustLocations(loc.startTag.start + 1, delta);
1631         }
1632
1633         if (paranoidConsistencyChecking) {
1634             checkConsistency();
1635         }
1636
1637         return oldValue;
1638     }
1639
1640     /**
1641      * {@inheritDoc}
1642      */
1643     public String addAttribute(Attribute newAttrib) {
1644         return addAttribute(newAttrib.getName(), newAttrib.getValue());
1645     }
1646
1647     /**
1648      * {@inheritDoc}
1649      */
1650     public Attribute getAttribute(String name) {
1651         if (null != uninserted) {
1652             throw new IllegalStateException("This element has not been added.");
1653         }
1654
1655         if (paranoidConsistencyChecking) {
1656             checkConsistency();
1657         }
1658
1659         // skip past the name portion
1660         int current = loc.startTag.start + 1;
1661
1662         while (current <= loc.startTag.end) {
1663             char inTagName = getDocument().docContent.charAt(current);
1664
1665             if (Character.isWhitespace(inTagName) || ('/' == inTagName) || ('>' == inTagName)) {
1666                 break;
1667             }
1668
1669             current++;
1670         }
1671
1672         // find the attribute matching this name
1673         tagRange attr = getAttributeLoc(name, new charRange(current, loc.startTag.end));
1674
1675         if (!attr.isValid()) {
1676             return null;
1677         }
1678
1679         // build the object
1680         return new Attribute(this, getDocument().docContent.substring(attr.startTag.start, attr.startTag.end + 1)
1681                 ,
1682                 getDocument().docContent.substring(attr.body.start, attr.body.end + 1));
1683     }
1684
1685     protected boolean checkConsistency() {
1686         assert loc.isValid();
1687
1688         charRange elementRange = new charRange(loc.startTag.start, loc.endTag.end);
1689
1690         assert elementRange.contains(loc.startTag);
1691         assert elementRange.contains(loc.body);
1692         assert elementRange.contains(loc.endTag);
1693
1694         if (null != children) {
1695             Iterator<LiteXMLElement> eachChild = children.iterator();
1696             Iterator<LiteXMLElement> nextChilds = children.iterator();
1697
1698             if (nextChilds.hasNext()) {
1699                 nextChilds.next();
1700             }
1701
1702             while (eachChild.hasNext()) {
1703                 LiteXMLElement aChild = eachChild.next();
1704
1705                 assert loc.contains(aChild.loc);
1706
1707                 if (nextChilds.hasNext()) {
1708                     LiteXMLElement nextChild = nextChilds.next();
1709
1710                     assert aChild.loc.compareTo(nextChild.loc) < 0;
1711                 } else {
1712                     assert !eachChild.hasNext();
1713                 }
1714
1715                 aChild.checkConsistency();
1716             }
1717         }
1718         return true;
1719     }
1720
1721     /**
1722      * The document we are a part of.
1723      *
1724      * @return The document we are a part of.
1725      */
1726     LiteXMLDocument getDocument() {
1727         return doc;
1728     }
1729 }