OdfElement.java

/**
 * **********************************************************************
 *
 * <p>DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * <p>Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * <p>Use is subject to license terms.
 *
 * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0. You can also obtain a copy of the License at
 * http://odftoolkit.org/docs/license.txt
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied.
 *
 * <p>See the License for the specific language governing permissions and limitations under the
 * License.
 *
 * <p>**********************************************************************
 */
package org.odftoolkit.odfdom.pkg;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.xerces.dom.ElementNSImpl;
import org.apache.xerces.dom.NodeImpl;
import org.apache.xerces.dom.ParentNode;
import org.json.JSONObject;
import org.odftoolkit.odfdom.changes.Component;
import org.odftoolkit.odfdom.changes.JsonOperationConsumer;
import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument;
import org.odftoolkit.odfdom.dom.OdfContentDom;
import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
import org.odftoolkit.odfdom.dom.OdfStylesDom;
import org.odftoolkit.odfdom.dom.element.OdfStylePropertiesBase;
import org.odftoolkit.odfdom.dom.element.office.OfficeAnnotationElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeBodyElement;
import org.odftoolkit.odfdom.dom.element.style.StyleHandoutMasterElement;
import org.odftoolkit.odfdom.dom.element.style.StyleStyleElement;
import org.odftoolkit.odfdom.dom.element.style.StyleTextPropertiesElement;
import org.odftoolkit.odfdom.dom.element.table.TableContentValidationsElement;
import org.odftoolkit.odfdom.dom.element.table.TableCoveredTableCellElement;
import org.odftoolkit.odfdom.dom.element.table.TableShapesElement;
import org.odftoolkit.odfdom.dom.element.text.TextAElement;
import org.odftoolkit.odfdom.dom.element.text.TextHElement;
import org.odftoolkit.odfdom.dom.element.text.TextNoteElement;
import org.odftoolkit.odfdom.dom.element.text.TextPElement;
import org.odftoolkit.odfdom.dom.element.text.TextParagraphElementBase;
import org.odftoolkit.odfdom.dom.element.text.TextSElement;
import org.odftoolkit.odfdom.dom.element.text.TextSpanElement;
import org.odftoolkit.odfdom.dom.element.text.TextTrackedChangesElement;
import org.odftoolkit.odfdom.dom.style.OdfStyleFamily;
import org.odftoolkit.odfdom.dom.style.props.OdfStylePropertiesSet;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeAutomaticStyles;
import org.odftoolkit.odfdom.incubator.doc.style.OdfStyle;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

public abstract class OdfElement extends ElementNSImpl {

  private static final long serialVersionUID = -4939293285696678939L;
  private boolean isComponentRoot = false;
  private boolean mIsIgnoredComponent = false;
  private Component mComponent = null;
  // ToDo: Only on component roots
  private int mComponentSize = 0;

  /** Creates a new instance of OdfElement */
  public OdfElement(OdfFileDom ownerDocument, String namespaceURI, String qualifiedName)
      throws DOMException {
    super(ownerDocument, namespaceURI, qualifiedName);
  }

  /** Creates a new instance of OdfElement */
  public OdfElement(OdfFileDom ownerDocument, OdfName aName) throws DOMException {
    super(ownerDocument, aName.getUri(), aName.getQName());
  }

  public abstract OdfName getOdfName();

  protected <T extends OdfElement> T getParentAs(Class<T> clazz) {
    Node parent = getParentNode();
    if (parent != null && clazz.isInstance(parent)) {
      return clazz.cast(parent);
    } else {
      return null;
    }
  }

  protected <T extends OdfElement> T getAncestorAs(Class<T> clazz) {
    Node node = getParentNode();
    while (node != null) {
      if (clazz.isInstance(node)) {
        return clazz.cast(node);
      }
      node = node.getParentNode();
    }
    return null;
  }

  /** @returns true if the given potentialParent is an ancestor of this element */
  public boolean hasAncestor(Node potentialParent) {
    Node parentNode = this.getParentNode();
    boolean isParent = false;
    do {
      if (parentNode == null) {
        break;
      } else if (parentNode.equals(potentialParent)) {
        isParent = true;
        break;
      }
    } while ((parentNode = parentNode.getParentNode()) != null);
    return isParent;
  }

  @Override
  public String toString() {
    return mapNode(this, new StringBuilder()).toString();
  }

  /** Only Siblings will be traversed by this method as Children */
  private static StringBuilder mapNodeTree(Node node, StringBuilder xml) {
    while (node != null) {
      // mapping node and this mapping include always all descendants
      xml = mapNode(node, xml);
      // next sibling will be mapped to XML
      node = node.getNextSibling();
    }
    return xml;
  }

  private static StringBuilder mapNode(Node node, StringBuilder xml) {
    if (node instanceof Element) {
      xml = mapElementNode(node, xml);
    } else if (node instanceof Text) {
      xml = mapTextNode(node, xml);
    }
    return xml;
  }

  private static StringBuilder mapTextNode(Node node, StringBuilder xml) {
    if (node != null) {
      xml = xml.append(node.getTextContent());
    }
    return xml;
  }

  private static StringBuilder mapElementNode(Node node, StringBuilder xml) {
    if (node != null) {
      xml = xml.append("<");
      xml = xml.append(node.getNodeName());
      xml = mapAttributeNode(node, xml);
      xml = xml.append(">");
      xml = mapNodeTree(node.getFirstChild(), xml);
      xml = xml.append("</");
      xml = xml.append(node.getNodeName());
      xml = xml.append(">");
    }
    return xml;
  }

  private static StringBuilder mapAttributeNode(Node node, StringBuilder xml) {
    NamedNodeMap attrs = null;
    int length;
    if ((attrs = node.getAttributes()) != null && (length = attrs.getLength()) > 0) {
      for (int i = 0; length > i; i++) {
        xml = xml.append(" ");
        xml = xml.append(attrs.item(i).getNodeName());
        xml = xml.append("=\"");
        xml = xml.append(attrs.item(i).getNodeValue());
        xml = xml.append("\"");
      }
    }
    return xml;
  }

  /**
   * Set the value of an ODF attribute by <code>OdfName</code>.
   *
   * @param name The qualified name of the ODF attribute.
   * @param value The value to be set in <code>String</code> form
   */
  public void setOdfAttributeValue(OdfName name, String value) {
    setAttributeNS(name.getUri(), name.getQName(), value);
  }

  /**
   * Set an ODF attribute to this element
   *
   * @param attribute the attribute to be set
   */
  public void setOdfAttribute(OdfAttribute attribute) {
    setAttributeNodeNS(attribute);
  }

  /**
   * Retrieves a value of an ODF attribute by <code>OdfName</code>.
   *
   * @param name The qualified name of the ODF attribute.
   * @return The value of the attribute as <code>String</code> or <code>null</code> if the attribute
   *     does not exist.
   */
  public String getOdfAttributeValue(OdfName name) {
    return getAttributeNS(name.getUri(), name.getLocalName());
  }

  /**
   * Retrieves an ODF attribute by <code>OdfName</code>.
   *
   * @param name The qualified name of the ODF attribute.
   * @return The <code>OdfAttribute</code> or <code>null</code> if the attribute does not exist.
   */
  public OdfAttribute getOdfAttribute(OdfName name) {
    return (OdfAttribute) getAttributeNodeNS(name.getUri(), name.getLocalName());
  }

  /**
   * Retrieves an ODF attribute by <code>NamespaceName</code>, and local name.
   *
   * @param namespace The namespace of the ODF attribute.
   * @param localname The local name of the ODF attribute.
   * @return The <code>OdfAttribute</code> or <code>null</code> if the attribute does not exist.
   */
  public OdfAttribute getOdfAttribute(NamespaceName namespace, String localname) {
    return (OdfAttribute) getAttributeNodeNS(namespace.getUri(), localname);
  }

  /**
   * Determines if an ODF attribute exists.
   *
   * @param name The qualified name of the ODF attribute.
   * @return True if the attribute exists.
   */
  public boolean hasOdfAttribute(OdfName name) {
    return hasAttributeNS(name.getUri(), name.getLocalName());
  }

  /**
   * returns the first child node that implements the given class.
   *
   * @param <T> The type of the ODF element to be found.
   * @param clazz is a class that extends OdfElement.
   * @param parentNode is the parent O of the children to be found.
   * @return the first child node of the given parentNode that is a clazz or null if none is found.
   */
  @SuppressWarnings("unchecked")
  public static <T extends OdfElement> T findFirstChildNode(Class<T> clazz, Node parentNode) {
    if (parentNode != null && parentNode instanceof ParentNode) {
      Node node = ((ParentNode) parentNode).getFirstChild();
      while ((node != null) && !clazz.isInstance(node)) {
        node = node.getNextSibling();
      }

      if (node != null) {
        return (T) node;
      }
    }

    return null;
  }

  /**
   * returns the first sibling after the given reference node that implements the given class.
   *
   * @param <T> The type of the ODF element to be found.
   * @param clazz is a class that extends OdfElement.
   * @param refNode the reference node of the siblings to be found.
   * @return the first sibling of the given reference node that is a class or null if none is found.
   */
  @SuppressWarnings("unchecked")
  public static <T extends OdfElement> T findNextChildNode(Class<T> clazz, Node refNode) {
    if (refNode != null) {
      Node node = refNode.getNextSibling();
      while (node != null && !clazz.isInstance(node)) {
        node = node.getNextSibling();
      }

      if (node != null) {
        return (T) node;
      }
    }

    return null;
  }

  /**
   * returns the first previous sibling before the given reference node that implements the given
   * class.
   *
   * @param clazz is a class that extends OdfElement.
   * @param refNode the reference node which siblings are to be searched.
   * @return the first previous sibling of the given reference node that is a class or null if none
   *     is found.
   */
  @SuppressWarnings("unchecked")
  public static <T extends OdfElement> T findPreviousChildNode(Class<T> clazz, Node refNode) {
    if (refNode != null) {
      Node node = refNode.getPreviousSibling();
      while (node != null && !clazz.isInstance(node)) {
        node = node.getPreviousSibling();
      }

      if (node != null) {
        return (T) node;
      }
    }

    return null;
  }

  /**
   * Clones this complete element with all descendants.
   *
   * @return the cloned element
   */
  public OdfElement cloneElement() {
    OdfElement cloneElement = ((OdfFileDom) this.ownerDocument).newOdfElement(this.getClass());
    // if it is an unknown ODF element
    if (cloneElement == null) {
      cloneElement =
          new OdfAlienElement(
              (OdfFileDom) this.getOwnerDocument(),
              OdfName.newName(
                  OdfNamespace.getNamespace(this.getNamespaceURI()), this.getTagName()));
    }

    if (attributes != null) {
      for (int i = 0; i < attributes.getLength(); i++) {
        Node item = attributes.item(i);
        String qname = null;
        String prefix = item.getPrefix();
        if (prefix == null) {
          qname = item.getLocalName();
          cloneElement.setAttribute(qname, item.getNodeValue());
        } else {
          qname = prefix + ":" + item.getLocalName();
          cloneElement.setAttributeNS(item.getNamespaceURI(), qname, item.getNodeValue());
        }
      }
    }

    // aside of the XML the flag of being a component root have to be copied
    if (this.isComponentRoot) {
      cloneElement.markAsComponentRoot(true);
      cloneElement.mComponentSize = this.mComponentSize;
      cloneElement.mIsIgnoredComponent = this.mIsIgnoredComponent;
      if (!this.mIsIgnoredComponent) {
        Component.createComponent(this.getComponent().getParent(), cloneElement);
      }
    }

    Node childNode = getFirstChild();
    while (childNode != null) {
      cloneElement.appendChild(childNode.cloneNode(true));
      childNode = childNode.getNextSibling();
    }
    // ToDo: There should be an easier - more obvious - way than this...
    if (this.selfAndDescendantTextIgnoredAsComponent()) {
      cloneElement.ignoredComponent(true);
    }
    cloneElement.mComponentSize = this.mComponentSize;
    cloneElement.mIsIgnoredComponent = this.mIsIgnoredComponent;

    return cloneElement;
  }

  /** Overwritten by AlienElement class, which represents XML elements of various names */
  protected OdfElement cloneOdfElement() {
    return ((OdfFileDom) this.ownerDocument).newOdfElement(this.getClass());
  }

  @Override
  /**
   * Clones this element but without cloning xml:id (and office:value) attributes
   *
   * @param deep if a deep copy should happen. If False, only the given element with attributes and
   *     no content is copied
   */
  public Node cloneNode(boolean deep) {
    OdfElement cloneElement = this.cloneOdfElement();

    if (attributes != null) {
      for (int i = 0; i < attributes.getLength(); i++) {
        Node item = attributes.item(i);
        String qname = null;
        String prefix = item.getPrefix();
        if (prefix == null) {
          qname = item.getLocalName();
          cloneElement.setAttribute(qname, item.getNodeValue());
        } else {
          qname = prefix + ":" + item.getLocalName();
          if (!qname.equals("xml:id")
              && !qname.equals("office:value")
              && !qname.equals("calcext:value-type")
              && !qname.equals("office:value-type")) {
            cloneElement.setAttributeNS(item.getNamespaceURI(), qname, item.getNodeValue());
          }
        }
      }
    }

    // aside of the XML the flag of being a component root have to be copied
    if (this.isComponentRoot) {
      cloneElement.markAsComponentRoot(true);
      cloneElement.mComponentSize = this.mComponentSize;
      cloneElement.mIsIgnoredComponent = this.mIsIgnoredComponent;
      if (!this.mIsIgnoredComponent) {
        Component.createComponent(this.getComponent().getParent(), cloneElement);
      }
    }

    if (deep) {
      Node childNode = getFirstChild();
      while (childNode != null) {
        cloneElement.appendChild(childNode.cloneNode(true));
        childNode = childNode.getNextSibling();
      }
    }
    // ToDo: There should be an easier - more obvious - way than this...
    if (this.selfAndDescendantTextIgnoredAsComponent()) {
      cloneElement.ignoredComponent(true);
    }
    cloneElement.mComponentSize = this.mComponentSize;
    cloneElement.mIsIgnoredComponent = this.mIsIgnoredComponent;

    return cloneElement;
  }

  /**
   * @param depth how many levels of children should be considered
   * @return the cloned node (element)
   * @depth level of children to be cloned All attributes except xml:id and office:value attributes
   *     will not be cloned.
   */
  public Node cloneNode(int depth) {
    OdfElement cloneElement = ((OdfFileDom) this.ownerDocument).newOdfElement(this.getClass());
    // if it is an unknown ODF element
    if (cloneElement == null) {
      cloneElement =
          new OdfAlienElement(
              (OdfFileDom) this.getOwnerDocument(),
              OdfName.newName(
                  OdfNamespace.getNamespace(this.getNamespaceURI()), this.getTagName()));
    }

    if (attributes != null) {
      for (int i = 0; i < attributes.getLength(); i++) {
        Node item = attributes.item(i);
        String qname = null;
        String prefix = item.getPrefix();
        if (prefix == null) {
          qname = item.getLocalName();
          cloneElement.setAttribute(qname, item.getNodeValue());
        } else {
          qname = prefix + ":" + item.getLocalName();
          // cell value & type handling might as well overwritten in cell class
          if (!qname.equals("xml:id")
              && !qname.equals("office:value")
              && !qname.equals("calcext:value-type")
              && !qname.equals("office:value-type")) {
            cloneElement.setAttributeNS(item.getNamespaceURI(), qname, item.getNodeValue());
          }
        }
      }
    }

    // aside of the XML the flag of being a component root have to be copied
    if (this.isComponentRoot) {
      cloneElement.markAsComponentRoot(true);
      cloneElement.mComponentSize = this.mComponentSize;
      cloneElement.mIsIgnoredComponent = this.mIsIgnoredComponent;
      if (!this.mIsIgnoredComponent) {
        Component.createComponent(this.getComponent().getParent(), cloneElement);
      }
    }

    if (depth > 0) {
      Node childNode = getFirstChild();
      while (childNode != null) {
        if (childNode instanceof OdfElement) {
          cloneElement.appendChild(((OdfElement) childNode).cloneNode(depth - 1));
        }
        childNode = childNode.getNextSibling();
      }
    }
    // ToDo: There should be an easier - more obvious - way than this...
    if (this.selfAndDescendantTextIgnoredAsComponent()) {
      cloneElement.ignoredComponent(true);
    }
    cloneElement.mComponentSize = this.mComponentSize;
    return cloneElement;
  }

  /**
   * Clones the content of the source element including attributes even xml:id to the target
   * element. Helpful when changing a <text:h> to a <text:p> and vice versa, when outline attribute
   * changes.
   *
   * @param source the element to copy the content & attributes from.
   * @param target the element to copy the content & attributes into.
   * @param deep if a deep copy should happen. If false only the source element attributes will be
   *     copied, otherwise all descendants.
   * @return the target element with all new nodes
   */
  // ToDo: Test if a parameter by reference isn't working here!
  public static OdfElement cloneNode(OdfElement source, OdfElement target, boolean deep) {
    NamedNodeMap attributes = source.getAttributes();
    if (attributes != null) {
      for (int i = 0; i < attributes.getLength(); i++) {
        Node item = attributes.item(i);
        String qname;
        String prefix = item.getPrefix();
        if (prefix == null) {
          qname = item.getLocalName();
          target.setAttribute(qname, item.getNodeValue());
        } else {
          qname = prefix + ":" + item.getLocalName();
          target.setAttributeNS(item.getNamespaceURI(), qname, item.getNodeValue());
        }
      }
    }

    // aside of the XML the flag of being a component root have to be copied
    if (source.isComponentRoot) {
      target.markAsComponentRoot(true);
      target.mComponentSize = source.mComponentSize;
      target.mComponent = source.mComponent;
    }

    if (deep) {
      Node childNode = source.getFirstChild();
      Node newNode;
      while (childNode != null) {
        if (childNode instanceof OdfElement) {
          newNode = ((OdfElement) childNode).cloneNode(true);
        } else {
          newNode = childNode.cloneNode(true);
        }
        target.appendChild(newNode);

        childNode = childNode.getNextSibling();
      }
    }
    // ToDo: There should be an easier - more obvious - way than this...
    if (source.selfAndDescendantTextIgnoredAsComponent()) {
      target.ignoredComponent(true);
    }
    target.mComponentSize = source.mComponentSize;
    return target;
  }

  @Override
  public Node appendChild(Node node) {
    // No Counting necessary as appendChild() will call insertBefore()
    if (node instanceof OdfElement) {
      OdfElement e = (OdfElement) node;
    }
    return super.appendChild(node);
  }

  /** Recursive traverse the potential text container and count its content size */
  private static int descendantsCount(Node parent, int size) {
    if (!isIgnoredElement((Element) parent)) {
      NodeList children = parent.getChildNodes();
      Node child;
      for (int i = 0; i < children.getLength(); i++) {
        child = children.item(i);
        if (child instanceof Text) {
          size += ((Text) child).getLength();
        } else if (child instanceof OdfElement) {
          OdfElement element = (OdfElement) child;
          if (Component.isTextSelection(element)) {
            size += descendantsCount(element, size);
          } else if (element.isComponentRoot()) {
            size += element.componentSize();
          }
        }
      }
    }
    return size;
  }

  /** Recursive traverse the text container and count the size of the content */
  public int componentSize() {
    ////// SVANTE CLEAN ME
    //		if(mComponentSize == null){
    //			if(isComponentRoot()){
    //				mComponentSize = 1;
    //			}else{
    //				mComponentSize = 0;
    //			}
    //		}
    return mComponentSize;
  }

  /**
   * indicates if some other object is equal to this one.
   *
   * @param obj - the reference object with which to compare.
   * @return true if this object is the same as the obj argument; false otherwise.
   */
  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }

    if ((obj == null) || !(obj instanceof OdfElement)) {
      return false;
    }

    OdfElement compare = (OdfElement) obj;

    // compare node name
    if (!localName.equals(compare.localName)) {
      return false;
    }

    if (!this.namespaceURI.equals(compare.namespaceURI)) {
      return false;
    }

    // compare node attributes
    if (attributes == compare.attributes) {
      return true;
    }

    if ((attributes == null) || (compare.attributes == null)) {
      return false;
    }

    int attr_count1 = attributes.getLength();
    int attr_count2 = compare.attributes.getLength();

    List<Node> attr1 = new ArrayList<Node>();
    for (int i = 0; i < attr_count1; i++) {
      Node node = attributes.item(i);
      if (node.getNodeValue().length() == 0) {
        continue;
      }
      attr1.add(node);
    }

    List<Node> attr2 = new ArrayList<Node>();
    for (int i = 0; i < attr_count2; i++) {
      Node node = compare.attributes.item(i);
      if (node.getNodeValue().length() == 0) {
        continue;
      }
      attr2.add(node);
    }

    if (attr1.size() != attr2.size()) {
      return false;
    }

    for (int i = 0; i < attr1.size(); i++) {
      Node n1 = attr1.get(i);
      if (n1.getLocalName().equals("name")
          && n1.getNamespaceURI().equals(OdfDocumentNamespace.STYLE.getUri())) {
        continue; // do not compare style names
      }
      Node n2 = null;
      int j = 0;
      for (j = 0; j < attr2.size(); j++) {
        n2 = attr2.get(j);
        if (n1.getLocalName().equals(n2.getLocalName())) {
          String ns1 = n1.getNamespaceURI();
          String ns2 = n2.getNamespaceURI();
          if (ns1 != null && ns2 != null && ns1.equals(ns2)) {
            break;
          }
        }
      }
      if (j == attr2.size()) {
        return false;
      }

      if (!n1.getTextContent().equals(n2.getTextContent())) {
        return false;
      }
    }

    // now compare child elements
    NodeList childs1 = this.getChildNodes();
    NodeList childs2 = compare.getChildNodes();

    int child_count1 = childs1.getLength();
    int child_count2 = childs2.getLength();
    if ((child_count1 == 0) && (child_count2 == 0)) {
      return true;
    }

    List<Node> nodes1 = new ArrayList<Node>();
    for (int i = 0; i < child_count1; i++) {
      Node node = childs1.item(i);
      if (node.getNodeType() == Node.TEXT_NODE) {
        if (node.getNodeValue().trim().length() == 0) {
          continue; // skip whitespace text nodes
        }
      }
      nodes1.add(node);
    }

    List<Node> nodes2 = new ArrayList<Node>();
    for (int i = 0; i < child_count2; i++) {
      Node node = childs2.item(i);
      if (node.getNodeType() == Node.TEXT_NODE) {
        if (node.getNodeValue().trim().length() == 0) {
          continue; // skip whitespace text nodes
        }
      }
      nodes2.add(node);
    }

    if (nodes1.size() != nodes2.size()) {
      return false;
    }

    for (int i = 0; i < nodes1.size(); i++) {
      Node n1 = nodes1.get(i);
      Node n2 = nodes2.get(i);
      if (!n1.equals(n2)) {
        return false;
      }
    }
    return true;
  }

  protected void onRemoveNode(Node node) {
    if (node != null) {
      Node child = node.getFirstChild();
      while (child != null) {
        this.onRemoveNode(child);
        child = child.getNextSibling();
      }

      if (OdfElement.class.isInstance(node)) {
        ((OdfElement) node).onRemoveNode();
      }
    }
  }

  protected void onInsertNode(Node node) {
    Node child = node.getFirstChild();
    while (child != null) {
      this.onInsertNode(child);
      child = child.getNextSibling();
    }

    if (OdfElement.class.isInstance(node)) {
      ((OdfElement) node).onInsertNode();
    }
  }

  protected void onRemoveNode() {}

  protected void onInsertNode() {}

  @Override
  public Node insertBefore(Node newChild, Node refChild) throws DOMException {
    Node n = null;
    onInsertNode(newChild);
    n = super.insertBefore(newChild, refChild);
    raiseComponentSize(newChild);
    return n;
  }

  /* Removes the element from the DOM tree, but keeping its ancestors by moving its children in its place */
  public static Element removeSingleElement(Element oldElement) throws DOMException {
    Element parent = (Element) oldElement.getParentNode();
    if (parent != null) {
      NodeList children = oldElement.getChildNodes();
      int childCount = children.getLength();
      Node lastChild = children.item(childCount - 1);
      parent.replaceChild(lastChild, oldElement);
      Node newChild;
      for (int i = childCount - 2; i >= 0; i--) {
        newChild = children.item(i);
        parent.insertBefore(newChild, lastChild);
        lastChild = newChild;
      }
    }
    return parent;
  }

  @Override
  public Node removeChild(Node oldChild) throws DOMException {
    onRemoveNode(oldChild);
    reduceComponentSize(oldChild);
    return super.removeChild(oldChild);
  }

  /** Component size is being reduced by the size of the child */
  private void reduceComponentSize(Node child) {
    if (child instanceof Text) {
      changeSize(-1 * ((Text) child).getLength());
    } else if (child instanceof Element) {
      if (Component.isTextComponentRoot(child)) {
        changeSize(-1);
      } else {
        changeSize(-1 * descendantsCount(child, 0));
      }
    }
  }

  /**
   * Returns if the text should be returned or is under a nested paragraph or ignored text element
   * (e.g. text:note-citation).
   */
  private boolean isIgnoredText(OdfElement parent) {
    boolean isIgnored = true;
    while (parent != null) {
      if (parent.isComponentRoot() || parent.selfAndDescendantTextIgnoredAsComponent()) {
        if (parent.selfAndDescendantTextIgnoredAsComponent()) {
          isIgnored = true;
        } else {
          isIgnored = false;
        }
        break;
      }
      parent = parent.getParentAs(OdfElement.class);
    }
    return isIgnored;
  }

  /**
   * @return true if the element does represent multiple instances. (only applicable for some
   *     elements as cell or row).
   */
  public boolean isRepeatable() {
    return Boolean.FALSE;
  }

  /** @return the repetition the element represents, by default it is 1 */
  public int getRepetition() {
    return 1;
  }

  /** Component size is being reduced by the size of the child */
  private void raiseComponentSize(Node child) {
    if (child instanceof Text) {
      if (!isIgnoredText((OdfElement) child.getParentNode())) {
        changeSize(((Text) child).getLength());
      }
    } else if (child instanceof Element) {
      if (child instanceof OdfElement && ((OdfElement) child).isComponentRoot()) {
        if (!((OdfElement) child).selfAndDescendantTextIgnoredAsComponent()) {
          // in theory 1 is the default and repeated factors and space count factors should be
          // applied
          // it is something different to size (which returns the content size, instead it is like a
          // width?)

          // Elements with repetition are: text:s, table:table-cell and table:table-row
          int repetition = ((OdfElement) child).getRepetition();
          if (repetition != 1) {
            changeSize(repetition);
          } else {
            changeSize(1);
          }
        }
        //				// SPECIAL HANDLING FOR FRAME/IMAGE COMPONENT
        //			} else if (child instanceof DrawImageElement) {
        //				Node precedingImageSibling = null;
        //				precedingImageSibling = child;
        //				boolean frameAlreadyCount = false;
        //				while ((precedingImageSibling = precedingImageSibling.getPreviousSibling()) != null) {
        //					if (precedingImageSibling instanceof DrawImageElement) {
        //						frameAlreadyCount = true;
        //					}
        //				}
        //				if (!frameAlreadyCount) {
        //					Node parent = child.getParentNode();
        //					if (parent != null) {
        //						Node grandParent = parent.getParentNode();
        //						if (grandParent != null) {
        //							((OdfElement) grandParent).changeSize(1);
        //						}
        //					}
        //				}
      } else {
        // ToDo: Improvement: we may limit to elements that may be text delimiter (span) or
        // inbetween a text container and text delimiter (for ongoing recursion)
        changeSize(descendantsCount(child, 0));
      }
    }
  }

  /** A change of the element will be raised to the top till a component is found */
  // ToBeMoved to TEXT CONTINER CHILDREN only!
  private int changeSize(int sizeDifference) {
    int size = 0;
    if (sizeDifference != 0) {
      OdfElement element = this;
      if (!element.isComponentRoot() && !isIgnoredElement(element)) {
        do {
          element = element.getParentAs(OdfElement.class);
        } while (element != null && !element.isComponentRoot() && !isIgnoredElement(element));
      }
      if (element.isComponentRoot()) {
        element.mComponentSize = element.componentSize() + sizeDifference;
        size = element.mComponentSize;
        //				element.mComponentSize += sizeDifference;
        //				size = element.mComponentSize;

      }
    }
    return size;
  }

  /** Removes all the content from the element */
  public void removeContent() {
    // ToDo: Remove Component List Structure -- The first loop is only temporary
    for (int i = 0; i < mComponent.size(); i++) {
      // remove component
      mComponent.remove(i);
    }
    mComponentSize = 0;
    NodeList children = this.getChildNodes();
    for (int i = children.getLength() - 1; i >= 0; i--) {
      Node child = children.item(i);
      if (child != null) {
        this.removeChild(child);
      }
    }
  }

  @Override
  public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
    raiseComponentSize(newChild);
    onRemoveNode(oldChild);
    onInsertNode(newChild);
    // Currently the replace only reduces
    // reduceComponentSize(oldChild);
    return super.replaceChild(newChild, oldChild);
  }

  /**
   * Accept an visitor instance to allow the visitor to do some operations. Refer to visitor design
   * pattern to get a better understanding.
   *
   * @param visitor an instance of DefaultElementVisitor
   */
  public void accept(ElementVisitor visitor) {
    visitor.visit(this);
  }

  /** Returns the component reference */
  public OdfElement getComponentRoot() {
    OdfElement element = this;
    if (!element.isComponentRoot()) {
      do {
        element = element.getParentAs(OdfElement.class);
      } while (element != null && !element.isComponentRoot());
    }
    return element;
  }

  public void markAsComponentRoot(boolean isRoot) {
    isComponentRoot = true;
    OdfElement parent = (OdfElement) getParentNode();
    // if the component was already added, addChild the size now
    //		if(parent != null && this instanceof DrawFrameElement){
    //			parent.raiseComponentSize(this);
    //		}
  }

  public Component getComponent() {
    return mComponent;
  }

  public void setComponent(Component component) {
    mComponent = component;
  }

  public boolean isComponentRoot() {
    return isComponentRoot;
  }

  /**
   * @return true if the text should not count as for component path nor the element root itself.
   *     This might occur for nested paragraphs or ignored text element (e.g. text:note-citation).
   */
  public boolean selfAndDescendantTextIgnoredAsComponent() {
    return mIsIgnoredComponent;
  }

  /**
   * @param true if the text should not count as for component path nor the element root itself.
   *     This might occur for nested paragraphs or ignored text element (e.g. text:note-citation).
   *     For instance called by a SAX Component parser, * * * * * * * see <code>
   *     org.odftoolkit.odfdom.component.OdfFileSaxHandler</code>
   */
  public void ignoredComponent(boolean isIngoredComponent) {
    mIsIgnoredComponent = isIngoredComponent;
  }

  /**
   * If the string is inserted into a text:p/text:h element and it will be inserted in the start/end
   * all spaces are replaced by <text:s/> element(s). tabulator and linefeeds are being removed.
   *
   * <p>If both the previous text node ends with a space and newString starts with a space, we would
   * need to encode the single leading space as an element, otherwise it would be stripped. Same
   * occurs for the next text node and an ending space. For Example: <span> text </span><text:s
   * c="7"/><span> text2 </span> <== SAVE when starting ending a span as well with space element
   * independent of preceding
   */
  protected static void appendUsingWhitespaceHandling(
      Node precedingNode, OdfElement parent, Node followingNode, String newString) {
    //		addTextNode(precedingNode, parent, followingNode, newString);
    // only addChild text, if parent exists
    if (parent != null) {
      //            // if there is only one span in a paragraph/heading
      //            if(parent instanceof TextParagraphElementBase){
      //                // add the new content to this span
      //                NodeList children = parent.getChildNodes();
      //                if(children.getLength() == 1){
      //                    Node child = children.item(0);
      //                    if(child instanceof TextSpanElement){
      //                        parent = (OdfElement) child;
      //                    }
      //                }
      //            }
      int spaceCount = 0;
      // Note: The delta between startPosition and endPosition marks the text to be written out
      // startPosition will only be raised to endposition, when characters have to be skipped!
      int startPos = 0;
      int endPos = 0;
      // check if first character is a white space
      for (int i = 0; i < newString.length(); i++) {
        char c = newString.charAt(i);
        if (c == '\u0020' // space
            || c == '\t' // \t (tabulator = 0x09)
            // \r (carriage return = 0x0D)
            || c == '\r'
            // \n (line feed =' 0x0A)
            || c == '\n') {
          spaceCount++;

          if (spaceCount > 1) {
            // if there are more than one space a space element have to be inserted, write out the
            // previous spaces
            if (endPos - startPos > 0) {
              precedingNode =
                  addTextNode(
                      precedingNode, parent, followingNode, newString.substring(startPos, endPos));
              // for the single whitespace not written
            }
            // NOT including the additional whitespace character
            startPos = endPos;
          }
        } else { // else if there was no whitespace character found or at the beginning
          if (spaceCount > 1 || i == 1 && spaceCount == 1) {
            TextSElement s = new TextSElement((OdfFileDom) parent.getOwnerDocument());
            // if there were multiple preceing whitespace, write out a space Element
            if (spaceCount > 1) {
              s.setTextCAttribute(spaceCount);
            }

            // write out space element
            precedingNode = addElementNode(precedingNode, parent, followingNode, s);
            endPos += spaceCount;
            startPos = endPos;
            spaceCount = 0;

            // reset space count to zero as now a character was found
          } else if (spaceCount == 1) {
            endPos++;
            spaceCount = 0;
          }

          // write out character
          endPos++; // including this character
        }
      }
      // reset space count to zero as now a character was found
      if (spaceCount > 1) {
        TextSElement s = new TextSElement((OdfFileDom) parent.getOwnerDocument());
        // if there were multiple preceing whitespace, write out a space Element
        if (spaceCount > 1) {
          s.setTextCAttribute(spaceCount);
        }
        // write out space element
        precedingNode = addElementNode(precedingNode, parent, followingNode, s);
        endPos += spaceCount;
        startPos = endPos;
        spaceCount = 0;
      }
      if (endPos - startPos > 0) {
        precedingNode =
            addTextNode(
                precedingNode, parent, followingNode, newString.substring(startPos, endPos));
      }
      if (spaceCount == 1) {
        TextSElement s = new TextSElement((OdfFileDom) parent.getOwnerDocument());
        addElementNode(precedingNode, parent, followingNode, s);
      }
    } else {
      Logger.getLogger(OdfElement.class.getName())
          .log(Level.SEVERE, "Node parent should not be NULL!");
    }
  }

  private static Node addElementNode(
      Node precedingNode, OdfElement parent, Node followingNode, Element newElement) {
    Node newNode = null;
    // APPEND: if there is no text node to expand and no element that follows
    if (followingNode == null) {
      newNode = parent.appendChild(newElement);
    } else if (followingNode != null) {
      // insert before the given element
      newNode = parent.insertBefore(newElement, followingNode);
    }
    return newNode;
  }

  private static Node addTextNode(
      Node precedingNode, OdfElement parent, Node followingNode, String newString) {
    Node newNode = null;
    // APPEND: if there is no text node to expand and no element that follows
    if (precedingNode == null && followingNode == null
        || precedingNode != null && precedingNode instanceof Element && followingNode == null) {
      newNode = parent.appendChild(parent.getOwnerDocument().createTextNode(newString));
    } else {
      // INSERT:
      if (precedingNode != null && precedingNode instanceof Text) {
        // insert at the end of the given text
        ((Text) precedingNode).appendData(newString);
        newNode = precedingNode;
      } else if (followingNode != null) {
        // insert before the given element
        newNode =
            parent.insertBefore(parent.getOwnerDocument().createTextNode(newString), followingNode);
      }
    }
    return newNode;
  }

  private static Node addElementNode(
      Node precedingNode, OdfElement parent, Node followingNode, OdfElement newElement) {
    Node newNode = null;
    // APPEND: if there is no text node to expand and no element that follows
    if (followingNode == null) {
      newNode = parent.appendChild(newElement);
    } else {
      // INSERT:
      // insert before the given following-node
      newNode = parent.insertBefore(newElement, followingNode);
    }
    return newNode;
  }

  /**
   * Splitting the element at the given position into two halves
   *
   * @param posStart The logical position of the first character (or other paragraph child
   *     component) that will be moved to the beginning of the new paragraph. Counting starts with
   *     0.
   * @return the new created second text container
   */
  public OdfElement split(int posStart) {
    OdfElement newSecondElement = this;
    // split with 0 is allowed. For instance to create new paragraphs!
    if (posStart > -1) {
      newSecondElement = (OdfElement) this.cloneNode(true);
      int size = OdfElement.getContentSize(this);

      // This will become the first paragraph
      // Only delete if the start node is within the component length
      // Do NOT do a a
      // if there is only one character the size is 1 and the textPosStart would be 0
      // TODO FIXME: Why was the parent DELETEd, when next line was after the condition??!?
      Element parent = (Element) this.getParentNode();
      if (size > posStart) {
        this.delete(posStart, size);
      }
      Node _nextSibling = this.getNextSibling();
      if (_nextSibling != null) {
        parent.insertBefore(newSecondElement, _nextSibling);
      } else {
        parent.appendChild(newSecondElement);
      }

      // only delete if the start position is not before the first component
      if (posStart != 0) {
        // minus one, as the textPosStart was already in the first Element
        newSecondElement.delete(0, posStart - 1);
      }
    }
    // FIXME: There is a better way than cloning and deleting two halves, but it works for POC
    // splitNodes(this.getFirstChild(), textPosStart, 0);
    return newSecondElement;
  }

  /** ******************************************************** */
  /**
   * Receives node from this text container element.
   *
   * @param textPosStart The start delimiter for the child
   * @return the child node might be text or element
   */
  public Node receiveNode(int textPosStart) {
    if (textPosStart < 0) {
      Logger.getLogger(OdfElement.class.getName())
          .warning(
              "A negative index " + textPosStart + " was given to insert text into the paragraph!");
    }
    // start recrusion
    ArrayList<Node> nodeContainer = new ArrayList<Node>(1);
    boolean withinTextContainer = this instanceof TextPElement || this instanceof TextHElement;
    TextContentTraverser.traverseSiblings(
        this.getFirstChild(),
        0,
        textPosStart,
        textPosStart + 1,
        TextContentTraverser.Algorithm.RECEIVE,
        nodeContainer,
        withinTextContainer);

    Node receivedNode = null;
    if (nodeContainer.size() == 1) {
      receivedNode = nodeContainer.get(0);
    }
    return receivedNode;
  }

  /**
   * @param textPosStart the first text level component to be marked, start counting with 0
   * @param textPosEnd the last text level component to be marked, start counting with 0
   * @param newSelection the element that should embrace the text defined by the positions provided
   */
  public void markText(int textPosStart, int textPosEnd, JSONObject formatChanges) {
    if (formatChanges != null) {
      if (textPosStart < 0) {
        Logger.getLogger(OdfElement.class.getName())
            .warning(
                "A negative index "
                    + textPosStart
                    + " was given to insert text into the paragraph!");
      }
      if (textPosEnd < textPosStart) {
        // might be caused by invalid span around whitespace that is being eleminated by ODF
        // whitespacehandling
        Logger.getLogger(OdfElement.class.getName())
            .warning(
                "The start index "
                    + textPosStart
                    + " shall not be higher than the end index "
                    + textPosEnd
                    + "!");
      }
      // incrementing textPosEnd to get in sync with string counting
      TextContentTraverser.traverseSiblings(
          this.getFirstChild(),
          0,
          textPosStart,
          textPosEnd + 1,
          TextContentTraverser.Algorithm.MARK,
          formatChanges,
          new HashMap<OdfName, OdfElement>());
    }
  }

  /** Counts the number of descendant components */
  public int countDescendantComponents() {
    return TextContentTraverser.traverseSiblings(
        this.getFirstChild(),
        0,
        0,
        Integer.MAX_VALUE,
        TextContentTraverser.Algorithm.COUNT,
        Integer.MAX_VALUE,
        Boolean.TRUE);
  }

  /** Counts the number of child components */
  public int countChildComponents(Boolean hasTextComponents) {
    return TextContentTraverser.traverseSiblings(
        this.getFirstChild(),
        0,
        0,
        Integer.MAX_VALUE,
        TextContentTraverser.Algorithm.COUNT,
        Integer.MAX_VALUE,
        hasTextComponents);
  }

  // Below recursion works, the new one not yet..
  //	public void moveChildrenTo(Element newParent) {
  //		// incrementing textPosEnd to get in sync with string counting
  //		TextContentTraverser.traverseSiblings(this.getFirstChild(), 0, 0, Integer.MAX_VALUE,
  // TextContentTraverser.Algorithm.MOVE, newParent);
  //	}
  public void moveChildrenTo(Element newParent) {
    moveNodes(this.getFirstChild(), newParent);
  }

  private void moveNodes(Node node, Element newParent) {
    while (node != null) {
      // IMPORTANT: Get next sibling first, otherwise references get lost when appending to new
      // parent
      Node _nextSibling = node.getNextSibling();
      if (node instanceof Element) {
        newParent.appendChild(node);
      } else if (node instanceof Text) {
        moveTextNode((Text) node, newParent);
      }
      node = _nextSibling;
    }
  }

  private void moveTextNode(Text node, Element newParent) {
    if (node != null) {
      newParent.appendChild(node);
    }
  }

  /**
   * Insert text to a certain position. The text will be appended to the previous position text, so
   * the span of the previous character will be expanded
   *
   * @param newString string to be inserted
   * @param position text index of the new string
   */
  public void insert(String newString, int textPosStart) {
    if (newString != null && !newString.isEmpty()) {
      insertContent(newString, textPosStart);
    }
  }

  public void insert(Node newNode, int textPosStart) {
    if (newNode != null) {
      insertContent(newNode, textPosStart);
    }
  }

  /**
   * Insert text to a certain position. The text will be appended to the previous position text, so
   * the span of the previous character will be expanded
   *
   * @param newString string to be inserted
   * @param position text index of the new string
   */
  private void insertContent(Object content, int textPosStart) { // parameter order?
    if (textPosStart < 0) {
      Logger.getLogger(OdfElement.class.getName())
          .warning(
              "A negative index " + textPosStart + " was given to insert text into the paragraph!");
    }
    Node firstChild = this.getFirstChild();
    if (firstChild == null) {
      // if there is no new node, simply exchange multiple whitespaces with <text:s>
      if (content instanceof String) {
        appendUsingWhitespaceHandling(null, this, null, (String) content);
      } else if (content instanceof Element) {
        this.appendChild((Element) content);
      }
    } else {
      List<Object> newData = new ArrayList<Object>(2);
      newData.add(content);
      int currentPos =
          TextContentTraverser.traverseSiblings(
              firstChild,
              0,
              textPosStart,
              textPosStart,
              TextContentTraverser.Algorithm.INSERT,
              newData);
      if (newData.size() == 1) {
        // if there is were element(s), but no components within this element
        if (content instanceof String) {
          appendUsingWhitespaceHandling(null, this, null, (String) content);
        } else if (content instanceof Element) {
          this.appendChild((Element) content);
        }
      }
      //				if (currentPos > textPosStart) {
      //					Logger.getLogger(OdfElement.class.getName()).warning("The index " + textPosStart + " is
      // outside the existing text of the paragraph!");
      //				}
    }
  }

  /**
   * Deletes text from this paragraph element.
   *
   * @param textPosStart Counting starts with 0, which is the first character of the paragraph.
   * @param textPosEnd The end delimiter for the deletion. To delete text to the end of the
   *     paragraph, as represent for the end of the paragraph Integer.MAX_VALUE can be used.
   */
  public void delete(int textPosStart, int textPosEnd) {
    if (textPosStart < 0) {
      Logger.getLogger(OdfElement.class.getName())
          .warning(
              "A negative index " + textPosStart + " was given to insert text into the paragraph!");
    }
    if (textPosEnd < textPosStart) {
      Logger.getLogger(OdfElement.class.getName())
          .warning(
              "The start index "
                  + textPosStart
                  + " have to be higher than the end index "
                  + textPosEnd
                  + "!");
    }

    /**
     * Deletion implementation: 1) The text position of the first deletion will be searched. 2)
     * Afterwards all following-sibling, text and nodes will be deleted, until the end text position
     * is found 3) Split the text take the returned and remove it from parent
     */
    List deleteStatus = new ArrayList(1);

    // start recrusion
    TextContentTraverser.traverseSiblings(
        this.getFirstChild(),
        0,
        textPosStart,
        textPosEnd + 1,
        TextContentTraverser.Algorithm.DELETE,
        deleteStatus);
  }

  private static class TextContentTraverser {

    /**
     * @param node the element node will be checked if it is a text or an element. If an element it
     *     will be dispatched to check component than executed
     * @param currentPos the current component position
     * @param posStart the text position where the span starts
     * @param posEnd the text position, where the span ends (one higher as the last component number
     *     to be included)
     * @param algorithm dependent on this variable a different subroutine is being used after
     *     traversing the sub-tree, e.g. insert, delete, mark, count..
     * @param data differs from the type of algorithm, e.g. for insert it contains the data to be
     *     inserted
     * @return the current position after the node was processed
     */
    private static int traverseSiblings(
        Node node, int currentPos, int posStart, int posEnd, Algorithm algorithm, Object... data) {
      // loop to take over components into the span & split them if required, until final position
      // was reached!
      if (algorithm.equals(Algorithm.DELETE)) {
        // position equal as after the last found even unknown components will be deleted
        while (node != null && currentPos <= posEnd) {
          // IMPORTANT: get next sibling first, otherwise references get lost by Xerces during
          // splitting
          Node _nextSibling = node.getNextSibling();
          if (node instanceof Element) {
            // ** THE COMMENTED CODE BELOW REMOVED A COMPONENT, WHICH BECAME EMPTY AFTER DELETING
            // SOME CONTENT.
            // ** THIS IS NOW CONSIDERED HARMFUL, AS PARAGRAPH/HEADING/TABLE ALWAYS REMAIN
            //						int countBefore = ((List) data).size();
            currentPos =
                checkElementNode((Element) node, currentPos, posStart, posEnd, algorithm, data[0]);
            // OPTIMIZATION: the data is being used as flag to realize, when the element being
            // checked does not contain any component
            //						List deleteStatus = (List) data;
            //						int countAfter = deleteStatus.size();
            //						if (countBefore < countAfter && (Boolean) deleteStatus.get(0) && ((OdfElement)
            // node).countDescendantComponents() == 0) {
            //							Element parent = (Element) node.getParentNode();
            //							if (parent != null) {
            //								// delete the empty boilerplate
            //								parent.removeChild(node);
            //								// reset the status
            //								deleteStatus.clear();
            //							}
            //						}
          } else if (node instanceof Text) {
            currentPos = algorithm.execute(node, currentPos, posStart, posEnd, data[0]);
          }
          // next sibling will be checked
          node = _nextSibling;
        }
      } else if (algorithm.equals(Algorithm.INSERT)) {

        // position equal as it could be inserted before on 0th (first) place
        while (node != null && (currentPos < posEnd || (posEnd == 0 && currentPos == 0))) {
          // IMPORTANT: get next sibling first, otherwise references get lost by Xerces during
          // splitting
          Node _nextSibling = node.getNextSibling();
          if (node instanceof Element) {
            currentPos =
                checkElementNode((Element) node, currentPos, posStart, posEnd, algorithm, data[0]);
          } else if (node instanceof Text) {
            currentPos = algorithm.execute(node, currentPos, posStart, posEnd, data[0]);
          }
          // next sibling will be checked
          node = _nextSibling;
        }
      } else if (algorithm.equals(Algorithm.COUNT)) {
        while (node != null && (currentPos < posEnd || (posEnd == 0 && currentPos == 0))) {
          // IMPORTANT: get next sibling first, otherwise references get lost by Xerces during
          // splitting
          Node _nextSibling = node.getNextSibling();
          if (node instanceof Element) {
            currentPos =
                checkElementNode(
                    (Element) node, currentPos, posStart, posEnd, algorithm, data[0], data[1]);
          } else if (node instanceof Text && (Boolean) data[1]) {
            currentPos = algorithm.execute(node, currentPos, posStart, posEnd, data[0], data[1]);
          }
          // next sibling will be checked
          node = _nextSibling;
        }
      } else if (algorithm.equals(Algorithm.MARK)) {
        while (node != null && currentPos < posEnd) {
          // IMPORTANT: get next sibling first, otherwise references get lost by Xerces during
          // splitting
          Node _nextSibling = node.getNextSibling();
          currentPos = algorithm.execute(node, currentPos, posStart, posEnd, data[0], data[1]);
          node = _nextSibling;
        }
      } else {
        boolean withinTextContainer = (Boolean) data[1];
        while (node != null && currentPos < posEnd) {
          // IMPORTANT: get next sibling first, otherwise references get lost by Xerces during
          // splitting
          Node _nextSibling = node.getNextSibling();
          // ToDo: || algorithm.equals(Algorithm.MOVE)
          if (node instanceof Element) {
            // if not yet marked as text container (text traversering enabled), check if passing a
            // container
            if (withinTextContainer == false) {
              if (node instanceof TextPElement || node instanceof TextHElement) {
                data[1] = Boolean.TRUE;
              }
            }
            currentPos =
                checkElementNode((Element) node, currentPos, posStart, posEnd, algorithm, data);
          } else if (node instanceof Text && withinTextContainer) {
            currentPos = algorithm.execute(node, currentPos, posStart, posEnd, data);
          }
          // next sibling will be checked
          node = _nextSibling;
        }
      }
      return currentPos;
    }

    /**
     * @param currentNode the element currentNode will be checked if it is a component. If a
     *     component it will be executed fully or partly moved into the span
     * @param currentPos the current component position
     * @param posStart the text position where the span starts
     * @param posEnd the text position, where the span ends (one higher as the last component number
     *     to be included)
     * @param newSpan the span collecting the marked components
     * @return
     */
    private static int checkElementNode(
        Element currentNode,
        int currentPos,
        int posStart,
        int posEnd,
        Algorithm algorithm,
        Object... data) {
      if (currentNode != null && !OdfElement.isIgnoredElement(currentNode)) {
        if (currentNode instanceof OdfElement
            && ((OdfElement) currentNode).isComponentRoot()
            && !((OdfElement) currentNode).mIsIgnoredComponent) {
          currentPos = algorithm.execute(currentNode, currentPos, posStart, posEnd, data);
        } else {
          // if element is no component, neglect the element (e.g. another <text:span>, but analyze
          // its content
          Node firstChild = currentNode.getFirstChild();
          if (firstChild != null) {
            currentPos =
                traverseSiblings(firstChild, currentPos, posStart, posEnd, algorithm, data);
          }
        }
      }
      return currentPos;
    }

    /**
     * @param content the content to be formatted. Will be moved from its former parent into the
     *     span/anchor.
     * @param _nextSibling the span will be added in front of the sibling or appended to the parent
     *     if the sibling is NULL
     * @param formatChanges the format changes to be applied to the content will be moved to
     */
    private static void formatContent(
        Node content,
        Node _nextSibling,
        JSONObject formatChanges,
        Map<OdfName, OdfElement> formatElementHolder) {
      OdfFileDom xmlDoc = (OdfFileDom) content.getOwnerDocument();
      // 2DO: What if I have an anchor AND an span? we have to add it one by one first anchor
      JSONObject charFormatChanges = formatChanges.optJSONObject("character");
      // if a reference should be added for the given content
      TextAElement newAElement = null;
      // add an anchor with hyperlink if necessary
      if (charFormatChanges != null
          && charFormatChanges.has("url")
          && !charFormatChanges.get("url").equals(JSONObject.NULL)) {
        newAElement =
            getAnchorElement(
                (Node) content, xmlDoc, charFormatChanges.optString("url"), formatElementHolder);
        // if an anchor was reused, do not add it
        if (!newAElement.equals(content)) {
          addNewParent(content, _nextSibling, newAElement);
        }
        _nextSibling = null;
      }

      if (newAElement != null
          // NOTE: Calc Issue workaround: No span within anchors possible in OpenOffice CALC (no
          // text will be shown)
          && (xmlDoc.getDocument() instanceof OdfSpreadsheetDocument
              // NOTE: Annotation workaround: No span within anchors possible in Annotations (no
              // text will be shown)
              || isOfficeAnnotationChild(newAElement))) {
        NodeList innerSpans =
            newAElement.getElementsByTagNameNS(OdfDocumentNamespace.TEXT.getUri(), "span");
        int spanCount = innerSpans.getLength();
        for (int i = spanCount - 1; i >= 0; i--) {
          TextSpanElement innerSpan = (TextSpanElement) innerSpans.item(i);
          OdfElement.removeSingleElement(innerSpan);
        }
      } else {
        // the changes might be only deletion OR only addition OR a mix of both
        TextSpanElement newSpanElement = getSpanElement(xmlDoc, formatChanges, formatElementHolder);
        // if a new span exist
        if (newSpanElement != null) {
          if (!newSpanElement.hasAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "style-name")) {
            newSpanElement = null;
          } else {
            addNewParent(content, _nextSibling, newSpanElement);
          }
        }
        formatDescendants(
            content,
            newSpanElement,
            charFormatChanges != null && charFormatChanges.has("url"),
            xmlDoc,
            formatChanges,
            formatElementHolder);
      }
    }

    // Annotation workaround: No span within anchors possible in Annotations (no text will be shown)
    private static boolean isOfficeAnnotationChild(NodeImpl element) {
      return isOfficeAnnotationChild(element, 4);
    }

    private static boolean isOfficeAnnotationChild(NodeImpl element, int rec) {
      if (null != element) {
        if (element instanceof OfficeAnnotationElement) {
          return true;
        } else {
          if (rec > 0 && element instanceof OdfElement) {
            return isOfficeAnnotationChild(((OdfElement) element).ownerNode, rec - 1);
          } else {
            return false;
          }
        }
      } else {
        return false;
      }
    }

    private static void formatDescendants(
        Node content,
        TextSpanElement newSpan,
        boolean removeAnchors,
        OdfFileDom ownerDoc,
        JSONObject formatChanges,
        Map<OdfName, OdfElement> formatElementHolder) {
      if (content instanceof TextSpanElement) {
        mergeSpans(
            (TextSpanElement) content, newSpan, ownerDoc, formatChanges, formatElementHolder);
      }
      while (content instanceof TextAElement && removeAnchors) {
        content = removeSingleNode(content, content.getNextSibling());
        if (content instanceof TextSpanElement) {
          mergeSpans(
              (TextSpanElement) content, newSpan, ownerDoc, formatChanges, formatElementHolder);
        }
      }

      if (content.hasChildNodes()) {
        NodeList children = content.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
          Node child = children.item(i);
          if (child instanceof Element) {
            formatDescendants(
                child, newSpan, removeAnchors, ownerDoc, formatChanges, formatElementHolder);
          }
        }
      }
    }

    private static TextSpanElement mergeSpans(
        TextSpanElement content,
        TextSpanElement newSpan,
        OdfFileDom ownerDoc,
        JSONObject formatChanges,
        Map<OdfName, OdfElement> formatElementHolder) {
      if (newSpan != null && content != newSpan) {
        String originalStyleName = newSpan.getStyleName();
        content = (TextSpanElement) OdfStyle.mergeSelectionWithSameRange(newSpan, content);
        String mergedStyleName = newSpan.getStyleName();
        // if the style name was altered from the new span being added
        if (!originalStyleName.equals(mergedStyleName)) {
          // create a new one
          TextSpanElement originSpan = new TextSpanElement(ownerDoc);
          // add the original style-name
          originSpan.setAttributeNS(
              OdfDocumentNamespace.TEXT.getUri(), "text:style-name", originalStyleName);
          // add restore it back to the container, for the following containers, so they won't be
          // influenced by the merge
          formatElementHolder.put(TextSpanElement.ELEMENT_NAME, originSpan);
        }
      }
      JsonOperationConsumer.addStyle(formatChanges, content, ownerDoc);
      return content;
    }

    private static void addNewParent(Node existingChild, Node _nextSibling, OdfElement newParent) {
      Node parent = existingChild.getParentNode();
      parent.removeChild(existingChild);
      newParent.appendChild(existingChild);
      if (_nextSibling != null) {
        parent.insertBefore(newParent, _nextSibling);
      } else {
        parent.appendChild(newParent);
      }
    }

    /** Removes a single node and moves its children in its position */
    private static Node removeSingleNode(Node oldNode, Node _nextSibling) {
      Node parent = oldNode.getParentNode();
      Node _firstChild = null;
      if (oldNode.hasChildNodes()) {
        NodeList children = oldNode.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
          Node child = children.item(i);
          if (i == 0) {
            _firstChild = child;
          }
          if (_nextSibling != null) {
            parent.insertBefore(child, _nextSibling);
          } else {
            parent.appendChild(child);
          }
        }
      }
      parent.removeChild(oldNode);
      return _firstChild;
    }

    /*  */
    static enum Algorithm {

      /** */
      INSERT(1),
      /** */
      DELETE(2),
      /** */
      MARK(3),
      /** */
      RECEIVE(4),
      /** */
      MOVE(5),
      /** */
      COUNT(6);
      private int mId;

      Algorithm(int id) {
        mId = id;
      }

      int execute(Node currentNode, int currentPos, int posStart, int posEnd, Object... content) {
        switch (mId) {
          case 1:
            currentPos = insert(currentNode, currentPos, posStart, posEnd, (List) content[0]);
            break;
          case 2:
            currentPos = delete(currentNode, currentPos, posStart, posEnd, (List) content[0]);
            break;
          case 3:
            currentPos =
                mark(
                    currentNode,
                    currentPos,
                    posStart,
                    posEnd,
                    (JSONObject) content[0],
                    (Map<OdfName, OdfElement>) content[1]);
            break;
          case 4:
            currentPos = receive(currentNode, currentPos, posStart, posEnd, (ArrayList) content[0]);
            break;
          case 5:
            moveChildrenTo(currentNode, currentPos, posStart, posEnd, (Element) content[0]);
            break;
          case 6:
            currentPos = count(currentNode, currentPos, (Boolean) content[1]);
            break;
        }
        return currentPos;
      }

      /**
       * This function takes a node and moves it fully or partly into the given <text:span/>, if the
       * node components (e.g. character) are within the given posStart and posEnd.
       *
       * @param currentNode The current child node to
       * @param currentPos the current component position to be checked. starting with 0
       * @param posStart the start position of the span.
       * @param posEnd the end position of the span as string position. NOTE: One higher than the
       *     originally given component position. Therefore always one higher than posStart.
       * @param newNode
       * @return the position of the next component to be checked
       */
      int mark(
          Node currentNode,
          int currentPos,
          int posStart,
          int posEnd,
          JSONObject formatChanges,
          Map<OdfName, OdfElement> formatElementHolder) {
        if (currentNode != null) {
          Integer nextSplitPos;
          // if the start is equal to the first position avoid the first split
          if (currentPos >= posStart) {
            // we are about to gather content in our span
            nextSplitPos = posEnd;
          } else {
            // we have not reached the area to mark
            nextSplitPos = posStart;
          }

          // ** GET NODE SIZE
          Integer contentLength = getNodeWidth(currentNode);

          // addChild the full currentNode ==> if the end of the span is equal to the end of the
          // currentNode
          boolean isTotalSelection =
              (currentPos + contentLength == nextSplitPos) && nextSplitPos == posEnd;
          // addChild the middle part of the currentNode ==> if the currentNode already starts
          // within the span, but the end is not within the span
          boolean isFirstPart = (currentPos >= posStart && currentPos + contentLength < posEnd);
          // split the currentNode ==> if the next split position is within the currentNode
          boolean needsSplit = currentPos + contentLength > nextSplitPos;

          // MOVE PARTS INTO THE SELECTION ELEMENT
          if (isTotalSelection || isFirstPart) {
            Node _nextSibling = currentNode.getNextSibling();
            formatContent(currentNode, _nextSibling, formatChanges, formatElementHolder);
            currentPos += contentLength;

            // SPLIT THE NODE
          } else if (needsSplit) {
            int secondPartLength = 1;
            Node secondPart = null;
            if (currentNode instanceof Text) {
              // splitCursor is the first character of second part (counting starts with 0)
              secondPart = ((Text) currentNode).splitText(nextSplitPos - currentPos);
              secondPartLength = ((Text) secondPart).getLength();
            } else {
              // handle component split...
              secondPart = ((OdfElement) currentNode).split(nextSplitPos - currentPos);
              secondPartLength = OdfElement.getNodeWidth(secondPart);
            }
            boolean reachedStartPosition = nextSplitPos != posEnd;
            if (reachedStartPosition) {
              // if the second split is still in the same currentNode..
              // if the end of the cut is equal to the end of the span

              if (currentPos + (contentLength - secondPartLength) == posEnd) {
                // after the split the secondPart is the full fomrat
                formatContent(
                    secondPart, secondPart.getNextSibling(), formatChanges, formatElementHolder);
                currentPos += contentLength;
              } else {
                // position changed based on the first cut part
                currentPos = currentPos + (contentLength - secondPartLength);
                currentPos =
                    mark(
                        secondPart,
                        currentPos,
                        posStart,
                        posEnd,
                        formatChanges,
                        formatElementHolder);
              }
            } else {
              formatContent(currentNode, secondPart, formatChanges, formatElementHolder);
              currentPos += contentLength - secondPartLength;
            }
            // SKIP THE NODE AS SELECTION NOT STARTED
          } else {
            currentPos += contentLength;
          }
        }
        return currentPos;
      }

      int insert(Node currentNode, int currentPos, int posStart, int posEnd, List content) {
        if (currentNode != null) {
          // // ** GET NODE SIZE
          // Integer contentLength = getNodeWidth(currentNode);
          // ** GET NODE SIZE
          Integer contentLength = 1; // component default is 1
          if (currentNode instanceof Text) {
            contentLength = ((Text) currentNode).getLength();
          } else {
            // get size from component
            contentLength = ((OdfElement) currentNode).getRepetition();
          }
          // ** There is content to be inserted..
          if (currentPos
              == posStart) { // if we are already at the right place, insert content before the
            // currentNode
            Node parent = currentNode.getParentNode();
            Object newData = content.get(0);
            if (newData instanceof String) {
              OdfElement.appendUsingWhitespaceHandling(
                  null, (OdfElement) parent, currentNode, (String) newData);
            } else if (content.get(0) instanceof Element) {
              OdfElement.addElementNode(
                  null, (OdfElement) parent, currentNode, (Element) content.get(0));
            }
            // Mark that the content has been added
            content.add(Boolean.TRUE);
            currentPos += contentLength;
          } else if (currentPos + contentLength >= posStart) {
            // if the complete text node is selected, append behind..
            if (currentPos + contentLength == posStart) {
              Object newData = content.get(0);
              if (newData instanceof String) {
                OdfElement.appendUsingWhitespaceHandling(
                    currentNode,
                    (OdfElement) currentNode.getParentNode(),
                    currentNode.getNextSibling(),
                    (String) newData);
              } else if (newData instanceof Element) {
                OdfElement.addElementNode(
                    currentNode,
                    (OdfElement) currentNode.getParentNode(),
                    currentNode.getNextSibling(),
                    (Element) newData);
              }
              // Mark that the content has been added
              content.add(Boolean.TRUE);
              currentPos = posStart;
            } else { // else if only a part of the text node is selected
              Node secondPart = null;
              if (currentNode instanceof Text) {
                // splitCursor is the first character of second part (counting starts with 0)
                secondPart = ((Text) currentNode).splitText(posStart - currentPos);
                Object newData = content.get(0);
                if (newData instanceof String) {
                  OdfElement.appendUsingWhitespaceHandling(
                      currentNode,
                      (OdfElement) currentNode.getParentNode(),
                      secondPart,
                      (String) newData);
                } else if (content instanceof Element) {
                  OdfElement.addElementNode(
                      currentNode,
                      (OdfElement) currentNode.getParentNode(),
                      secondPart,
                      (Element) content);
                }
                // Mark that the content has been added
                content.add(Boolean.TRUE);
              } else {
                // handle component split...
                secondPart = ((OdfElement) currentNode).split(posStart - currentPos);
                Node parent = currentNode.getParentNode();
                Object newData = content.get(0);
                if (newData instanceof String) {
                  OdfElement.appendUsingWhitespaceHandling(
                      currentNode, (OdfElement) parent, secondPart, (String) newData);
                } else if (content instanceof Element) {
                  OdfElement.addElementNode(
                      currentNode, (OdfElement) parent, secondPart, (Element) content);
                }
                // Mark that the content has been added
                content.add(Boolean.TRUE);
              }
              currentPos += contentLength;
            }
            // ** SKIP THE NODE AS SELECTION NOT STARTED
          } else {
            currentPos += contentLength;
          }
        }
        return currentPos;
      }

      // Within the text node, either the first or the last part have to be deleted (best
      // selectable)
      int delete(Node currentNode, int currentPos, int posStart, int posEnd, List deleteStatus) {

        if (currentNode != null) {
          Integer nextSplitPos;
          // if the start is equal to the first position avoid the first split
          if (currentPos >= posStart) {
            // we are about to gather content in our span
            nextSplitPos = posEnd;
          } else {
            // we have not reached the area to mark
            nextSplitPos = posStart;
          }
          // ** GET NODE SIZE
          Integer contentLength = getNodeWidth(currentNode);

          // addChild the middle part of the currentNode ==> if the currentNode already starts
          // within the span, but the end is not within the span
          boolean inExecutionMode =
              currentPos >= posStart
                  && contentLength + currentPos > posStart
                  && currentPos + contentLength <= posEnd;
          // split the currentNode ==> if the next split position is within the currentNode
          // We need to search to the next found component, as all unknown components will be
          // deleted after the known
          boolean needsSplit =
              currentPos + contentLength > nextSplitPos && nextSplitPos - currentPos != 0;

          // APPLY ACTION TO PARTS (here delete them)
          if (inExecutionMode) {
            OdfElement parent = (OdfElement) currentNode.getParentNode();
            parent.removeChild(currentNode);
            deleteStatus.add(0, Boolean.TRUE);

            int childCount = parent.countDescendantComponents();
            // remove empty boilerplate
            while (!parent.isComponentRoot() && childCount == 0) {
              OdfElement grandParent = (OdfElement) parent.getParentNode();
              if (grandParent instanceof OfficeBodyElement) {
                break;
              }
              String styleName =
                  parent.getAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "style-name");
              // if we are about to remove the last text span of a paragraph..
              if ((parent instanceof TextSpanElement
                  && (grandParent instanceof TextParagraphElementBase)
                  && childCount == 0
                  && grandParent.getChildNodes().getLength() == 1
                  && styleName != null)) {
                copyTextProperties(styleName, grandParent);
              }
              grandParent.removeChild(parent);
              parent = grandParent;
              childCount = parent.countDescendantComponents();
            }
            currentPos += contentLength;

            // SPLIT THE NODE
          } else if (needsSplit) {
            int secondPartLength = 1;
            Node secondPart = null;
            if (currentNode instanceof Text) {
              // splitCursor is the first character of second part (counting starts with 0)
              secondPart = ((Text) currentNode).splitText(nextSplitPos - currentPos);
              secondPartLength = ((Text) secondPart).getLength();
            } else {
              // handle span split...
              secondPart = ((OdfElement) currentNode).split(nextSplitPos - currentPos);
              secondPartLength = OdfElement.getNodeWidth(secondPart);
            }
            boolean reachedStartPosition = nextSplitPos != posEnd;
            if (reachedStartPosition) {
              // if the second split is still in the same currentNode..
              // if the end of the cut is equal to the end of the span
              if (currentPos + (contentLength - secondPartLength) == posEnd) {
                OdfElement parent = (OdfElement) currentNode.getParentNode();
                parent.removeChild(secondPart);
                deleteStatus.add(0, Boolean.TRUE);
                // remove empty boilderplate
                while (!parent.isComponentRoot() && parent.countDescendantComponents() == 0) {
                  OdfElement grandParent = (OdfElement) parent.getParentNode();
                  if (grandParent instanceof OfficeBodyElement) {
                    break;
                  }
                  grandParent.removeChild(parent);
                  parent = grandParent;
                }
                currentPos += contentLength;
              } else {
                // position changed based on the first cut part
                currentPos = currentPos + (contentLength - secondPartLength);
                currentPos = delete(secondPart, currentPos, posStart, posEnd, deleteStatus);
              }
            } else {
              OdfElement parent = (OdfElement) currentNode.getParentNode();
              parent.removeChild(currentNode);
              deleteStatus.add(0, Boolean.TRUE);
              // remove empty boilderplate
              while (!parent.isComponentRoot() && parent.countDescendantComponents() == 0) {
                OdfElement grandParent = (OdfElement) parent.getParentNode();
                if (grandParent instanceof OfficeBodyElement) {
                  break;
                }
                grandParent.removeChild(parent);
                parent = grandParent;
              }
              currentPos += contentLength - secondPartLength;
            }
            // SKIP THE NODE AS SELECTION NOT STARTED
          } else {
            currentPos += contentLength;
          }
        }
        return currentPos;

        //				Integer contentLength = node.getLength();
        //				// see if targetposition (either start or end) is within this text node
        //				if (currentPos + contentLength >= currentTargetPos) {
        //					// If the delete pos is this complete text node and it is already past startPos,
        // delete node
        //					if (currentPos + contentLength == currentTargetPos && textPosEnd == currentTargetPos)
        // {
        //						node.getParentNode().removeChild(node);
        //					} else { // if delete position is within the text node
        //						Integer splitPosition = null;
        //						splitPosition = currentTargetPos - currentPos;
        //						Text secondPart = node.splitText(splitPosition);
        //						//if position within text was the startNode, only delete second part
        //						if (currentTargetPos == textPosStart) {
        //							// if the end position is in the the same text node as the start position
        //							if (currentPos + contentLength >= textPosEnd) {
        //								// split the string once more
        //								secondPart.splitText(textPosEnd - splitPosition - currentPos);
        //							}
        //							node.getParentNode().removeChild(secondPart);
        //						} else {//if we had been in the delete mode, only delete first part
        //							// node has become the firstNode after split
        //							node.getParentNode().removeChild(node);
        //						}
        //					}
        //				} else {
        //					// if we are in the deletion mode
        //					if (currentTargetPos == textPosEnd) {
        //						// delete the complete node
        //						node.getParentNode().removeChild(node);
        //					}
        //				}
        //				return currentPos += contentLength;
      }

      /** Copies the text properties from the given style name to the style of the target element */
      private void copyTextProperties(String sourceStyleName, OdfElement targetElement) {
        // we move the text style properties from the span to the paragraph--
        OdfOfficeAutomaticStyles autoStyles = null;
        // the automatic styles are in a spreadsheet always in the content.xml, only when a table is
        // in a header/footer it would be in the styles.xml (latter we do not support)
        if (targetElement.ownerDocument instanceof OdfContentDom) {
          autoStyles = ((OdfContentDom) targetElement.ownerDocument).getAutomaticStyles();
        } else { // if the span is in a header/footer the element is in the styles.xml (part of the
          // master page style)
          autoStyles = ((OdfStylesDom) targetElement.ownerDocument).getAutomaticStyles();
        }
        OdfStyle spanStyle = autoStyles.getStyle(sourceStyleName, OdfStyleFamily.Text);
        if (spanStyle != null) {
          OdfStylePropertiesBase textProps =
              spanStyle.getPropertiesElement(OdfStylePropertiesSet.TextProperties);
          if (textProps != null
              && textProps.attributes != null
              && textProps.attributes.getLength() > 0) {
            StyleStyleElement paraStyle =
                ((TextParagraphElementBase) targetElement).getOrCreateUnqiueAutomaticStyle();
            OdfStylePropertiesBase paraTextProps =
                paraStyle.getPropertiesElement(OdfStylePropertiesSet.TextProperties);
            if (paraTextProps == null) {
              paraTextProps =
                  ((OdfFileDom) targetElement.ownerDocument)
                      .newOdfElement(StyleTextPropertiesElement.class);
              paraStyle.appendChild(paraTextProps);
            }
            for (int i = 0; i < textProps.attributes.getLength(); i++) {
              Attr attr = (Attr) textProps.attributes.item(i);
              String ns = attr.getNamespaceURI();
              String prefix = attr.getPrefix();
              String localName = attr.getLocalName();
              paraTextProps.setAttributeNS(ns, prefix + ':' + localName, attr.getValue());
            }
          }
        }
      }

      private int receive(
          Node currentNode, int currentPos, int posStart, int posEnd, ArrayList newNodeContainer) {
        if (currentNode != null) {

          // ** GET NODE SIZE
          Integer contentLength = 1; // component default is 1
          if (currentNode instanceof Text) {
            contentLength = ((Text) currentNode).getLength();
          } else {
            // get size from component
            contentLength = ((OdfElement) currentNode).getRepetition();
          }

          // if the current node is selected
          if (currentPos == posStart && (contentLength == 1 || currentNode instanceof Text)) {
            newNodeContainer.add(currentNode);
            currentPos = posEnd;
          } else if (currentPos + contentLength
              > posStart) { // else if only a part of the text node is selected
            Node secondPart = null;
            if (currentNode instanceof Text) {
              // splitCursor is the first character of second part (counting starts with 0)
              secondPart = ((Text) currentNode).splitText(posStart - currentPos);
              newNodeContainer.add(secondPart);
            } else {
              // handle component split...
              Node thirdPart = null;
              Node parent = ((OdfElement) currentNode).getParentNode();
              secondPart = ((OdfElement) currentNode).split(posStart - currentPos);
              if (((OdfElement) secondPart).getRepetition() > 1) {
                thirdPart = ((OdfElement) secondPart).split(1);
              }
              newNodeContainer.add(secondPart);
            }
            currentPos = posEnd;
            // ** SKIP THE NODE AS SELECTION NOT STARTED
          } else {
            currentPos += contentLength;
          }
        }
        return currentPos;
      }

      private void moveChildrenTo(
          Node currentNode, int currentPos, int posStart, int posEnd, Element newParent) {
        if (currentNode != null) {
          newParent.appendChild(currentNode);
        }
      }

      /**
       * @return in opposite of all other algorithms the currentPos is being used to count the
       *     number of components
       */
      private int count(Node currentNode, int currentPos, Boolean isTextCounting) {
        if (currentNode != null) {

          // ** GET NODE SIZE
          Integer contentLength = 1; // component default is 1
          if (!(currentNode instanceof Text)) {
            // get size from component
            contentLength = ((OdfElement) currentNode).getRepetition();
          } else if (isTextCounting == null || isTextCounting) {
            if (((Text) currentNode).toString().trim().length() > 0) {
              contentLength = ((Text) currentNode).getLength();
            }
          }
          currentPos += contentLength;
        }
        return currentPos;
      }
    }
  }

  /**
   * Returns if the text should be returned or is under a nested paragraph or ignored text element
   * (e.g. text:note-citation).
   */
  private static boolean isIgnoredText(Text text) {
    boolean isIgnored = true;
    Node parentNode = text.getParentNode();
    if (parentNode instanceof OdfElement) {
      isIgnored = isIgnoredElement((OdfElement) parentNode);
    }
    return isIgnored;
  }

  private static boolean isIgnoredElement(OdfElement element) {
    boolean isIgnored = true;
    if (!element.mIsIgnoredComponent) {
      while (element != null) {
        if (element.isComponentRoot() && !element.selfAndDescendantTextIgnoredAsComponent()) {
          isIgnored = false;
          break;
        } else {
          if (element.selfAndDescendantTextIgnoredAsComponent()) {
            isIgnored = true;
            break;
          } else {
            Node parent = element.getParentAs(OdfElement.class);
            if (parent instanceof OdfElement) {
              isIgnored = isIgnoredElement((OdfElement) parent);
            }
            break;
          }
        }
      }
    }
    return isIgnored;
  }

  private static int getContentSize(Node currentNode) {
    int contentLength = 0; // by default there is no length

    Node nextChild = currentNode.getFirstChild();
    while (nextChild != null) {
      contentLength += getNodeWidth(nextChild);
      nextChild = nextChild.getNextSibling();
    }
    return contentLength;
  }

  //	public void addReference(String url, int startPos, int endPos){
  //		if (url != null && !url.isEmpty() && !url.equals("null")) {
  //			OdfFileDom xmlDoc = (OdfFileDom) this.getOwnerDocument();
  //
  //			// 2DO: can I reuse an anchor?
  //			TextAElement anchor = new TextAElement(xmlDoc);
  //			anchor.setXlinkHrefAttribute(url);
  //			anchor.setXlinkTypeAttribute("simple");
  //
  //			// 2DO: ONLY remove all text:a descendants from the selection, there is a function parameter
  // missing?!
  //			NodeList anchors = this.getElementsByTagNameNS(OdfDocumentNamespace.TEXT.getUri(), "a");
  //			int anchorCount = anchors.getLength();
  //			for (int i = anchorCount - 1; i >= 0; i--) {
  //				TextAElement a = (TextAElement) anchors.item(i);
  //				OdfElement.removeSingleElement(a);
  //			}
  //			Element parent = (Element) this.getParentNode();
  //			if (parent != null) {
  //				this = (TextSpanElement) parent.replaceChild(anchor, this);
  //				anchor.appendChild(this);
  //			} else {
  //				((OdfElement) this.getParentNode()).markText(startPos, endPos, anchor, attrs);
  //			}
  //
  //			// NOTE: Calc Issue workaround: No span within anchors possible in OpenOffice CALC (no text
  // will be shown)
  //			if (xmlDoc.getDocument() instanceof OdfSpreadsheetDocument) {
  //				NodeList innerSpans = anchor.getElementsByTagNameNS(OdfDocumentNamespace.TEXT.getUri(),
  // "span");
  //				int spanCount = innerSpans.getLength();
  //				for (int i = spanCount - 1; i >= 0; i--) {
  //					TextSpanElement innerSpan = (TextSpanElement) innerSpans.item(i);
  //					OdfElement.removeSingleElement(innerSpan);
  //				}
  //			}
  //		}
  //		// if any hyperlink should be removed!
  //		if (url == null) {
  //			// remove all text:a descendants
  //			NodeList anchors = this.getElementsByTagNameNS(OdfDocumentNamespace.TEXT.getUri(), "a");
  //			int anchorCount = anchors.getLength();
  //			for (int i = anchorCount - 1; i >= 0; i--) {
  //				TextAElement a = (TextAElement) anchors.item(i);
  //				OdfElement.removeSingleElement(a);
  //			}
  //		}
  //	}
  /**
   * @returns the <text:a> element to apply the new format. If created it will be stored within the
   *     spanContainer
   */
  private static TextAElement getAnchorElement(
      Node content, OdfFileDom ownerDoc, String url, Map<OdfName, OdfElement> formatElementHolder) {
    TextAElement newAnchor = null;
    if (formatElementHolder != null) {
      if (formatElementHolder.containsKey(TextAElement.ELEMENT_NAME)) {
        newAnchor = (TextAElement) formatElementHolder.get(TextAElement.ELEMENT_NAME);
      } else {
        if (content instanceof TextAElement) {
          newAnchor = (TextAElement) content;
          newAnchor.setXlinkHrefAttribute(url);
        } else {
          newAnchor = createAnchorElement(ownerDoc, url);
        }

        // we keep the anchor for following siblings
        formatElementHolder.put(TextAElement.ELEMENT_NAME, newAnchor);
      }
    }
    return newAnchor;
  }

  /** @returns a new Anchor element with the changes as new format */
  // ToDo: Should be moved as static creation function to the span element class!!
  private static TextAElement createAnchorElement(OdfFileDom ownerDoc, String url) {
    TextAElement containerElement = new TextAElement(ownerDoc);
    containerElement.setXlinkHrefAttribute(url);
    return containerElement;
  }

  /**
   * @returns the span element to apply the new format. If created it will be stored within the
   *     spanContainer
   */
  private static TextSpanElement getSpanElement(
      OdfFileDom ownerDoc, JSONObject formatChanges, Map<OdfName, OdfElement> formatElementHolder) {
    TextSpanElement newNode = null;
    if (formatElementHolder != null) {
      if (formatElementHolder.containsKey(TextSpanElement.ELEMENT_NAME)) {
        newNode = (TextSpanElement) formatElementHolder.get(TextSpanElement.ELEMENT_NAME);
      } else {
        newNode = createSpanElement(ownerDoc, formatChanges);
        // we keep the span for following siblings
        formatElementHolder.put(TextSpanElement.ELEMENT_NAME, newNode);
      }
    }
    return newNode;
  }

  /** @returns a new span element with the changes as new format */
  // ToDo: Should be moved as static creation function to the span element class!!
  private static TextSpanElement createSpanElement(OdfFileDom ownerDoc, JSONObject formatChanges) {
    TextSpanElement spanElement = null;
    spanElement = new TextSpanElement(ownerDoc);
    JsonOperationConsumer.addStyle(formatChanges, spanElement, ownerDoc);
    return spanElement;
  }

  private static int getNodeWidth(Node currentNode) {
    // ** GET NODE SIZE
    int contentLength = 0; // by default there is no length
    // if a text
    if (currentNode instanceof Text) {
      if (!isIgnoredText((Text) currentNode)) {
        contentLength = ((Text) currentNode).getLength();
      }
      // if a component element and NOT ignored
    } else if (currentNode instanceof OdfElement) {
      if (Component.isComponentRoot((OdfElement) currentNode)
          || currentNode instanceof TextSElement) {
        if (isIgnoredElement((OdfElement) currentNode)) {
          contentLength = 0;
        } else {
          // get size from component
          contentLength = ((OdfElement) currentNode).getRepetition();
        }
      } else {
        Node nextChild = ((OdfElement) currentNode).firstChild;
        while (nextChild != null) {
          contentLength += getNodeWidth(nextChild);
          nextChild = nextChild.getNextSibling();
        }
      }
    }
    return contentLength;
  }

  /** Copy attributes from one element to another, existing attributes will be overwritten */
  public static void copyAttributes(OdfElement from, OdfElement to) {
    NamedNodeMap attributes = from.getAttributes();
    for (int i = 0; i < attributes.getLength(); i++) {
      Attr node = (Attr) attributes.item(i);
      to.setAttributeNS(node.getNamespaceURI(), node.getNodeName(), node.getValue());
    }
  }

  /** @return the first child element of a given parent */
  public Element getChildElement(String uri, String localName) {
    return getChildElement(uri, localName, 0);
  }

  /** @return the child element of a given parent from a given position (starting with 0) */
  public Element getChildElement(String uri, String localName, int position) {
    NodeList childList = this.getElementsByTagNameNS(uri, localName);
    return (Element) childList.item(position);
  }

  public static boolean isIgnoredElement(Element element) {
    return isIgnoredElement(element.getNamespaceURI(), element.getLocalName());
  }

  /**
   * ToDo: Move away to parser, as it is application logic not ODF relevant! Elements that blocks
   * the creation of operations due to implementation issues
   */
  public static boolean isIgnoredElement(String uri, String localName) {
    boolean isIgnored = false;
    if (uri != null && uri.equals(TextNoteElement.ELEMENT_NAME.getUri())) {
      // text:note
      if (localName.equals(TextNoteElement.ELEMENT_NAME.getLocalName())) {
        isIgnored = true;
      }
    }
    if (uri != null && uri.equals(TextTrackedChangesElement.ELEMENT_NAME.getUri())) {
      // text:tracked-changes
      if (localName.equals(TextTrackedChangesElement.ELEMENT_NAME.getLocalName())) {
        isIgnored = true;
      }
    }
    if (uri != null && uri.equals(TableShapesElement.ELEMENT_NAME.getUri())) {
      // table:shapes
      if (localName.equals(TableShapesElement.ELEMENT_NAME.getLocalName())) {
        isIgnored = true;
      }
      // table:covered-table-cell
      if (localName.equals(TableCoveredTableCellElement.ELEMENT_NAME.getLocalName())) {
        isIgnored = true;
      }
    }
    if (uri != null && uri.equals(TableContentValidationsElement.ELEMENT_NAME.getUri())) {
      // table:content-validations
      if (localName.equals(TableContentValidationsElement.ELEMENT_NAME.getLocalName())) {
        isIgnored = true;
      }
    }
    if (uri != null && uri.equals(StyleHandoutMasterElement.ELEMENT_NAME.getUri())) {
      // style:handout-master
      if (localName.equals(StyleHandoutMasterElement.ELEMENT_NAME.getLocalName())) {
        isIgnored = true;
      }
    }
    return isIgnored;
  }

  /** @returns the next element sibling of the given node or null if none exists */
  public static OdfElement getNextSiblingElement(Node node) {
    OdfElement nextElement = null;
    Node _nextSibling = node.getNextSibling();
    if (_nextSibling instanceof OdfElement) {
      nextElement = (OdfElement) _nextSibling;
    } else if (_nextSibling instanceof Text) {
      nextElement = getNextSiblingElement(_nextSibling);
    }
    return nextElement;
  }

  /** @returns the next element sibling of the given node or null if none exists */
  public static OdfElement getPreviousSiblingElement(Node node) {
    OdfElement previousElement = null;
    Node _previousElement = node.getPreviousSibling();
    if (_previousElement instanceof OdfElement) {
      previousElement = (OdfElement) _previousElement;
    } else if (_previousElement instanceof Text) {
      previousElement = getPreviousSiblingElement(_previousElement);
    }
    return previousElement;
  }

  /** @returns the first element child of the this or null if none exists */
  public OdfElement getFirstChildElement() {
    OdfElement firstElementChild = null;
    NodeList nodeList = this.getChildNodes();
    Node node = nodeList.item(0);
    if (node != null) {
      if (node instanceof OdfElement) {
        firstElementChild = (OdfElement) node;
      } else {
        firstElementChild = getNextSiblingElement(node);
      }
    }
    return firstElementChild;
  }

  /** @returns the last element child of the this or null if none exists */
  public OdfElement getLastChildElement() {
    OdfElement lastElementChild = null;
    NodeList nodeList = this.getChildNodes();
    Node node = nodeList.item(0);
    for (int i = nodeList.getLength(); i >= 0; i--) {
      if (node instanceof OdfElement) {
        lastElementChild = (OdfElement) node;
        break;
      }
    }
    return lastElementChild;
  }

  public int countPrecedingSiblingElements() {
    int i = 0;
    Node node = this.getPreviousSibling();
    while (node != null) {
      node = node.getPreviousSibling();
      if (node instanceof Element) {
        i++;
      }
    }
    return i;
  }

  // ToDo: Move this to a intermediate class, e.g. ComponentRootElement
  /** @return the component size of a heading, which is always 1 */
  public void setRepetition(int repetition) {
    // does not work for all classes
  }

  /**
   * @return the concatenated text contained by itself and all descendants (subtree). Does not take
   *     into account indented XML, as ODF whitespace handling require to neglect preceeding text
   *     content
   */
  public String getTextContent() {
    StringBuilder buffer = new StringBuilder();
    NodeList nodeList = this.getChildNodes();
    int i;
    for (i = 0; i < nodeList.getLength(); i++) {
      Node node;
      node = nodeList.item(i);
      if (node.getNodeType() == Node.TEXT_NODE) {
        buffer.append(node.getNodeValue());
      } else if (node.getNodeType() == Node.ELEMENT_NODE) {
        if (node instanceof TextSpanElement) {
          buffer.append(((TextSpanElement) node).getTextContent());
        } else if (node.getNodeName().equals("text:s")) {
          Integer count = ((TextSElement) node).getTextCAttribute();
          for (int j = 0; j < (count != null ? count : 1); j++) buffer.append(' ');
        } else if (node.getNodeName().equals("text:tab")) buffer.append('\t');
        else if (node.getNodeName().equals("text:line-break")) {
          String lineseperator = System.getProperty("line.separator");
          buffer.append(lineseperator);
        } else if (node.getNodeName().equals("text:a"))
          buffer.append(((TextAElement) node).getTextContent());
      }
    }
    return buffer.toString();
  }
}