001package com.hfg.xml;
002
003import java.io.*;
004import java.util.*;
005
006import org.xml.sax.InputSource;
007
008import com.hfg.util.Case;
009import com.hfg.util.CompareUtil;
010import com.hfg.util.collection.OrderedMap;
011import com.hfg.util.io.GZIP;
012import com.hfg.util.io.NoClosePrintWriter;
013import com.hfg.util.StringUtil;
014import com.hfg.util.collection.CollectionUtil;
015import com.hfg.xml.parser.XMLTagReader;
016
017//------------------------------------------------------------------------------
018/**
019 XMLTag is a generic container for XML. It can be used both to construct XML or
020 to deconstruct XML into a DOM-like structure.
021
022 @author J. Alex Taylor, hairyfatguy.com
023 */
024//------------------------------------------------------------------------------
025// com.hfg XML/HTML Coding Library
026//
027// This library is free software; you can redistribute it and/or
028// modify it under the terms of the GNU Lesser General Public
029// License as published by the Free Software Foundation; either
030// version 2.1 of the License, or (at your option) any later version.
031//
032// This library is distributed in the hope that it will be useful,
033// but WITHOUT ANY WARRANTY; without even the implied warranty of
034// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
035// Lesser General Public License for more details.
036//
037// You should have received a copy of the GNU Lesser General Public
038// License along with this library; if not, write to the Free Software
039// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
040//
041// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
042// jataylor@hairyfatguy.com
043//------------------------------------------------------------------------------
044   
045public class XMLTag extends XMLContainerImpl implements XMLNode, Comparable
046{
047
048   //###########################################################################
049   // PRIVATE FIELDS
050   //###########################################################################
051
052   private String mName;
053   private Map<String, XMLAttribute> mAttributes;
054   private XMLNamespace mNamespace;
055   // Attribute sorting is preferred for the consistency of attribute order
056   private boolean mSortAttributesBeforeWriting = true;
057
058   private static char sQuoteChar = '\''; // Not final since it can be changed
059   private static final String  NL = System.getProperty("line.separator");
060
061   //###########################################################################
062   // CONSTRUCTORS
063   //###########################################################################
064   
065   //---------------------------------------------------------------------------
066   /**
067    @param inName the tag name
068    */
069   public XMLTag(String inName)
070   {
071      setTagName(inName);
072   }
073
074   //---------------------------------------------------------------------------
075   /**
076    @param inName the tag name
077    */
078   public XMLTag(XMLName inName)
079   {
080      if (inName != null)
081      {
082         setTagName(inName.getLocalName());
083         setNamespace(inName.getNamespace());
084      }
085   }
086
087   //---------------------------------------------------------------------------
088   /**
089    @param inName the tag name
090    @param inAttributes a Collection of XMLAttribute objects
091    */
092   public XMLTag(String inName, Collection<XMLAttribute> inAttributes)
093   {
094      setTagName(inName);
095      setAttributes(inAttributes);
096   }
097  
098   //---------------------------------------------------------------------------
099   /**
100    @param inName the tag name
101    @param inAttributes a Map of attribute (names and values as String objects)
102    */
103   public XMLTag(String inName, Map<String,String> inAttributes)
104   {
105      if (CollectionUtil.hasValues(inAttributes))
106      {
107         List<XMLAttribute> attributes = new ArrayList<>();
108
109         for (String name : inAttributes.keySet())
110         {
111            attributes.add(new XMLAttribute(name, inAttributes.get(name)));
112         }
113
114         setAttributes(attributes);
115      }
116      setTagName(inName);
117   }
118   
119   //---------------------------------------------------------------------------
120   /**
121    @param inName the tag name
122    @param inAttributes a Collection of XMLAttribute objects
123    @param inContent the tag's content
124    */
125   public XMLTag(String inName, Collection inAttributes, String inContent)
126   {
127      this(inName, inAttributes);
128      setContent(inContent);
129   }
130   
131   //---------------------------------------------------------------------------
132   /**
133    @param inName the tag name
134    @param inAttributes a ImageMap of attribute (names and values as String objects)
135    @param inContent the tag's content
136    */
137   public XMLTag(String inName, Map inAttributes, String inContent)
138   {
139      this(inName, inAttributes);
140      setContent(inContent);
141   }
142   
143   //----------------------------------------------------------------------
144   /**
145    @param inXML an InputStream containing XML text
146    */
147   public XMLTag(InputStream inXML)
148         throws XMLException, IOException
149   {
150      construct(inXML);
151   }
152
153   //----------------------------------------------------------------------
154   /**
155    @param inXML a Reader containing XML text
156    */
157   public XMLTag(Reader inXML)
158         throws XMLException, IOException
159   {
160      construct(inXML);
161   }
162
163   //---------------------------------------------------------------------------
164   /**
165    @param inXMLFile a File containing XML text
166    */
167   public XMLTag(File inXMLFile)
168         throws XMLException, IOException
169   {
170      if (null == inXMLFile)
171      {
172         throw new XMLException("null File passed to XMLTag()!");
173      }
174
175      construct(new FileReader(inXMLFile));
176   }
177 
178   
179   //###########################################################################
180   // PUBLIC METHODS
181   //###########################################################################
182
183   //---------------------------------------------------------------------------
184   public static void useDoubleQuotes(boolean inValue)
185   {
186      if (inValue)
187      {
188         sQuoteChar = '\"';
189      }
190      else
191      {
192         sQuoteChar = '\'';
193      }
194   }
195
196   //---------------------------------------------------------------------------
197   public XMLTag setSortAttributesBeforeWriting(boolean inValue)
198   {
199      mSortAttributesBeforeWriting = inValue;
200      return this;
201   }
202
203   //---------------------------------------------------------------------------
204   @Override
205   public XMLTag clone()
206   {
207      XMLTag cloneObj = super.clone();
208
209      if (mAttributes != null)
210      {
211         cloneObj.mAttributes = new OrderedMap<>();
212         for (String key : mAttributes.keySet())
213         {
214            cloneObj.mAttributes.put(key, mAttributes.get(key).clone());
215         }
216      }
217
218      return cloneObj;
219   }
220
221   //---------------------------------------------------------------------------
222   @Override
223   public boolean equals(Object inObj2)
224   {
225      return (0 == compareTo(inObj2));
226   }
227
228   //--------------------------------------------------------------------------
229   @Override
230   public int compareTo(Object inObj2)
231   {
232      int result = -1;
233
234      if (inObj2 instanceof XMLTag)
235      {
236         XMLTag tag2 = (XMLTag) inObj2;
237
238         result = CompareUtil.compare(getTagName(), tag2.getTagName());
239
240         if (0 == result)
241         {
242            // Compare attributes
243            result = CompareUtil.compare(getAttributes(), tag2.getAttributes());
244
245            if (0 == result)
246            {
247               // Compare content & subtags
248               result = CompareUtil.compare(getContentPlusSubtagList(), tag2.getContentPlusSubtagList());
249            }
250         }
251      }
252
253      return result;
254   }
255
256   //---------------------------------------------------------------------------
257   @Override
258   public XMLNode setContent(CharSequence inContent)
259   {
260      return (XMLNode) super.setContent(inContent);
261   }
262
263
264   //--------------------------------------------------------------------------
265   public XMLTag addSubtag(String inTagName)
266   {
267      XMLTag subtag = new XMLTag(inTagName);
268      addSubtag(subtag);
269
270      return subtag;
271   }
272
273   //--------------------------------------------------------------------------
274   public XMLTag addSubtag(XMLName inTagName)
275   {
276      XMLTag subtag = new XMLTag(inTagName);
277      addSubtag(subtag);
278
279      return subtag;
280   }
281
282   //--------------------------------------------------------------------------
283   public String getStartTag()
284   {
285      Collection attributes = null;
286      if (mAttributes != null) attributes = mAttributes.values();
287
288      return XMLUtil.composeStartTag(getTagName(), attributes, false);
289   }
290
291   //--------------------------------------------------------------------------
292   public String getEndTag()
293   {
294      return XMLUtil.composeEndTag(getTagName());
295   }
296
297   //--------------------------------------------------------------------------
298   @Override
299   public boolean isEmptyTag()
300   {
301      boolean returnValue = false;
302
303      if (null == mContentAndSubtagList
304            || 0 == mContentAndSubtagList.size())
305      {
306         returnValue = true;
307      }
308
309      return returnValue;
310   }
311
312
313   //---------------------------------------------------------------------------
314   @Override
315   public XMLTag setTagName(XMLName inName)
316   {
317      // Check the name for XML validity.
318      XMLUtil.checkXMLNameValidity(inName.getLocalName());
319
320      mName = inName.getLocalName();
321      setNamespace(inName.getNamespace());
322
323      return this;
324   }
325
326   //---------------------------------------------------------------------------
327   @Override
328   public XMLTag setTagName(String inName)
329   {
330      // Check the name for XML validity.
331      XMLUtil.checkXMLNameValidity(inName);
332
333      mName = inName;
334
335      return this;
336   }
337
338   //---------------------------------------------------------------------------
339   @Override
340   public String getTagName()
341   {
342      return mName;
343   }
344
345   //---------------------------------------------------------------------------
346   public String getQualifiedTagName()
347   {
348      return (mNamespace != null && mNamespace.getPrefix() != null ? mNamespace.getPrefix() + ":" : "") + mName;
349   }
350
351   //---------------------------------------------------------------------------
352   @Override
353   public void verifyTagName(XMLName inTagName)
354   {
355      verifyTagName(inTagName, Case.SENSITIVE);
356   }
357
358   //---------------------------------------------------------------------------
359   @Override
360   public void verifyTagName(XMLName inTagName, Case inCaseSensitivity)
361   {
362      try
363      {
364         verifyTagName(inTagName.getLocalName(), inCaseSensitivity);
365      }
366      catch (XMLException e)
367      {
368         // Try the qualified tag name
369         verifyTagName(inTagName.getQualifiedName(), inCaseSensitivity);
370      }
371   }
372
373   //---------------------------------------------------------------------------
374   @Override
375   public void verifyTagName(String inTagName)
376   {
377      verifyTagName(inTagName, Case.SENSITIVE);
378   }
379
380   //---------------------------------------------------------------------------
381   @Override
382  public void verifyTagName(String inTagName, Case inCaseSensitivity)
383   {
384      boolean verified = false;
385      if (Case.INSENSITIVE.equals(inCaseSensitivity))
386      {
387         if (getTagName().equalsIgnoreCase(inTagName))
388         {
389            verified = true;
390         }
391      }
392      else if (getTagName().equals(inTagName))
393      {
394         verified = true;
395      }
396
397      if (! verified)
398      {
399         throw new XMLException("Expected a tag name of " + StringUtil.singleQuote(inTagName)
400                                + " but was given a tag with name " + StringUtil.singleQuote(getTagName()) + "!");
401      }
402   }
403
404
405   //---------------------------------------------------------------------------
406   @Override
407   public XMLTag setAttribute(String inAttributeName, Object inValue)
408   {
409      String stringValue = (inValue != null ? inValue.toString() : "");
410      XMLAttribute attribute = new XMLAttribute(inAttributeName, stringValue);
411      setAttribute(attribute);
412
413      return this;
414   }
415
416   //---------------------------------------------------------------------------
417   @Override
418   public XMLTag setAttribute(XMLName inAttributeName, Object inValue)
419   {
420      String stringValue = (inValue != null ? inValue.toString() : "");
421      XMLAttribute attribute = new XMLAttribute(inAttributeName, stringValue);
422      setAttribute(attribute);
423
424      return this;
425   }
426
427   //---------------------------------------------------------------------------
428   @Override
429   public XMLTag setAttribute(XMLAttribute inAttribute)
430   {
431      if (inAttribute != null)
432      {
433         if (null == mAttributes) mAttributes = new OrderedMap<>(4);
434
435         // Avoid overstriking the attribute's element when the attribute was
436         // reused without cloning. 
437         if (inAttribute.getElement() != null)
438         {
439            inAttribute = inAttribute.clone();
440         }
441
442         mAttributes.put(inAttribute.getName(), inAttribute);
443
444         inAttribute.setElement(this);
445      }
446
447      return this;
448   }
449
450   //---------------------------------------------------------------------------
451   /**
452    Takes a Collection of XMLAttribute objects as input.
453    */
454   @Override
455   public void setAttributes(Collection<XMLAttribute> inAttributes)
456   {
457      if (inAttributes != null)
458      {
459         for (XMLAttribute attribute : inAttributes)
460         {
461            setAttribute(attribute);
462         }
463      }
464   }
465
466   //---------------------------------------------------------------------------
467   @Override
468   public XMLAttribute getAttribute(XMLName inAttributeName)
469   {
470      return getAttribute(inAttributeName.getLocalName());
471   }
472
473   //---------------------------------------------------------------------------
474   @Override
475   public XMLAttribute getAttribute(String inAttributeName)
476   {
477      XMLAttribute attribute = null;
478
479      if (mAttributes != null)
480      {
481         attribute = mAttributes.get(inAttributeName);
482      }
483
484      return attribute;
485   }
486
487   //---------------------------------------------------------------------------
488   @Override
489   public boolean hasAttributes()
490   {
491      return (CollectionUtil.hasValues(mAttributes));
492   }
493
494   //---------------------------------------------------------------------------
495   @Override
496   public boolean hasAttribute(XMLName inAttributeName)
497   {
498      return (getAttribute(inAttributeName) != null);
499   }
500
501   //---------------------------------------------------------------------------
502   @Override
503   public boolean hasAttribute(String inAttributeName)
504   {
505      return (getAttribute(inAttributeName) != null);
506   }
507
508   //---------------------------------------------------------------------------
509   @Override
510   public String getAttributeValue(XMLName inAttributeName)
511   {
512      return getAttributeValue(inAttributeName.getLocalName());
513   }
514
515   //---------------------------------------------------------------------------
516   @Override
517   public String getAttributeValue(String inAttributeName)
518   {
519      XMLAttribute attribute = getAttribute(inAttributeName);
520
521      String value = null;
522
523      if (attribute != null)
524      {
525         value = attribute.getUnscapedValue();
526      }
527
528      return value;
529   }
530   
531   //---------------------------------------------------------------------------
532   /**
533    Returns a Collection of XMLAttribute objects.
534    */
535   @Override
536   public Collection<XMLAttribute> getAttributes()
537   {
538      Collection<XMLAttribute> attributes = null;
539
540      if (mAttributes != null)
541      {
542         attributes = mAttributes.values();
543      }
544
545      return attributes;
546   }
547
548   //---------------------------------------------------------------------------
549   @Override
550   public XMLAttribute removeAttribute(String inAttributeName)
551   {
552      XMLAttribute attribute = null;
553
554      if (mAttributes != null)
555      {
556         attribute = mAttributes.remove(inAttributeName);
557      }
558
559      return attribute;
560   }
561
562   //---------------------------------------------------------------------------
563   @Override
564   public XMLAttribute removeAttribute(XMLName inAttributeName)
565   {
566      return removeAttribute(inAttributeName.getLocalName());
567   }
568
569
570
571   //---------------------------------------------------------------------------
572   @Override
573   public XMLNamespace getNamespace()
574   {
575      return mNamespace;
576   }
577
578   //---------------------------------------------------------------------------
579   @Override
580   public XMLTag setNamespace(XMLNamespace inValue)
581   {
582      mNamespace = inValue;
583
584      return this;
585   }
586
587   //---------------------------------------------------------------------------
588   public XMLTag setDefaultXMLNamespaceDeclaration(XMLNamespace inValue)
589   {
590      return setAttribute("xmlns", inValue.getURI());
591   }
592
593   //---------------------------------------------------------------------------
594   public XMLTag addXMLNamespaceDeclaration(XMLNamespace inValue)
595   {
596      return setAttribute("xmlns" + (StringUtil.isSet(inValue.getPrefix()) ? ":" + inValue.getPrefix() : ""), inValue.getURI());
597   }
598
599   //---------------------------------------------------------------------------
600   /**
601    Returns a List of all nodes with the specified attribute name and specified attribute value.
602    */
603   @Override
604   public List<? extends XMLNode> findNodesByAttributeValue(XMLName inAttribute, String inValue)
605   {
606      return findNodesByAttributeValue(inAttribute.getLocalName(), inValue);
607   }
608
609   //---------------------------------------------------------------------------
610   /**
611    Returns a List of all nodes with the specified attribute name and specified attribute value.
612    */
613   @Override
614   public List<? extends XMLNode> findNodesByAttributeValue(String inAttribute, String inValue)
615   {
616      List<XMLNode> outList = new ArrayList<>();
617
618      recursiveFindNodesByAttributeValue(inAttribute, inValue, outList);
619
620      return outList;
621   }
622
623   //---------------------------------------------------------------------------
624   @Override
625   public String toString()
626   {
627      return "<" + mName + ">";
628   }
629
630   //---------------------------------------------------------------------------
631   @Override
632   public String toXML()
633   {
634      ByteArrayOutputStream outStream = null;
635      try
636      {
637         outStream = new ByteArrayOutputStream(2048);
638         toXML(outStream);
639         outStream.close();
640      }
641      catch (Exception e)
642      {
643         throw new XMLException(e);
644      }
645
646      return outStream.toString();
647   }
648
649   //---------------------------------------------------------------------------
650   @Override
651   public void toXML(OutputStream inStream)
652   {
653      PrintWriter writer = new PrintWriter(inStream);
654
655      toXML(writer);
656      
657      writer.flush();
658   }
659
660   //---------------------------------------------------------------------------
661   @Override
662   public synchronized void toXML(Writer inWriter)
663   {
664      toXML(inWriter, null);
665   }
666
667   //---------------------------------------------------------------------------
668   protected void toXML(Writer inWriter, XMLNamespaceSet inDeclaredNamespaces)
669   {
670      try
671      {
672         XMLNamespaceSet declaredNamespaces = updateDeclaredNamespaces(inDeclaredNamespaces);
673
674         String tagName = getTagName();
675         if (tagName != null)
676         {
677            String nsPrefix = null;
678            boolean addedNamespaceAttr = false;
679            if (mNamespace != null)
680            {
681               // Namespace already declared?
682               if (declaredNamespaces != null
683                   && declaredNamespaces.contains(mNamespace)
684                   && !mNamespace.equals(declaredNamespaces.getDefault()))
685               {
686                  nsPrefix = mNamespace.getPrefix();
687               }
688               else if (null == declaredNamespaces
689                        || ! declaredNamespaces.contains(mNamespace))
690               {
691                  // Need to declare the namespace
692                  setDefaultXMLNamespaceDeclaration(mNamespace);
693                  addedNamespaceAttr = true;
694
695                  if (null == declaredNamespaces)
696                  {
697                     declaredNamespaces = new XMLNamespaceSet(4);
698                  }
699
700                  declaredNamespaces.setDefault(mNamespace);
701               }
702            }
703
704            if (nsPrefix != null)
705            {
706               tagName = nsPrefix + ":" + tagName;
707            }
708
709            writeStartTag(inWriter, declaredNamespaces, tagName);
710
711            if (addedNamespaceAttr)
712            {
713               // We shouldn't permanently alter the tag.
714               removeAttribute("xmlns");
715            }
716         }
717
718         if (mContentAndSubtagList != null)
719         {
720            for (Object content : mContentAndSubtagList)
721            {
722               if (content instanceof String)
723               {
724                  inWriter.write(content.toString());
725               }
726               else if (content instanceof byte[])
727               {
728                  inWriter.write(GZIP.uncompressToString((byte[]) content));
729               }
730               else
731               {
732                  XMLizable subtag = (XMLizable) content;
733                  if (subtag instanceof XMLTag)
734                  {
735                     ((XMLTag)subtag).toXML(inWriter, declaredNamespaces);
736                  }
737                  else
738                  {
739                     subtag.toXML(inWriter);
740                  }
741               }
742            }
743         }
744
745         if (tagName != null)
746         {
747            // Closing tag?
748            if (!isEmptyTag())
749            {
750               inWriter.write(XMLUtil.composeEndTag(tagName));
751            }
752         }
753      }
754      catch (IOException e)
755      {
756         throw new RuntimeException(e);
757      }
758   }
759
760   //---------------------------------------------------------------------------
761   @Override
762   public String toIndentedXML(int inInitialIndentLevel, int inIndentSize)
763   {
764      ByteArrayOutputStream outStream;
765      try
766      {
767         outStream = new ByteArrayOutputStream();
768         PrintWriter writer = new PrintWriter(outStream);
769         toIndentedXML(writer, inInitialIndentLevel, inIndentSize);
770         writer.close();
771      }
772      catch (Exception e)
773      {
774         throw new XMLException(e);
775      }
776
777      return outStream.toString();
778   }
779   
780   //---------------------------------------------------------------------------
781   /**
782    Writes out the tag (and any subtags) to te specified OutputStream. Note that
783    the OutputStream is not closed by this method.
784    * @param inOutputStream OutputStream to which the XML is written.
785    */
786   @Override
787   public void toIndentedXML(OutputStream inOutputStream, int inInitialIndentLevel, int inIndentSize)
788   {
789       PrintWriter writer = null;
790       try
791       {
792           writer = new NoClosePrintWriter(inOutputStream);
793           toIndentedXML(writer, inInitialIndentLevel, inIndentSize);
794       }
795       finally
796       {
797           if (writer != null) writer.close();
798       }
799   }
800
801   //--------------------------------------------------------------------------
802   @Override
803   public synchronized void toIndentedXML(Writer inWriter, int inInitialIndentLevel, int inIndentSize)
804   {
805      toIndentedXML(inWriter, inInitialIndentLevel, inIndentSize, null);
806   }
807
808   //--------------------------------------------------------------------------
809   @Override
810   public synchronized void toIndentedXML(Writer inWriter, int inInitialIndentLevel, int inIndentSize,
811                                          XMLNamespaceSet inDeclaredNamespaces)
812   {
813      try
814      {
815         boolean emptyTag = isEmptyTag();
816         boolean contentPresent = false;
817         String indent = StringUtil.polyChar(' ', inInitialIndentLevel * inIndentSize);
818
819         if (inInitialIndentLevel > 0)
820         {
821            inWriter.write(NL);
822            inWriter.write(indent);
823         }
824
825         XMLNamespaceSet declaredNamespaces = updateDeclaredNamespaces(inDeclaredNamespaces);
826
827         String tagName = getTagName();
828         if (tagName != null)
829         {
830            String nsPrefix = null;
831            boolean addedNamespaceAttr = false;
832            if (mNamespace != null)
833            {
834               // Namespace already declared?
835               if (declaredNamespaces != null
836                   && ! mNamespace.equals(declaredNamespaces.getDefault()))
837               {
838                  nsPrefix = mNamespace.getPrefix();
839               }
840               else if (null == declaredNamespaces
841                        || ! declaredNamespaces.contains(mNamespace))
842               {
843                  // Need to declare the namespace
844                  setDefaultXMLNamespaceDeclaration(mNamespace);
845                  addedNamespaceAttr = true;
846
847                  if (null == declaredNamespaces)
848                  {
849                     declaredNamespaces = new XMLNamespaceSet(4);
850                  }
851
852                  declaredNamespaces.setDefault(mNamespace);
853               }
854            }
855
856            if (nsPrefix != null)
857            {
858               tagName = nsPrefix + ":" + tagName;
859            }
860
861            writeStartTag(inWriter, declaredNamespaces, tagName);
862
863            if (addedNamespaceAttr)
864            {
865               // We shouldn't permanently alter the tag.
866               removeAttribute("xmlns");
867            }
868         }
869
870         if (mContentAndSubtagList != null)
871         {
872            for (int i = 0; i < mContentAndSubtagList.size(); i++)
873            {
874               Object content = mContentAndSubtagList.get(i);
875
876               if (content instanceof XMLNode)
877               {
878                  ((XMLNode) content).toIndentedXML(inWriter, (contentPresent ? 0 : inInitialIndentLevel + 1), inIndentSize,
879                                                    declaredNamespaces);
880               }
881               else if (content instanceof String)
882               {
883                  inWriter.write(content.toString());
884                  contentPresent = true;
885               }
886               else if (content instanceof byte[])
887               {
888                  byte[] bytes = (byte[]) content;
889                  String contentValue = GZIP.uncompressToString(bytes);
890                  contentPresent = true;
891
892                  inWriter.write(contentValue);
893               }
894               else if (content instanceof XMLComment)
895               {
896                  if (!contentPresent)
897                  {
898                     inWriter.write(NL);
899                     inWriter.write(StringUtil.polyChar(' ', (inInitialIndentLevel + 1) * inIndentSize));
900                  }
901                  inWriter.write(((XMLComment) content).toXML());
902//               if (!contentPresent)
903//               {
904//                  inWriter.println();
905//               }
906               }
907               else if (content instanceof XMLizable)
908               {
909                  inWriter.write(((XMLizable) content).toXML());
910               }
911            }
912         }
913
914         if (tagName != null)
915         {
916            if (!emptyTag)
917            {
918               if (!contentPresent)
919               {
920                  inWriter.write(NL);
921                  inWriter.write(indent);
922               }
923               inWriter.write(XMLUtil.composeEndTag(tagName));
924            }
925         }
926
927         if (inInitialIndentLevel == 0) inWriter.flush();
928      }
929      catch (IOException e)
930      {
931         throw new RuntimeException(e);
932      }
933   }
934
935   //--------------------------------------------------------------------------
936   @Override
937   public void replaceCharacterEntities()
938   {
939      // Check the attribute values
940      if (mAttributes != null
941          && mAttributes.size() > 0)
942      {
943         for (XMLAttribute attr : mAttributes.values())
944         {
945            String newValue = XMLUtil.convertCharacterEntitiesToNumeric(attr.getValue());
946            if (! newValue.equals(attr.getValue()))
947            {
948               attr.setValue(newValue);
949            }
950         }
951      }
952
953      if (mContentAndSubtagList != null)
954      {
955         for (int i = 0; i < mContentAndSubtagList.size(); i++)
956         {
957            Object content = mContentAndSubtagList.get(i);
958
959            if (content instanceof XMLNode)
960            {
961               ((XMLNode)content).replaceCharacterEntities();
962            }
963            else if (content instanceof String)
964            {
965               String newValue = XMLUtil.convertCharacterEntitiesToNumeric((String)content);
966               if (! newValue.equals(content))
967               {
968                  mContentAndSubtagList.set(i, newValue);
969               }
970            }
971            else if (content instanceof byte[])
972            {
973               byte[] bytes = (byte[]) content;
974               String contentValue = GZIP.uncompressToString(bytes);
975
976               String newValue = XMLUtil.convertCharacterEntitiesToNumeric(contentValue);
977               if (! newValue.equals(contentValue))
978               {
979                  mContentAndSubtagList.set(i, GZIP.compress(newValue));
980               }
981            }
982         }
983      }
984   }
985
986   //##########################################################################
987   // PROTECTED METHODS
988   //##########################################################################
989
990   //---------------------------------------------------------------------------
991   protected void sortAttributes(List<XMLAttribute> inAttributes)
992   {
993      if (inAttributes != null)
994      {
995         Collections.sort(inAttributes);
996      }
997   }
998
999   //##########################################################################
1000   // PRIVATE METHODS
1001   //##########################################################################
1002
1003   //----------------------------------------------------------------------
1004   private void construct(InputStream inXML)
1005         throws XMLException, IOException
1006   {
1007      construct(new InputStreamReader(inXML));
1008   }
1009
1010   //----------------------------------------------------------------------
1011   private void construct(Reader inXML)
1012         throws XMLException, IOException
1013   {
1014      XMLTag rootTag = createFromInputSource(new InputSource(inXML));
1015      setTagName(rootTag.getTagName());
1016      setAttributes(rootTag.getAttributes());
1017      mNamespace = rootTag.mNamespace;
1018      mContentAndSubtagList = rootTag.mContentAndSubtagList;
1019   }
1020
1021   //----------------------------------------------------------------------
1022   private static synchronized XMLTag createFromInputSource(InputSource inXML)
1023         throws XMLException, IOException
1024   {
1025      XMLTagReader tagReader = new XMLTagReader();
1026      tagReader.parse(inXML);
1027      return tagReader.getRootNode();
1028
1029/*      try
1030      {
1031         XMLReader parser = getParser();
1032         parser.setContentHandler(getDocHandler());
1033         parser.setProperty("http://xml.org/sax/properties/lexical-handler", getDocHandler());
1034         parser.parse(inXML);
1035      }
1036      catch (Exception e)
1037      {
1038         throw new XMLException(e);
1039      }
1040
1041      return getDocHandler().getRootTag();
1042*/
1043   }
1044
1045
1046   //---------------------------------------------------------------------------
1047   private void recursiveFindNodesByAttributeValue(String inAttribute, String inValue, List<XMLNode> inList)
1048   {
1049      if (inAttribute != null)
1050      {
1051         if (hasAttribute(inAttribute)
1052             && (null == inValue
1053                 || getAttributeValue(inAttribute).equals(inValue)))
1054         {
1055            inList.add(this);
1056         }
1057      }
1058      else
1059      {
1060         for (XMLAttribute attr : getAttributes())
1061         {
1062            if (null == inValue
1063                || attr.getValue().equals(inValue))
1064            {
1065               inList.add(this);
1066            }
1067         }
1068      }
1069
1070      if (mContentAndSubtagList != null)
1071      {
1072         for (Object content : mContentAndSubtagList)
1073         {
1074            if (content instanceof XMLTag)
1075            {
1076               ((XMLTag)content).recursiveFindNodesByAttributeValue(inAttribute, inValue, inList);
1077            }
1078         }
1079      }
1080   }
1081
1082   //---------------------------------------------------------------------------
1083   /**
1084    Composes an xml start tag (ex: "&lt;inName att1='value1' att2='value2'>").
1085    If the isEmptyTag parameter is true, the output tag will end with "/>".
1086    ex: "&lt;inName att1='value1' att2='value2'/>". Attributes appear in
1087    alphabetical order for consistency.
1088    */
1089   private void writeStartTag(Writer inWriter, XMLNamespaceSet inDeclaredNamespaces, String inTagName)
1090      throws IOException
1091   {
1092      inWriter.write("<");
1093      inWriter.write(inTagName);
1094
1095
1096      // Write attributes.
1097      if (mAttributes != null
1098          && mAttributes.size() > 0)
1099      {
1100         List<XMLAttribute> sordidAttributes = new ArrayList<>(mAttributes.values());
1101         if (mSortAttributesBeforeWriting)
1102         {
1103            sortAttributes(sordidAttributes);
1104         }
1105
1106         for (XMLAttribute attribute : sordidAttributes)
1107         {
1108            inWriter.write(" ");
1109
1110            XMLNamespace namespace = attribute.getNamespace();
1111            if (namespace != null
1112                  && namespace.getPrefix() != null
1113                  && (null == inDeclaredNamespaces
1114                      || null == inDeclaredNamespaces.getDefault()
1115                      || ! namespace.equals(inDeclaredNamespaces.getDefault())))
1116            {
1117               inWriter.write(namespace.getPrefix());
1118               inWriter.write(":");
1119            }
1120            inWriter.write(attribute.getName());
1121
1122            String value = attribute.getValue();
1123            if (null == value) value = "";
1124            inWriter.write("=");
1125
1126            String safeValue = "''";
1127            if (value != null)
1128            {
1129               if (sQuoteChar == '\'')
1130               {
1131                  safeValue =  "'" + XMLUtil.escapeAttributeValue(value) + "'";
1132               }
1133               else
1134               {
1135                  safeValue = "\"" + XMLUtil.escapeDoubleQuotedAttributeValue(value) + "\"";
1136               }
1137            }
1138            inWriter.write(safeValue);
1139         }
1140      }
1141
1142      if (isEmptyTag()) inWriter.write(" /");
1143      inWriter.write(">");
1144   }
1145
1146   //---------------------------------------------------------------------------
1147   private XMLNamespaceSet updateDeclaredNamespaces(XMLNamespaceSet inDeclaredNamespaces)
1148   {
1149      XMLNamespaceSet expandedNamespaceSet = inDeclaredNamespaces;
1150      if (getAttributes() != null)
1151      {
1152         boolean cloned = false;
1153         for (XMLAttribute attr : getAttributes())
1154         {
1155            String qualifiedAttrName = attr.getQualifiedName();
1156            if (qualifiedAttrName.startsWith("xmlns"))
1157            {
1158               if (! cloned)
1159               {
1160                  expandedNamespaceSet = new XMLNamespaceSet((inDeclaredNamespaces != null ? inDeclaredNamespaces.size() : 0) + 5);
1161                  if (CollectionUtil.hasValues(inDeclaredNamespaces))
1162                  {
1163                     expandedNamespaceSet.addAll(inDeclaredNamespaces);
1164                  }
1165                  cloned = true;
1166               }
1167
1168               if (qualifiedAttrName.equals("xmlns"))
1169               {
1170                  expandedNamespaceSet.setDefault(XMLNamespace.getNamespace(attr.getValue()).clone());
1171               }
1172               else if (qualifiedAttrName.startsWith("xmlns:"))
1173               {
1174                  expandedNamespaceSet.add(XMLNamespace.getNamespace(qualifiedAttrName.substring(6), attr.getValue()).clone());
1175                  if (inDeclaredNamespaces != null)
1176                  {
1177                     // Propagate the previous default
1178                     expandedNamespaceSet.setDefault(inDeclaredNamespaces.getDefault());
1179                  }
1180               }
1181            }
1182         }
1183      }
1184
1185      return expandedNamespaceSet;
1186   }
1187
1188   //###########################################################################
1189   // INNER CLASS
1190   //###########################################################################
1191
1192   private class ProducerThread extends Thread
1193   {
1194      OutputStream mOutputStream;
1195
1196      //----------------------------------------------------------------------
1197      public ProducerThread(OutputStream inOutputStream)
1198      {
1199         mOutputStream = inOutputStream;
1200      }
1201
1202      //----------------------------------------------------------------------
1203      public void run()
1204      {
1205         toXML(mOutputStream);
1206         try
1207         {
1208            mOutputStream.close();
1209         }
1210         catch (IOException e)
1211         {
1212            e.printStackTrace();
1213         }
1214      }
1215
1216
1217   }
1218
1219}