All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hfg.xml.XMLContainerImpl Maven / Gradle / Ivy

There is a newer version: 20240423
Show newest version
package com.hfg.xml;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.hfg.exception.ProgrammingException;
import com.hfg.util.Case;
import com.hfg.util.CompareUtil;
import com.hfg.util.Recursion;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.io.GZIP;

//------------------------------------------------------------------------------
/**
 XML object that can contain sub-objects but not a name or attributes.

 @author J. Alex Taylor, hairyfatguy.com
 */
//------------------------------------------------------------------------------
// com.hfg XML/HTML Coding Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------

public abstract class XMLContainerImpl implements XMLContainer
{
   protected List mContentAndSubtagList; // Can contain XMLizable objects or Strings
   private   XMLContainer mParent;

   private static final boolean ESCAPE = true;
   private static final boolean NO_ESCAPE = false;
   private static final boolean WITH_EMBEDDED_SUBTAGS = true;
   private static final boolean WITHOUT_EMBEDDED_SUBTAGS = false;

   protected static enum  sMode { FIND, REMOVE };
   private static int   sCompressionThreshold = 1024;

   //---------------------------------------------------------------------------
   @Override
   public XMLTag clone()
   {
      XMLTag cloneObj;
      try
      {
         cloneObj = (XMLTag) super.clone();
      }
      catch (CloneNotSupportedException e)
      {
         throw new ProgrammingException(e);
      }

      if (mContentAndSubtagList != null)
      {
         cloneObj.mContentAndSubtagList = new ArrayList<>(mContentAndSubtagList.size());
         for (Object content : mContentAndSubtagList)
         {
            if (content instanceof XMLizable)
            {
               XMLizable subtagClone = ((XMLizable)content).clone();
               cloneObj.mContentAndSubtagList.add(subtagClone);
               if (subtagClone instanceof XMLContainer)
               {
                  ((XMLContainer)subtagClone).setParentNode(cloneObj);
               }
            }
            else
            {
               cloneObj.mContentAndSubtagList.add(content);
            }
         }
      }

      return cloneObj;
   }

   //---------------------------------------------------------------------------
   public boolean hasContent()
   {
      boolean returnValue = false;

      if (mContentAndSubtagList != null)
      {
         for (Object content : mContentAndSubtagList)
         {
            if (content instanceof String
               || content instanceof XMLCDATA
               || content instanceof byte[]) // Compressed content is stored as a byte[]
            {
               returnValue = true;
               break;
            }
         }
      }

      return returnValue;
   }

   //---------------------------------------------------------------------------
   public boolean hasCDATA()
   {
      boolean returnValue = false;

      if (mContentAndSubtagList != null)
      {
         for (Object content : mContentAndSubtagList)
         {
            if (content instanceof XMLCDATA)
            {
               returnValue = true;
               break;
            }
         }
      }

      return returnValue;
   }

   //---------------------------------------------------------------------------
   public boolean hasContentOrSubtags()
   {
      return (CollectionUtil.hasValues(mContentAndSubtagList));
   }

   //---------------------------------------------------------------------------
   public List getContentPlusSubtagList()
   {
      return mContentAndSubtagList;
   }


   //---------------------------------------------------------------------------
   public synchronized XMLContainer setContent(CharSequence inContent)
   {
      clearContent();

      addContent(inContent);

      return this;
   }

   //---------------------------------------------------------------------------
   public synchronized XMLContainer setContent(int inContent)
   {
      return setContent(inContent + "");
   }


   //---------------------------------------------------------------------------
   // Clears content (not subtags).
   public synchronized void clearContent()
   {
      if (mContentAndSubtagList != null)
      {
         for (int i = 0; i < mContentAndSubtagList.size(); i++)
         {
            Object content = mContentAndSubtagList.get(i);

            // Ignore subtags and remove compressed or uncompressed content.
            if (content instanceof String
               || content instanceof byte[])
            {
               mContentAndSubtagList.remove(i--);
            }
         }
      }
   }

   //---------------------------------------------------------------------------
   public XMLContainer addContent(CharSequence inContent)
   {
      addContent(inContent, ESCAPE);

      return this;
   }

   //---------------------------------------------------------------------------
   public XMLContainer addContentWithoutEscaping(CharSequence inContent)
   {
      addContent(inContent, NO_ESCAPE);

      return this;
   }

   //---------------------------------------------------------------------------
   public String getContent()
   {
      return getContent(WITHOUT_EMBEDDED_SUBTAGS);
   }

   //---------------------------------------------------------------------------
   /**
    The returned content also contains any embedded subtags.
    */
   private String getContentWithEmbeddedSubtags()
   {
      return getContent(WITH_EMBEDDED_SUBTAGS);
   }

   //----------------------------------------------------------------------------
   public String getUnescapedContent()
   {
      return XMLUtil.unescapeContent(getContent());
   }

   //---------------------------------------------------------------------------
   public synchronized void addSubtag(XMLizable inSubtag)
   {
      if (null == inSubtag)
      {
         throw new RuntimeException("The added xml subTag cannot be null");
      }

      if (null == mContentAndSubtagList) mContentAndSubtagList = new ArrayList(2);

      mContentAndSubtagList.add(inSubtag);

      if (inSubtag instanceof XMLContainer)
      {
         ((XMLContainer)inSubtag).setParentNode(this);
      }
   }

   //---------------------------------------------------------------------------
   /**
    Added the specified subtag at the specified position in the list of this tag's
    children.
    @param inIndex the index at which the specified subtag should be added
    @param inSubtag the subtag which should be added to this tag
    */
   public synchronized void addSubtag(int inIndex, XMLizable inSubtag)
   {
      if (null == inSubtag)
      {
         throw new RuntimeException("The added xml subTag cannot be null");
      }

      if (null == mContentAndSubtagList) mContentAndSubtagList = new ArrayList(2);

      if (inIndex > mContentAndSubtagList.size())  // Prevent trying to insert past the end
      {
         inIndex = mContentAndSubtagList.size();
      }

      mContentAndSubtagList.add(inIndex, inSubtag);

      if (inSubtag instanceof XMLContainer)
      {
         ((XMLContainer)inSubtag).setParentNode(this);
      }
   }

   //---------------------------------------------------------------------------
   public synchronized void addSubtags(Collection inSubtags)
   {
      if (null == inSubtags)
      {
         throw new RuntimeException("The added Collection of xml subTags cannot be null");
      }

      if (null == mContentAndSubtagList)
      {
         mContentAndSubtagList = new ArrayList<>(inSubtags.size());
      }

      for (XMLizable subtag : inSubtags)
      {
         addSubtag(subtag);
      }
   }

   //---------------------------------------------------------------------------
   public synchronized  void setSubtags(List inSubtags)
   {
      clearSubtags();
      if (CollectionUtil.hasValues(inSubtags))
      {
         addSubtags(inSubtags);
      }
   }

   //---------------------------------------------------------------------------
   public  List getSubtags()
   {
      List subtagList = new ArrayList(5);

      if (mContentAndSubtagList != null)
      {
         for (Object content : mContentAndSubtagList)
         {
            if (content instanceof XMLizable)
            {
               subtagList.add((T)content);
            }
         }
      }

      return subtagList;
   }

   //---------------------------------------------------------------------------
   public  List getXMLNodeSubtags()
   {
      List subtagList = new ArrayList<>(5);

      if (mContentAndSubtagList != null)
      {
         for (Object content : mContentAndSubtagList)
         {
            if (content instanceof XMLNode)
            {
               subtagList.add((T)content);
            }
         }
      }

      return subtagList;
   }

   //---------------------------------------------------------------------------
   /**
    Returns the subtag with the specified attribute name and value.
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @return the subtag which matched the attribute criteria
    @throws RuntimeException if multiple subtags match the specified attribute criteria
    */
   public  T getSubtagByAttribute(XMLName inAttributeName, String inAttributeValue)
   {
      return getSubtagByAttribute(inAttributeName != null ? inAttributeName.getLocalName() : null, inAttributeValue);
   }

   //---------------------------------------------------------------------------
   /**
    Returns the subtag with the specified attribute name and value.
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @return the subtag which matched the attribute criteria
    @throws RuntimeException if multiple subtags match the specified attribute criteria
    */
   public  T getSubtagByAttribute(String inAttributeName, String inAttributeValue)
   {
      T requestedSubtag = null;

      List subtags = getSubtagsByAttribute(inAttributeName, inAttributeValue);
      if (CollectionUtil.hasValues(subtags))
      {
         if (subtags.size() > 1)
         {
            throw new RuntimeException(subtags.size() + " subtags matching the requested attribute ["
                                       + inAttributeName + "=" + inAttributeValue + "] instead of the required 1!");
         }
         else
         {
            requestedSubtag = subtags.get(0);
         }
      }

      return requestedSubtag;
   }

   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags with the specified attribute name and value.
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @return the list of all subtags which matched the attribute criteria
    */
   public  List getSubtagsByAttribute(XMLName inAttributeName, String inAttributeValue)
   {
      return getSubtagsByAttribute(inAttributeName != null ? inAttributeName.getLocalName() : null, inAttributeValue);
   }

   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags with the specified attribute name and value.
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @return the list of all subtags which matched the attribute criteria
    */
   public  List getSubtagsByAttribute(@Nullable String inAttributeName, String inAttributeValue)
   {
      List subtagList = null;

      int index = inAttributeName != null ? inAttributeName.indexOf(":") : -1;
      if (index > 0)
      {
         subtagList = getSubtagsByAttribute(new XMLName(inAttributeName.substring(index + 1), XMLNamespace.getNamespaceViaPrefix(inAttributeName.substring(0, index))), inAttributeValue);   
      }
      else
      {
         List xmlNodes = getXMLNodeSubtags();
         if (CollectionUtil.hasValues(xmlNodes))
         {
            subtagList = new ArrayList<>(5);
            for (T node : xmlNodes)
            {
               if (inAttributeName != null)
               {

                  String attributeValue = node.getAttributeValue(inAttributeName);
                  if (0 == CompareUtil.compare(attributeValue, inAttributeValue))
                  {
                     subtagList.add(node);
                  }
               }
               else // Allow the attribute name to be null
               {
                  for (XMLAttribute attr : node.getAttributes())
                  {
                     if (0 == CompareUtil.compare(attr.getValue(), inAttributeValue))
                     {
                        subtagList.add(node);
                        break;
                     }
                  }
               }
            }
         }
      }

      return subtagList;
   }

   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags with the specified attribute name and value.
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValuePattern the Pattern to compare to attribute values via find(). Allows null.
    @return the list of all subtags which matched the attribute criteria
    */
   public  List getSubtagsByAttribute(String inAttributeName, Pattern inAttributeValuePattern)
   {
      List subtagList = null;

      List xmlNodes = getXMLNodeSubtags();
      if (CollectionUtil.hasValues(xmlNodes))
      {
         subtagList = new ArrayList<>(5);
         for (T node : xmlNodes)
         {
            if (inAttributeName != null)
            {
               if (node.hasAttribute(inAttributeName))
               {
                  String attributeValue = node.getAttributeValue(inAttributeName);
                  if (null == inAttributeValuePattern
                      || (attributeValue != null
                          && inAttributeValuePattern.matcher(attributeValue).find()))
                  {
                     subtagList.add(node);
                  }
               }
            }
            else // Allow the attribute name to be null
            {
               for (XMLAttribute attr : node.getAttributes())
               {
                  if (null == inAttributeValuePattern
                        || inAttributeValuePattern.matcher(attr.getValue()).find())
                  {
                     subtagList.add(node);
                     break;
                  }
               }
            }
         }
      }

      return subtagList;
   }

   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags with the specified attribute name and value
    (if recursion is off) or at any level below this object (if recursion is on).
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @param inRecursion flag to indicate whether or not recursion should be used
    @return the list of all subtags which matched the attribute criteria
    */
   public  List getSubtagsByAttribute(String inAttributeName, String inAttributeValue, Recursion inRecursion)
   {
      List outList = new ArrayList<>(5);

      if (inRecursion == Recursion.ON)
      {
         Pattern attributeValuePattern = null;
         if (inAttributeValue != null)
         {
            attributeValuePattern = Pattern.compile("^" + Pattern.quote(inAttributeValue) + "$");
         }

         recursivelyGetSubtagsByAttribute(inAttributeName, attributeValuePattern, this, outList, sMode.FIND); // Treat the attribute value as a literal pattern
      }
      else
      {
         outList = getSubtagsByAttribute(inAttributeName, inAttributeValue);
         for (XMLNode subtag : outList)
         {
            removeSubtag(subtag);
         }
      }

      return outList;
   }

   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags with the specified attribute name and value.
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValuePattern the Pattern to compare to attribute values via find(). Allows null.
    @param inRecursion flag to indicate whether or not recursion should be used
    @return the list of all subtags which matched the attribute criteria
    */
   public  List getSubtagsByAttribute(String inAttributeName, Pattern inAttributeValuePattern, Recursion inRecursion)
   {
      List outList = new ArrayList<>(5);

      if (inRecursion == Recursion.ON)
      {
         recursivelyGetSubtagsByAttribute(inAttributeName, inAttributeValuePattern, this, outList, sMode.FIND);
      }
      else
      {
         outList = getSubtagsByAttribute(inAttributeName, inAttributeValuePattern);
      }

      return outList;
   }


   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags of the specified Java Class on this XMLTag object
    The same as getSubtagsByClass(inClassName, Recursion.OFF)
    */
   public  List getSubtagsByClass(Class inClassType)
   {
      return getSubtagsByClass(inClassType, Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   /**
    Returns a List of all subtags of the specified Java Class on this XMLTag object
    (if recursion is off) or at any level below this object (if recursion is on).
    */
   public  List getSubtagsByClass(Class inClassType, Recursion inRecursion)
   {
      List outList = new ArrayList<>(10);

      if (inClassType != null)
      {
         if (inRecursion == Recursion.ON)
         {
            recursivelyGetSubtagsByClass(inClassType, this, outList, sMode.FIND);
         }
         else
         {
            if (mContentAndSubtagList != null)
            {
               for (Object content : mContentAndSubtagList)
               {
                  if (inClassType.isInstance(content))
                  {
                     outList.add((T)content);
                  }
               }
            }
         }
      }

      return outList;
   }


   //---------------------------------------------------------------------------
   public synchronized void removeSubtag(XMLizable inSubtag)
   {
      if (mContentAndSubtagList != null)
      {
         // Remove all instances of the subtag in the list.
         while (mContentAndSubtagList.remove(inSubtag))
         {
         }
      }
   }

   //---------------------------------------------------------------------------
   public synchronized void removeSubtag(int inIndex)
   {
      if (mContentAndSubtagList != null)
      {
         mContentAndSubtagList.remove(inIndex);
      }
   }

   //---------------------------------------------------------------------------
   /**
    Removes subtags of the specified Java Class on this object
    The same as removeSubtagsByClass(inClassName, Recursion.OFF)
    @return the List of removed objects
    */
   public  List removeSubtagsByClass(Class inClassType)
   {
      return removeSubtagsByClass(inClassType, Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   /**
    Removes subtags of the specified Java Class on this XMLTag object
    (if recursion is off) or at any level below this object (if recursion is on).
    @return the List of removed objects
    */
   public  List removeSubtagsByClass(Class inClassType, Recursion inRecursion)
   {
      List outList = new ArrayList<>(10);

      if (inClassType != null)
      {
         if (inRecursion == Recursion.ON)
         {
            recursivelyGetSubtagsByClass(inClassType, this, outList, sMode.REMOVE);
         }
         else
         {
            outList = getSubtagsByClass(inClassType);
            for (T subtag : outList)
            {
               removeSubtag(subtag);
            }
         }
      }

      return outList;
   }

   //---------------------------------------------------------------------------
   public synchronized void clearSubtags()
   {
      if (mContentAndSubtagList != null)
      {
         for (int i = 0; i < mContentAndSubtagList.size(); i++)
         {
            Object content = mContentAndSubtagList.get(i);

            if (content instanceof XMLizable) // Ignore. Content or comments
            {
               mContentAndSubtagList.remove(i--);
            }
         }
      }
   }

   //---------------------------------------------------------------------------
   public synchronized int indexOf(XMLizable inSubtag)
   {
      int index = -1;
      if (mContentAndSubtagList != null)
      {
         for (int i = 0; i < mContentAndSubtagList.size(); i++)
         {
            Object content = mContentAndSubtagList.get(i);
            if (content.equals(inSubtag))
            {
               index = i;
               break;
            }
         }
      }

      return index;
   }


   //---------------------------------------------------------------------------
   public int getTotalTagCount()
   {
      int count = 1;

      if (mContentAndSubtagList != null)
      {
         for (Object content : mContentAndSubtagList)
         {
            try
            {
               XMLNode subtag = (XMLNode) content;
               count += subtag.getTotalTagCount();
            }
            catch (ClassCastException e)
            {
               // Ignore. Content or Comment.
            }
         }
      }

      return count;
   }


   //---------------------------------------------------------------------------
   public int countSubtagsByName(XMLName inTagName)
   {
      return countSubtagsByName(inTagName.getLocalName(), Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   public int countSubtagsByName(String inTagName)
   {
      return countSubtagsByName(inTagName, Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   public int countSubtagsByName(String inTagName, Recursion inRecursion)
   {
      int subtagCount = 0;

      if (inTagName != null)
      {
         if (inRecursion == Recursion.ON)
         {
            subtagCount = recursivelyCountSubtagsByName(inTagName, this);
         }
         else
         {
            if (mContentAndSubtagList != null)
            {
               for (Object content : mContentAndSubtagList)
               {
                  if (content instanceof XMLNode)
                  {
                     String tagName = ((XMLNode) content).getTagName();
                     if (tagName != null
                         && tagName.equals(inTagName))
                     {
                        subtagCount++;
                     }
                  }
               }
            }
         }
      }

      return subtagCount;
   }

   //---------------------------------------------------------------------------
   public  T getRequiredSubtagByName(XMLName inTagName)
   {
      return getRequiredSubtagByName(inTagName.getLocalName());
   }

   //---------------------------------------------------------------------------
   public  T getRequiredSubtagByName(String inTagName)
   {
      List subtags = getSubtagsByName(inTagName);
      if (!CollectionUtil.hasValues(subtags))
      {
         throw new XMLException("The required " + inTagName + " subtag was not found!");
      }
      else if (subtags.size() > 1)
      {
         throw new XMLException(subtags.size() + " " + inTagName + " subtags found!");
      }

      return subtags.get(0);
   }

   //---------------------------------------------------------------------------
   public  T getRequiredSubtagById(String inId)
   {
      List subtags = getSubtagsByAttribute("id", inId);
      if (! CollectionUtil.hasValues(subtags))
      {
         throw new XMLException("Subtag with the required id '" + inId + "' subtag was not found!");
      }
      else if (subtags.size() > 1)
      {
         throw new XMLException(subtags.size() + " " + "subtags found with id '" + inId + "'!");
      }

      return subtags.get(0);
   }

   //---------------------------------------------------------------------------
   public  T getRequiredSubtagById(String inId, Recursion inRecursion)
   {
      List subtags = getSubtagsByAttribute("id", inId, inRecursion);
      if (! CollectionUtil.hasValues(subtags))
      {
         throw new XMLException("Subtag with the required id '" + inId + "' subtag was not found!");
      }
      else if (subtags.size() > 1)
      {
         throw new XMLException(subtags.size() + " " + "subtags found with id '" + inId + "'!");
      }

      return subtags.get(0);
   }

   //---------------------------------------------------------------------------
   public  T getOptionalSubtagByName(XMLName inTagName)
   {
      return getOptionalSubtagByName(inTagName.getLocalName(), Case.SENSITIVE);
   }

   //---------------------------------------------------------------------------
   public  T getOptionalSubtagByName(XMLName inTagName, Case inCaseSensitivity)
   {
      return getOptionalSubtagByName(inTagName.getLocalName(), inCaseSensitivity);
   }

   //---------------------------------------------------------------------------
   public  T getOptionalSubtagByName(String inTagName)
   {
      return getOptionalSubtagByName(inTagName, Case.SENSITIVE);
   }

   //---------------------------------------------------------------------------
   public  T getOptionalSubtagByName(String inTagName, Case inCaseSensitivity)
   {
      List subtags = getSubtagsByName(inTagName, inCaseSensitivity, Recursion.OFF);
      if (subtags.size() > 1)
      {
         throw new XMLException(subtags.size() + " " + inTagName + " subtags found!");
      }

      return subtags.size() == 1 ? subtags.get(0) : null;
   }

   //---------------------------------------------------------------------------
   public  List getSubtagsByName(XMLName inTagName)
   {
      return getSubtagsByName(inTagName.getLocalName(), Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   public  List getSubtagsByName(String inTagName)
   {
      return getSubtagsByName(inTagName, Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   public  List getSubtagsByName(XMLName inTagName, Recursion inRecursion)
   {
      return getSubtagsByName(inTagName.getLocalName(), inRecursion);
   }

   //---------------------------------------------------------------------------
   public  List getSubtagsByName(String inTagName, Recursion inRecursion)
   {
      return getSubtagsByName(inTagName, Case.SENSITIVE, inRecursion);
   }

   //---------------------------------------------------------------------------
   public  List getSubtagsByName(String inTagName, Case inCaseSensitivity, Recursion inRecursion)
   {
      List outList = new ArrayList<>(10);

      if (inTagName != null)
      {
         if (inRecursion == Recursion.ON)
         {
            recursivelyGetSubtagsByName(inTagName, this, outList, sMode.FIND);
         }
         else
         {
            if (mContentAndSubtagList != null)
            {
               for (Object content : mContentAndSubtagList)
               {
                  if (content instanceof XMLNode)
                  {
                     String tagName = ((XMLNode) content).getTagName();
                     if (tagName != null
                         && (inCaseSensitivity.equals(Case.SENSITIVE) ? tagName.equals(inTagName) :
                                                                        tagName.equalsIgnoreCase(inTagName)))
                     {
                        outList.add((T) content);
                     }
                  }
               }
            }
         }
      }

      return outList;
   }

   //---------------------------------------------------------------------------
   public synchronized  List removeSubtagsByName(XMLName inTagName)
   {
      return removeSubtagsByName(inTagName.getLocalName());
   }

   //---------------------------------------------------------------------------
   public synchronized  List removeSubtagsByName(String inTagName)
   {
      return removeSubtagsByName(inTagName, Recursion.OFF);
   }

   //---------------------------------------------------------------------------
   /**
    Removes all subtags with the specified tag name from this XMLTag object
    (if recursion is off) or at any level below this object (if recursion is on).
    @param inTagName the name of the subtags to remove
    @param inRecursion flag to indicate whether or not recursion should be used
    @return the list of removed subtags
    */
   public synchronized  List removeSubtagsByName(String inTagName, Recursion inRecursion)
   {
      List outList = new ArrayList<>(10);
      if (inTagName != null)
      {
         if (inRecursion == Recursion.ON)
         {
            recursivelyGetSubtagsByName(inTagName, this, outList, sMode.REMOVE);
         }
         else
         {
            outList = getSubtagsByName(inTagName);
            for (XMLNode subtag : outList)
            {
               removeSubtag(subtag);
            }
         }
      }

      return outList;
   }

   //---------------------------------------------------------------------------
   /**
    Removes all subtags with the specified tag name from this XMLTag object
    (if recursion is off) or at any level below this object (if recursion is on).
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @param inRecursion flag to indicate whether or not recursion should be used
    @return the list of removed subtags
    */
   public synchronized  List removeSubtagsByAttribute(XMLName inAttributeName, String inAttributeValue, Recursion inRecursion)
   {
      return removeSubtagsByAttribute(inAttributeName != null ? inAttributeName.getLocalName() : null, inAttributeValue, inRecursion);
   }

   //---------------------------------------------------------------------------
   /**
    Removes all subtags with the specified tag name from this XMLTag object
    (if recursion is off) or at any level below this object (if recursion is on).
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValue the attribute value to match on
    @param inRecursion flag to indicate whether or not recursion should be used
    @return the list of removed subtags
    */
   public synchronized  List removeSubtagsByAttribute(String inAttributeName, String inAttributeValue, Recursion inRecursion)
   {
      List outList = new ArrayList<>(10);

      if (inRecursion == Recursion.ON)
      {
         Pattern attributeValuePattern = null;
         if (inAttributeValue != null)
         {
            attributeValuePattern = Pattern.compile(Pattern.quote(inAttributeValue)); // Treat the attribute value as a literal pattern
         }

         recursivelyGetSubtagsByAttribute(inAttributeName, attributeValuePattern, this, outList, sMode.REMOVE);
      }
      else
      {
         outList = getSubtagsByAttribute(inAttributeName, inAttributeValue);
         for (XMLNode subtag : outList)
         {
            removeSubtag(subtag);
         }
      }

      return outList;
   }

   //---------------------------------------------------------------------------
   /**
    Removes all subtags with the specified tag name from this XMLTag object
    (if recursion is off) or at any level below this object (if recursion is on).
    @param inAttributeName the attribute name to match on. Allows null.
    @param inAttributeValuePattern the attribute value pattern to match with
    @param inRecursion flag to indicate whether or not recursion should be used
    @return the list of removed subtags
    */
   public synchronized  List removeSubtagsByAttribute(String inAttributeName, Pattern inAttributeValuePattern, Recursion inRecursion)
   {
      List outList = new ArrayList<>(10);

      if (inRecursion == Recursion.ON)
      {
         recursivelyGetSubtagsByAttribute(inAttributeName, inAttributeValuePattern, this, outList, sMode.REMOVE);
      }
      else
      {
         outList = getSubtagsByAttribute(inAttributeName, inAttributeValuePattern);
         for (XMLNode subtag : outList)
         {
            removeSubtag(subtag);
         }
      }

      return outList;
   }


   //---------------------------------------------------------------------------
   public void setParentNode(XMLContainer inParent)
   {
      mParent = inParent;
   }

   //---------------------------------------------------------------------------
   public XMLContainer getParentNode()
   {
      return mParent;
   }

   //---------------------------------------------------------------------------
   public XMLContainer getPreviousSibling()
   {
      XMLContainer requestedSibling = null;
      if (getParentNode() != null)
      {
         List siblings = getParentNode().getSubtags();
         int index = siblings.indexOf(this) - 1;
         while (index >= 0)
         {
            XMLizable sibling = siblings.get(index);
            if (sibling instanceof XMLContainer)
            {
               requestedSibling = (XMLContainer) sibling;
               break;
            }
            index--;
         }
      }

      return requestedSibling;
   }

   //---------------------------------------------------------------------------
   public XMLContainer getNextSibling()
   {
      XMLContainer requestedSibling = null;
      if (getParentNode() != null)
      {
         List siblings = getParentNode().getSubtags();
         int index = siblings.indexOf(this) + 1;
         while (index < siblings.size())
         {
            XMLizable sibling = siblings.get(index);
            if (sibling instanceof XMLContainer)
            {
               requestedSibling = (XMLContainer) sibling;
               break;
            }

            index++;
         }
      }

      return requestedSibling;
   }

   //###########################################################################
   // PROTECTED METHODS
   //###########################################################################

   //---------------------------------------------------------------------------
   protected String innerHTML()
   {
      StringBuilderPlus html = new StringBuilderPlus();

      if (mContentAndSubtagList != null)
      {
         for (Object content : mContentAndSubtagList)
         {
            if (content instanceof String)
            {
               html.append(content.toString());
            }
            else if (content instanceof byte[])
            {
               html.append(GZIP.uncompressToString((byte[]) content));
            }
            else
            {
               XMLizable subtag = (XMLizable) content;
               html.append(subtag.toXML());
            }
         }
      }

      return html.toString();
   }

   //###########################################################################
   // PRIVATE METHODS
   //###########################################################################

   //--------------------------------------------------------------------------
   /**
    Adds content with or without escaping. Large content is compressed to save space.
    */
   private synchronized void addContent(CharSequence inContent, boolean escape)
   {
      if (inContent != null) // Allow for setting "" as a way to avoid empty tag syntax if necessary.
      {
         if (null == mContentAndSubtagList)
         {
            mContentAndSubtagList = new ArrayList<>(2);
         }


         String content = inContent.toString();
         if (escape)
         {
            content = XMLUtil.escapeContentIfNecessary(inContent.toString());
         }

         mContentAndSubtagList.add(content.length() > sCompressionThreshold ? GZIP.compress(content) : content);
      }
   }


   //---------------------------------------------------------------------------
   private String getContent(boolean includeEmbeddedSubtags)
   {
      if (null == mContentAndSubtagList) return "";

      StringBuilder outContent = new StringBuilder();
      for (Object content : mContentAndSubtagList)
      {
         if (content instanceof String)
         {
            outContent.append(content);
         }
         else if (content instanceof byte[])
         {
            outContent.append(GZIP.uncompressToString((byte[]) content));
         }
         else if (content instanceof XMLCDATA)
         {
            outContent.append(((XMLCDATA) content).getContent());
         }
         else if (content instanceof XMLizable)
         {
            if (includeEmbeddedSubtags)
            {
               XMLizable subTag = (XMLizable) content;
               outContent.append(subTag.toXML());
            }
         }
         else
         {
            throw new XMLException("Unexpected content type: '" + content + "'.");
         }
      }

      return outContent.toString();
   }

   //---------------------------------------------------------------------------
   private static  void recursivelyGetSubtagsByName(String inTagName, XMLContainer inRootTag, List inList, sMode inMode)
   {
      List subtags = inRootTag.getSubtags();
      if (CollectionUtil.hasValues(subtags))
      {
         for (int i = 0; i < subtags.size(); i++)
         {
            XMLizable subtag = subtags.get(i);
            if (subtag instanceof XMLContainer)
            {
               if (subtag instanceof XMLNode)
               {
                  if (((XMLNode) subtag).getTagName().equals(inTagName))
                  {
                     inList.add((T) subtag);
                     if (inMode == sMode.REMOVE)
                     {
                        inRootTag.removeSubtag(subtag);
                        subtags.remove(i);
                        i--;
                     }
                  }
               }

               recursivelyGetSubtagsByName(inTagName, (XMLContainer) subtag, inList, inMode);
            }
         }
      }
   }

   //---------------------------------------------------------------------------
   private static int recursivelyCountSubtagsByName(String inTagName, XMLContainer inRootTag)
   {
      int count = 0;

      List subtags = inRootTag.getSubtags();
      if (CollectionUtil.hasValues(subtags))
      {
         for (int i = 0; i < subtags.size(); i++)
         {
            XMLizable subtag = subtags.get(i);
            if (subtag instanceof XMLContainer)
            {
               if (subtag instanceof XMLNode)
               {
                  if (((XMLNode) subtag).getTagName().equals(inTagName))
                  {
                     count++;
                  }
               }

               count += recursivelyCountSubtagsByName(inTagName, (XMLContainer) subtag);
            }
         }
      }

      return count;
   }

   //---------------------------------------------------------------------------
      private static  void recursivelyGetSubtagsByAttribute(String inAttributeName, Pattern inAttributeValuePattern, XMLContainerImpl inRootTag, List inList, sMode inMode)
   {
      List subtags = inRootTag.getSubtags();
      if (CollectionUtil.hasValues(subtags))
      {
         for (int i = 0; i < subtags.size(); i++)
         {
            XMLizable subtag = subtags.get(i);
            if (! (subtag instanceof XMLContainerImpl))
            {
               // Skip comments and CDATA
               continue;
            }

            if (subtag instanceof XMLNode)
            {
               if (inAttributeName != null)
               {
                  String attributeValue = ((XMLNode) subtag).getAttributeValue(inAttributeName);
                  if (null == inAttributeValuePattern
                      || (attributeValue != null
                        && inAttributeValuePattern.matcher(attributeValue).find()))
                  {
                     inList.add((T)subtag);
                     if (inMode == sMode.REMOVE)
                     {
                        inRootTag.removeSubtag(subtag);
                        subtags.remove(i);
                        i--;
                     }
                  }
               }
               else // Allow the attribute name to be null
               {
                  for (XMLAttribute attr : ((XMLTag)subtag).getAttributes())
                  {
                     if (null == inAttributeValuePattern
                           || (attr.getValue() != null
                           && inAttributeValuePattern.matcher(attr.getValue()).find()))
                     {
                        inList.add((T)subtag);
                        if (inMode == sMode.REMOVE)
                        {
                           inRootTag.removeSubtag(subtag);
                           subtags.remove(i);
                           i--;
                        }
                     }
                  }
               }
            }

            recursivelyGetSubtagsByAttribute(inAttributeName, inAttributeValuePattern, (XMLContainerImpl) subtag, inList, inMode);
         }
      }
   }

   //---------------------------------------------------------------------------
   private static  void recursivelyGetSubtagsByClass(Class inClassType, XMLContainerImpl inRootTag, List inList, sMode inMode)
   {
      List subtags = inRootTag.getSubtags();
      if (CollectionUtil.hasValues(subtags))
      {
         for (int i = 0; i < subtags.size(); i++)
         {
            XMLContainerImpl subtag = subtags.get(i);

            if (inClassType.isInstance(subtag))
            {
               inList.add((T)subtag);

               if (inMode == sMode.REMOVE)
               {
                  inRootTag.removeSubtag(subtag);
                  subtags.remove(i);
                  i--;
               }
            }

            recursivelyGetSubtagsByClass(inClassType, subtag, inList, inMode);
         }
      }
   }

}