# ShadowRemapper.java

    /*
    ** Module   :ShadowRemapper.java
    ** Abstract :Back-end for tracking batch edit session changes.
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
    ** 001 SIY 20050506   @21205 Created initial version.
    ** 002 SIY 20050524   @21261 Fixed moving nodes.
    ** 003 SIY 20090514   @42180 Added refresh() method, some minor cleanups.
    ** 004 SIY 20090531   @42503 Reworked and cleaned up implementation. Fixed
    **                           node deletion issue.
    ** 005 SIY 20090712   @43138 Fixed moving nodes. Also, changes now are applied
    **                           in logically consistent order to make sure that
    **                           all prerequisites are satisfied.
    ** 006 SIY 20090910   @43867 Fixed chained node moves of nodes at the same
    **                           parent.
    ** 007 SIY 20091007   @44126 Committing changes is moved from DirectoryService
    **                           to this class. Completely reworked entire
    **                           committing algorithm to make it as safe as
    **                           possible in regard to possible directory corruption.
    **                           Moved rollback algorithm into this class.
    ** 008 HC  20170612          More logging added.
    ** 009 TJD 20220504          Java 11 compatibility minor changes
    ** 010 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 011 SP  20250416          Used CaseInsensitive maps for changed, removed, moved. Removed toLowerCase().
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.directory;

    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.util.*;

    import java.util.*;
    import java.util.function.*;

    /**
     * This class implements an <code>Remapper</code> interface which is used to
     * hold changes made by application during batch editing session. It uses real
     * back-end to retrieve unchanged data and holds changed data in memory. When
     * application accesses the data they are present as if all changes are
     * actually made in the real back-end.
     * <p>
     * General concept of used approach is rather simple: hold all changes which
     * are necessary to convert backend into new state. While changes are not
     * applied, information about these changes is used to handle requests so
     * users of this class see backend as if all changes are already applied.
     * <p>
     * Internally all changes are stored in three maps - changed nodes, removed
     * nodes and moved nodes. Changed nodes are those which were created or their
     * content was changed. Removed nodes contains all nodes which were explicitly
     * or implicitly (for example by moving) removed. And moved nodes map contains
     * nodes which were moved on user request to the new location.
     * <p>
     * When changes are applied implementation performs operations in particular
     * order which guarantees that operations are compatible. For example, when it
     * moves node to the new location it adds all necessary intermediate nodes
     * present in changed nodes map or when subtree is deleted, leaf nodes are
     * deleted first, then deleted their parents, then parents of parents and so
     * forth.
     *
     * @author SIY
     * @version 1.0
     */

    class ShadowRemapper
    implements Remapper
    {
       /** The logger instance. */
       private static final CentralLogger LOG = CentralLogger.get(ShadowRemapper.class);

       /** Object which is used for instance data access synchronization */
       protected final Object mutex = new Object();

       /** Real back-end with data */
       private Remapper backend = null;

       /** Changed nodes */
       private Map<String, DirNode> changed = new CaseInsensitiveHashMap<>();

       /** Removed nodes */
       private Map<String, DirNode> removed = new CaseInsensitiveHashMap<>();

       /** Moved nodes */
       private Map<String, String> moved = new CaseInsensitiveLinkedHashMap<>();

       /**
        * Construct an instance of the XmlRemapper for a given file.
        *
        * @param   backend
        *          Real back-end with data.
        */
       ShadowRemapper(Remapper backend)
       {
          this.backend = backend;
       }

       /**
        * Logs a warning message.
        *
        * @param   nodeId
        *          Node name that will become part of the resulting warning message.
        * @param   msg
        *          The message to log.
        */
       private static void logWarning(String nodeId, String msg)
       {
          LOG.warning(msg + " [nodeId=" + nodeId + "]");
       }

       /**
        * Add new node of specified class with specified ID. For node
        * initialization list of attributes and corresponding list of attribute
        * values should be provided. The presence of the mandatory attributes in
        * the lists is obligatory.
        *
        * @param   nodeId
        *          Node name.
        * @param   nodeClass
        *          Node class name.
        * @param   names
        *          A list of names of the attributes.
        * @param   values
        *          A list of values of the attributes.
        *
        * @return  <code>true</code> if operation was successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNode(String nodeId, String nodeClass, String[] names,
                              Object[] values)
       {
          nodeId = IdUtils.normalize(nodeId);
          String[] path = IdUtils.splitId(nodeId);

          Consumer<String> trace = msg -> LOG.fine("Failed to add node. " + msg);

          if (path == null)
          {
             trace.accept("Invalid node path.");
             return false;
          }

          DirNode node = locateNode(path[0], false);
          if (node == null)
          {
             trace.accept("Parent does not exist.");
             return false;  //Parent does not exists
          }

          node = locateNode(nodeId, false);
          if (node != null)
          {
             trace.accept("A node of the same path already exists.");
             return false;  //Already exists
          }

          node = validateNode(path[1], nodeClass, names, values);
          if (node == null)
          {
             LOG.warning("The node is not valid.");
             return false;
          }

          //Load intermediate nodes
          locateNode(path[0], true);

          synchronized (mutex)
          {
             DirNode tmpNode = changed.get(path[0]);
             if (tmpNode != null)
             {
                tmpNode.addChild(path[1]);
             }

             removed.remove(nodeId);       //Clear node just in case
             changed.put(nodeId, node);    //Add it to list of changed
          }

          return true;
       }

       /**
        * Add new <code>BitField</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeBitField(String nodeId, String name, BitField value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>BitSelector</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeBitSelector(String nodeId, String name,
                                         BitSelector value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>boolean</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeBoolean(String nodeId, String name, boolean value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>byte array</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeByteArray(String nodeId, String name, byte[] value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>DateValue</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeDate(String nodeId, String name, DateValue value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>double</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeDouble(String nodeId, String name, double value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>int</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeInteger(String nodeId, String name, int value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>String</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeString(String nodeId, String name, String value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Add new <code>TimeValue</code> value to the attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean addNodeTime(String nodeId, String name, TimeValue value)
       {
          return addNodeValue(nodeId, name, value);
       }

       /**
        * Initialise back-end and open session.
        *
        * @return  <code>true</code> if operation was successful.
        */
       public boolean bind()
       {
          return true;
       }

       /**
        * No-op implementation. This class does not support refreshing and always
        * assumes that refreshing was successful.
        *
        * @return  <code>true</code> if operation was successful.
        */
       public boolean refresh()
       {
          return true;
       }

       /**
        * Remove specified node from the directory (unless node has children).
        *
        * @param   nodeId
        *          Node identifier.
        *
        * @return  <code>true</code> if operation was successful and
        *          <code>false</code> otherwise.
        */
       public boolean deleteNode(String nodeId)
       {
          String finalNodeId = nodeId;
          Consumer<String> trace = msg -> LOG.fine("Failed to delete node '" + finalNodeId + "'. " + msg);

          //Check if node is already in the list
          DirNode node = locateNode(nodeId, false);
          if (node == null)
          {
             trace.accept("It could not be located.");
             return false;
          }

          nodeId = IdUtils.normalize(nodeId);
          if (node.getChildCount() != 0)
          {
             // node can be deleted if all its child nodes also listed for deletion

             // Note, that we don't need to check child nodes recursively, because
             // they also can be marked for deletion only if their child nodes are
             // ready for deletion.

             String[] childNames = node.getChildNames();
             for (String child : childNames)
             {
                if(!removed.containsKey(nodeId + "/" + child))
                {
                   trace.accept("The child node '" + child + "' is not marked for deletion.");
                   return false;
                }
             }
          }

          synchronized (mutex)
          {
             String[] path = IdUtils.splitId(nodeId);
             if (path != null)
             {
                DirNode tmpNode = changed.get(path[0]);

                if (tmpNode != null)
                {
                   tmpNode.removeChild(path[1]);
                }
             }
             changed.remove(nodeId);
             removed.put(nodeId, node);
          }

          return true;
       }

       /**
        * Remove whole node attribute. Note that call will fail if attribute is
        * marked mandatory.
        *
        * @param   nodeId
        *          Node name.
        * @param   name
        *          Attribute name.
        *
        * @return  <code>true</code> if operation was successful and
        *          <code>false</code> otherwise.
        */
       public boolean deleteNodeAttribute(String nodeId, String name)
       {
          //Check if node is already in the list
          DirNode node = locateNode(nodeId, true);
          if (node == null)
          {
             return false;
          }

          return node.removeAttribute(name);
       }

       /**
        * Remove particular value from the node attribute. Note that removing last
        * value from the attribute deletes entire attribute unless attribute is
        * marked mandatory.
        *
        * @param   nodeId
        *          Node name.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Attribute value index.
        *
        * @return  <code>true</code> if operation was successful and
        *          <code>false</code> otherwise.
        */
       public boolean deleteNodeAttributeValue(String nodeId, String name,
                                               int index)
       {
          //Check if node is already in the list
          DirNode node = locateNode(nodeId, true);
          if (node == null)
          {
             return false;
          }
          Attribute attr = node.getAttribute(name);
          if (attr == null)
          {
             return false;
          }
          if (!attr.deleteValue(index))
          {
             return false;
          }
          if (attr.getCount() == 0)
          {
             return node.removeAttribute(name);
          }
          return true;
       }

       /**
        * Return list of all node attribute definitions.
        *
        * @param   nodeId
        *          Node for which list of attributes is requested.
        *
        * @return  Array of attribute definitions.
        */
       public AttributeDefinition[] enumerateNodeAttributes(String nodeId)
       {
          synchronized (mutex)
          {
             DirNode node = locateNode(nodeId, true);
             if (node == null)
             {
                return null;
             }

             AttributeDefinition[] src = node.getNodeClass().getClassDefinition();
             if (src == null)
             {
                return src;
             }

             //Create a copy of the array but not elements.
             //There is no need to copy elements since
             //AttributeDefinition is "read only".
             AttributeDefinition[] res = new AttributeDefinition[src.length];
             for (int i = 0; i < src.length; i++)
             {
                res[i] = src[i];
             }

             return res;
          }
       }

       /**
        * Get direct list of children nodes for current node.
        *
        * @param   nodeId
        *          Node for which list of nodes is requested.
        *
        * @return  An array of names of child nodes.
        */
       public String[] enumerateNodes(String nodeId)
       {
          synchronized (mutex)
          {
             DirNode node = locateNode(nodeId, true);
             if (node == null)
             {
                return null;
             }

             return node.getChildNames();
          }
       }

       /**
        * Get definitions for all class attributes.
        *
        * @param   className
        *          Name of the class.
        *
        * @return  An array of objects describing class attributes.
        */
       public AttributeDefinition[] getClassDefinition(String className)
       {
          ObjectClass objClass = getObjClass(className);
          if (objClass == null)
          {
             return null;
          }

          return objClass.getClassDefinition();
       }

       /**
        * Get list of names of all defined classes.
        *
        * @return  Array of names of the classes.
        */
       public String[] getClassNames()
       {
          return SchemaStorage.getSchema().getClassNames();
       }

       /**
        * Get <code>BitField</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>BitField</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public BitField getNodeBitField(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>BitSelector</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>BitSelector</code> then
        * <code>null</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public BitSelector getNodeBitSelector(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>Boolean</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>Boolean</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public Boolean getNodeBoolean(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>byte array</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>byte array</code> then
        * <code>null</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public byte[] getNodeByteArray(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Return class name for the specified node.
        *
        * @param   nodeId
        *          Node for which class name is requested.
        *
        * @return  Node class name.
        */
       public String getNodeClassName(String nodeId)
       {
          DirNode node = locateNode(nodeId, false);
          if (node == null)
          {
             return null;
          }
          return node.getNodeClass().getName();
       }

       /**
        * Get <code>DateValue</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>DateValue</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public DateValue getNodeDate(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>Double</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>Double</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public Double getNodeDouble(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>Integer</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>Integer</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public Integer getNodeInteger(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>String</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>String</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public String getNodeString(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Get <code>TimeValue</code> attribute value for specified node ID and
        * index. If wrong parameter (node ID, attribute name or index) are passed
        * or attribute type is not <code>TimeValue</code> then <code>null</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  Value of the attribute for the specified attribute name and
        *          index.
        */
       public TimeValue getNodeTime(String nodeId, String name, int index)
       {
          return getNodeAttributeValue(nodeId, name, index);
       }

       /**
        * Class is immutable if the back-end does not allow any change to its
        * state.
        *
        * @param   className
        *          Name of the class.
        *
        * @return  <code>true</code> if class is read-only.
        */
       public boolean isClassImmutable(String className)
       {
          ObjectClass objClass = getObjClass(className);
          if (objClass == null)
          {
             return true;
          }

          return objClass.isClassImmutable();
       }

       /**
        * Class is leaf if no other object can be created under objects of the
        * class. Also known as terminal.
        *
        * @param   className
        *          Name of the class.
        *
        * @return  <code>true</code> if class is leaf.
        */
       public boolean isClassLeaf(String className)
       {
          ObjectClass objClass = getObjClass(className);
          if (objClass == null)
          {
             return false;
          }

          return objClass.isClassLeaf();
       }

       /**
        * Moves or renames the existing object, specified by nodeId to different
        * location. The parent object of the newId should exist and allow for
        * children creation.
        *
        * @param   nodeId
        *          An ID of the node to move.
        * @param   newId
        *          The desired new location of the node.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean moveNode(String nodeId, String newId)
       {
          String finalNodeId = nodeId;
          String finalNewId = newId;
          Consumer<String> trace = msg -> LOG.fine("Failed to move node from '" + finalNodeId +
                                                   "' to '" + finalNewId + "'. ");

          DirNode node = locateNode(nodeId, false);

          if (node == null)
          {
             trace.accept("The source node could not be located.");
             return false;
          }

          String[] newPath = IdUtils.splitId(newId);
          if (newPath == null)
          {
             trace.accept("The target node path is invalid.");
             return false;
          }

          DirNode newParent = locateNode(newPath[0], false);
          if (newParent == null)
          {
             trace.accept("Parent does not exist.");
             return false;
          }

          if (newParent.isLeaf())
          {
             trace.accept("Leaf node is not a valid parent.");
             return false;
          }

          newId = IdUtils.normalize(newId);
          if (newParent.hasChild(newPath[1]) && !removed.containsKey(newId))
          {
             trace.accept("Cannot move to an existing node.");
             return false;
          }

          nodeId = IdUtils.normalize(nodeId);

          synchronized (mutex)
          {
             //Check for existing node
             if (changed.get(newId) != null)
             {
                trace.accept("The target node has pending changes.");
                return false;
             }

             changed.remove(nodeId);
             node.setName(newPath[1]);
             changed.put(newId, node);

             moved.put(newId, nodeId);
             removed.put(nodeId, new DirNode(node));

             // move all child nodes
             List<String> tmp = selectChildNodes(nodeId, changed.keySet());

             if (tmp.size() > 1)
             {
                int sz = nodeId.length();

                // now remove nodes with old names and put them back with new names
                for (String key : tmp)
                {
                   node = changed.remove(key);

                   changed.put(newId + key.substring(sz), node);
                   removed.put(key, new DirNode(node));
                }
             }
          }

          return true;
       }

       /**
        * Set <code>BitField</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeBitField(String nodeId, String name, int index,
                                      BitField value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>BitSelector</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeBitSelector(String nodeId, String name, int index,
                                         BitSelector value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>boolean</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeBoolean(String nodeId, String name, int index,
                                     boolean value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>byte array</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeByteArray(String nodeId, String name, int index,
                                       byte[] value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>DateValue</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeDate(String nodeId, String name, int index,
                                  DateValue value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>double</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeDouble(String nodeId, String name, int index,
                                    double value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>int</code> attribute value for specified node ID, attribute
        * name and index. If wrong parameter is passed or attribute read-only then
        * <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeInteger(String nodeId, String name, int index,
                                     int value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>String</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeString(String nodeId, String name, int index,
                                    String value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Set <code>TimeValue</code> attribute value for specified node ID,
        * attribute name and index. If wrong parameter is passed or attribute
        * read-only then <code>false</code> is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       public boolean setNodeTime(String nodeId, String name, int index,
                                  TimeValue value)
       {
          return setNodeValue(nodeId, name, index, value);
       }

       /**
        * Close session and shutdown back-end.
        *
        * @return  <code>true</code> if operation was successful.
        */
       public boolean unbind()
       {
          return true;
       }

       /**
        * Update external storage. This method is invoked by the DirectoryService
        * to commit the changes made by batch editing into external storage.
        *
        * @return  <code>true</code> if operation was successful.
        */
       public boolean update()
       {
          return true;
       }

       /**
        * Verifies that calling thread is properly bound.
        *
        * @return  <code>true</code> if thread is properly bound.
        */
       protected boolean isBound()
       {
          return true;
       }

       /**
        * Create backup of all affected nodes.
        *
        * @param   baseId
        *          ID of the base node (root) of the backup.
        *
        * @return  <code>true</code> if operation was successful.
        */
       boolean createBackup(String baseId)
       {
          synchronized (mutex)
          {
             List<String> newNames      = new ArrayList<>();
             List<String> newValues     = new ArrayList<>();
             List<String> changedNames  = new ArrayList<>();
             List<String> changedValues = new ArrayList<>();

             //Go through "changed" list and choose new nodes

             for (String nodeId : getChangedList())
             {
                if (backend.getNodeClassName(nodeId) == null &&
                    moved.get(nodeId) == null)
                {
                   newNames.add("values");
                   newValues.add(nodeId);
                }
                else
                {
                   changedNames.add("values");
                   changedValues.add(nodeId);
                }
             }

             //List new nodes

             boolean rc;
             rc = backend.addNode(baseId + "/added", "strings",
                                  toArray(newNames),
                                  toArray(newValues));
             if (!rc)
             {
                return rc;
             }
             //List deleted nodes
             newNames.clear();
             newValues.clear();
             for (String nodeId : getDeletedList())
             {
                newNames.add("values");
                newValues.add(nodeId);
             }
             rc = backend.addNode(baseId + "/deleted", "strings",
                                  toArray(newNames),
                                  toArray(newValues));
             if (!rc)
             {
                return rc;
             }

             //List moved nodes
             newNames.clear();
             newValues.clear();
             for (String nodeId : getMovedList())
             {
                newNames.add("values");
                //<new ID> <old ID>
                newValues.add(nodeId + " " + moved.get(nodeId));
             }
             rc = backend.addNode(baseId + "/moved", "strings",
                                  toArray(newNames),
                                  toArray(newValues));
             if (!rc)
             {
                return rc;
             }
             //List changed nodes
             rc = backend.addNode(baseId + "/changed", "strings",
                                  toArray(changedNames),
                                  toArray(changedValues));
             if (!rc)
             {
                return rc;
             }
             //Save content of the changed nodes
             for (String nodeId : getChangedList())
             {
                DirNode node = loadNode(nodeId);
                if (node == null)
                {
                   if (moved.get(nodeId) == null)
                   {
                      // this is a new node, not added nor moved
                      continue;
                   }
                   if (backend.getNodeClassName(moved.get(nodeId)) == null)
                   {
                      // this is a new node moved on top of new node, there is
                      // no backing data in real backend
                      continue;
                   }
                   node = loadNode(moved.get(nodeId));
                   if (node == null)
                   {
                      return false;
                   }
                }
                //Create subtree, if necessary
                String[] list = IdUtils.listPaths(baseId + "/nodes" + nodeId);

                // skip root and nodeId nodes when creating intermediate nodes
                for (int j = 2; list != null && j < (list.length - 1); j++)
                {
                   String res = backend.getNodeClassName(list[j]);
                   if (res == null)
                   {
                      rc = backend.addNode(list[j], "container", null, null);
                   }
                   //Ignore error here, if something is really wrong
                   //DirectoryService.addNode() will tell us anyway
                }
                rc = DirectoryService.addNode(backend,
                                              baseId + "/nodes" + nodeId,
                                              node.getNodeClass().getName(),
                                              node.getAttributes(), true, false);
                if (!rc)
                {
                   return rc;
                }
             }
          }
          return true;
       }

       /**
        * Perform all steps necessary to safely commit changes into backend.
        *
        * @param    batchId
        *           ID of the processed subtree.
        * @param    backupId
        *           Backup ID.
        *
        * @return   <code>true</code> if operation was successful.
        */
       boolean commit(String batchId, String backupId)
       {
          boolean rc = false;
          String lockId = batchId + "/lock";

          try
          {
             // create backup
             rc = DirectoryService.copySubtree(backend,
                                               batchId,
                                               backend,
                                               backupId,
                                               true);
             if (!rc)
             {
                return rc;
             }

             // create lock

             rc = backend.addNode(lockId, "lock", null, null);
             if (!rc)
             {
                logWarning(lockId, "Unable to commit batch. Failed to add lock node.");
                return rc;
             }

             rc = backend.addNodeString(lockId, "backupId", backupId);
             if (!rc)
             {
                logWarning(lockId, "Unable to commit batch. Failed to set backupId on " +
                                   "the lock node.");
                return rc;
             }

             rc = backend.addNodeBoolean(lockId, "backed-up", true);
             if (!rc)
             {
                logWarning(lockId, "Unable to commit batch. Failed to set backed-up on " +
                                        "the lock node.");
                return rc;
             }

             // commit backed up tree before applying changes
             rc = backend.update();
             if (!rc)
             {
                logWarning(lockId, "Unable to commit batch. Failed to commit external storage.");
                return rc;
             }

             // now we can start applying changes
             rc = applyChanges();
             if (!rc)
             {
                logWarning(lockId, "Unable to commit batch. Failed apply changes.");

                // applying of changes failed, roll back all changes
                rollbackWorker(batchId, backupId);
                backend.update();
                return rc;
             }
             rc = backend.deleteNode(lockId);

             // note: here we can safely ignore result of next operation
             // in worst case we will get orphaned backup
             DirectoryService.pruneSubtree(backend, backupId, false);

             if (!rc)
             {
                // This one is really hard to recover from.
                // Since node removal is failed, most likely removal of
                // backup is failed too. So, all following changes in the
                // subtree will be rolled back during next restore.
                // From the other hand, failure here does mean that
                // backend can't work properly anymore, so, most likely
                // all subsequent updates will fail too.
                // TODO: perhaps we should introduce special "R/O" mode
                // switch for backend in order to prevent further updates
                // and saving of (potentially incorrect) content.
                logWarning(lockId, "Unable to commit batch. Failed to delete lock node.");
                return rc;
             }

             rc = backend.update();
             if (!rc)
             {
                logWarning(lockId, "Unable to commit batch. Failed to commit external storage.");

                // commit failed, roll back all changes
                rollbackWorker(batchId, backupId);
                return rc;
             }
          }
          finally
          {
             // ignore errors here
             backend.deleteNode(lockId);
             DirectoryService.pruneSubtree(backend, backupId, false);
          }
          return rc;
       }

       /**
        * Roll back the changes made in source directory, using backup subtree as
        * an authoritative source of information. If rollback operation is
        * successful, then backend is updated.
        *
        * @param    batchId
        *           ID of the processed subtree.
        * @param    backupId
        *           ID of the backup.
        *
        * @return   <code>true</code> if rollback was successful.
        */
       boolean rollback(String batchId, String backupId)
       {
          boolean rc = rollbackWorker(batchId, backupId);
          if (!rc)
          {
             return rc;
          }

          // note that successful rollback will remove /lock node
          // so, cleanup should only remove backup
          rc = DirectoryService.pruneSubtree(backend, backupId, false);
          if (rc)
          {
             rc = backend.update();
          }

          return rc;
       }

       /**
        * Apply changes to main back-end.
        *
        * @return  <code>true</code> if operation was successful.
        */
       boolean applyChanges()
       {
          synchronized (mutex)
          {
             if (!applyMovedNodes())
             {
                return false;
             }
             if (!applyDeletedNodes(getDeletedList()))
             {
                return false;
             }
             if (!applyChangedNodes())
             {
                return false;
             }
             moved.clear();
             changed.clear();
             removed.clear();
          }
          return true;
       }

       /**
        * Rollback worker method.
        *
        * @param    batchId
        *           Updated subtree.
        * @param    backupId
        *           Backup of the subtree.
        *
        * @return   <code>true</code> if operation is successful.
        */
       private boolean rollbackWorker(String batchId, String backupId)
       {
          // check attributes
          Attribute[] attr = DirectoryService.getNodeAttributes(backend, backupId);
          boolean rc = DirectoryService.setNodeAttributes(backend, batchId, attr);
          if (!rc)
          {
             return rc;
          }
          // check child nodes
          String[] nodes = backend.enumerateNodes(backupId);
          if (nodes != null)
          {
             Set<String> saved = new CaseInsensitiveHashSet<>(nodes.length);
             saved.addAll(Arrays.asList(nodes));
             Set<String> current = new CaseInsensitiveHashSet<>();

             nodes = backend.enumerateNodes(batchId);
             if (nodes != null)
             {
                current.addAll(Arrays.asList(nodes));
             }
             for(String name : saved)
             {
                if (current.contains(name))
                {
                   rc = rollbackWorker(batchId + "/" + name, backupId + "/" + name);
                   current.remove(name);
                }
                else
                {
                   rc = DirectoryService.copySubtree(backend,
                                                     backupId + "/" + name,
                                                     backend,
                                                     batchId + "/" + name,
                                                     true);
                }
                if (!rc)
                {
                   return rc;
                }
             }

             if (!current.isEmpty())
             {
                for (String name : current)
                {
                   rc = DirectoryService.pruneSubtree(backend,
                                                      batchId + "/" + name,
                                                      false);
                   if (!rc)
                   {
                      return rc;
                   }
                }
             }
          }
          return true;
       }

       /**
        * Apply moved nodes to backend.
        *
        * @return   <code>true</code> if operation was successful and
        *           <code>false</code> otherwise.
        */
       private boolean applyMovedNodes()
       {
          for (String nodeId : getMovedList())
          {
             List<String> tmp = selectChildNodes(nodeId, getDeletedList());
             if (!tmp.isEmpty())
             {
                // Collection is sorted in ascending order,
                // so we must reverse it before use.
                Collections.reverse(tmp);

                if (!applyDeletedNodes(tmp))
                {
                   return false;
                }

                removed.keySet().removeAll(tmp);
             }

             String[] newIds = IdUtils.listPaths(nodeId);

             // make sure that all intermediate nodes are present
             for (String id : newIds)
             {
                // do not process last node
                if (id.equals(nodeId))
                {
                   break;
                }
                // Note that destination is already checked and preloaded so
                // it must be present in list of changed nodes and we don't need
                // to call backend.
                if (changed.containsKey(id) && !removed.containsKey(id))
                {
                   if (!addChangedNode(id))
                   {
                      return false;
                   }
                   // since we performed this operation we don't need to repeat
                   // it again when we will be applying changed nodes
                   changed.remove(id);
                }
             }

             // move node at backend only if it really exists, otherwise
             // it is present in changed list and will be added later
             if (backend.getNodeClassName(moved.get(nodeId)) != null)
             {
                if (!backend.moveNode(moved.get(nodeId), nodeId))
                {
                   return false;
                }
             }
          }

          return true;
       }

       /**
        * Removed specified nodes to backend.
        *
        * @param   forDelete
        *          Collection of nodes which need to be deleted. Note that
        *          collection must be in proper order - child nodes must be
        *          before their parents.
        *
        * @return   <code>true</code> if operation was successful and
        *           <code>false</code> otherwise.
        */
       private boolean applyDeletedNodes(Collection<String> forDelete)
       {
          for (String nodeId : forDelete)
          {
             if (backend.getNodeClassName(nodeId) != null)
             {
                if (!backend.deleteNode(nodeId))
                {
                   logWarning(nodeId, "Unable to delete node.");
                   return false;
                }
             }
          }

          return true;
       }

       /**
        * Apply changed nodes to backend.
        *
        * @return   <code>true</code> if operation was successful and
        *           <code>false</code> otherwise.
        */
       private boolean applyChangedNodes()
       {
          for (String nodeId : getChangedList())
          {
             boolean res = addChangedNode(nodeId);
             if (!res)
             {
                return false;
             }
          }

          return true;
       }

       /**
        * Add node with specified ID from list of changed nodes.
        *
        * @param    nodeId
        *           ID of node to add.
        *
        * @return   <code>true</code> if operation was successful and
        *           <code>false</code> otherwise.
        */
       private boolean addChangedNode(String nodeId)
       {
          DirNode node = changed.get(nodeId);

          return DirectoryService.addNode(backend,
                                          nodeId,
                                          node.getNodeClass().getName(),
                                          node.getAttributes(),
                                          true,
                                          false);
       }

       /**
        * Worker routine for all addNodeXXX() methods.
        *
        * @param    nodeId
        *           Node ID.
        * @param    name
        *           Attribute name.
        * @param    value
        *           Attribute value.
        *
        * @return   <code>true</code> if operation was successful.
        */
       private boolean addNodeValue(String nodeId, String name, Object value)
       {
          //Check if node is already in the list
          DirNode node = locateNode(nodeId, true);
          if (node == null)
          {
             return false;
          }

          return node.addAttributeValue(name, value, false);
       }

       /**
        * Return value of specified variable of node attribute.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        *
        * @return  value of the attribute for the specified attribute name and
        *          index or <code>null</code>.
        */
       @SuppressWarnings("unchecked")
       private <T> T getNodeAttributeValue(String nodeId, String name, int index)
       {
          DirNode node = locateNode(nodeId, false);
          if (node == null)
          {
             return null;
          }

          Attribute attr = node.getAttribute(name);
          if (attr == null)
          {
             return null;
          }

          return (T) attr.getValue(index);
       }

       /**
        * Set attribute value for specified node ID, attribute name and index. If
        * wrong parameter is passed or attribute read-only then <code>false</code>
        * is returned.
        *
        * @param   nodeId
        *          Node identifier.
        * @param   name
        *          Attribute name.
        * @param   index
        *          Index of the value to retrieve.
        * @param   value
        *          New value for the attribute variable.
        *
        * @return  <code>true</code> is operation is successful and
        *          <code>false</code> otherwise.
        */
       private boolean setNodeValue(String nodeId, String name, int index,
                                    Object value)
       {
          DirNode node = locateNode(nodeId, true);
          if (node == null)
          {
             return false;
          }

          return node.setAttributeValue(name, value, index);
       }

       /**
        * Get list of all changed nodes.
        *
        * @return  array of node IDs.
        */
       private Collection<String> getChangedList()
       {
          return new TreeSet<>(changed.keySet());
       }

       /**
        * Get list of all moved nodes.
        *
        * @return  array of node IDs.
        */
       private Collection<String> getMovedList()
       {
          return moved.keySet();
       }

       /**
        * Get list of all deleted nodes.
        *
        * @return  array of node IDs.
        */
       private Collection<String> getDeletedList()
       {
          Set<String> mov = new CaseInsensitiveHashSet<>();
          mov.addAll(moved.values());
          List<String> res = new ArrayList<>();

          for (String key : new TreeSet<>(removed.keySet()))
          {
             if (!mov.contains(key))
             {
                // note that nodes added in reverse order
                res.add(0, key);
             }
          }

          return res;
       }

       /**
        * Find node in the local list of changed nodes or load it from real
        * back-end. If requested, node is stored in the local list of changed
        * nodes.
        *
        * @param   nodeId
        *          Node ID.
        * @param   preload
        *          If this parameter is <code>true</code> then loaded node is
        *          stored in the list of changed nodes.
        *
        * @return  located or loaded node or <code>null</code> if no such node
        *          exists.
        */
       private DirNode locateNode(String nodeId, boolean preload)
       {
          nodeId = IdUtils.normalize(nodeId);

          if (nodeId == null)
          {
             return null;
          }

          synchronized (mutex)
          {
             DirNode node = removed.get(nodeId);

             if (node != null)  //node was deleted
             {
                return null;
             }

             node = changed.get(nodeId);

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

             //retrieve node from the back-end
             node = loadNode(nodeId);

             if (node != null && preload)
             {
                changed.put(nodeId, node);

                String[] paths = IdUtils.listPaths(nodeId);

                for (int i = 0; paths != null && i < (paths.length - 1); i++)
                {
                   if (changed.get(paths[i]) == null)
                   {
                      DirNode tmpNode = loadNode(paths[i]);
                      if (tmpNode == null)
                      {
                         throw new RuntimeException("Unexpected failure loading " +
                                                    "intermediate node " + paths[i]);
                      }

                      changed.put(paths[i], tmpNode);
                   }
                }
             }

             return node;
          }
       }

       /**
        * Return ObjectClass for a given name.
        *
        * @param   nodeClass
        *          Class name to find.
        *
        * @return  ObjectClass if class with such a name exists and
        *          <code>null</code> otherwise.
        */
       private ObjectClass getObjClass(String nodeClass)
       {
          return SchemaStorage.getSchema().getObjectClass(nodeClass);
       }

       /**
        * Load node data from real back-end.
        *
        * @param   nodeId
        *          Node ID.
        *
        * @return  loaded node or <code>null</code> in case of error.
        */
       private DirNode loadNode(String nodeId)
       {
          DirNode node = null;

          nodeId = IdUtils.normalize(nodeId);
          if (nodeId == null)
          {
             return null;
          }

          String nodeClass = backend.getNodeClassName(nodeId);
          if (nodeClass == null)
          {
             return null;
          }

          String name = "";

          if (!nodeId.isEmpty())
          {
             String[] path = IdUtils.splitId(nodeId);
             if (path == null)
             {
                return null;
             }

             name = path[1];
          }
          ObjectClass objClass = getObjClass(nodeClass);

          node = new DirNode(objClass, name);

          Attribute[] attrs = DirectoryService.getNodeAttributes(backend, nodeId);
          for (int i = 0; attrs != null && i < attrs.length; i++)
          {
             if (!node.setAttribute(attrs[i]))
                return null;
          }

          String[] list = backend.enumerateNodes(nodeId);
          for (int i = 0; i < list.length; i++)
          {
             if (!node.addChild(list[i]))
             {
                return null;
             }
          }

          return node;
       }

       /**
        * Verify node initialisation data and make sure that such a node can be
        * created and it will be valid.
        *
        * @param   nodeName
        *          Node name.
        * @param   nodeClass
        *          Node class.
        * @param   names
        *          Array of attribute names.
        * @param   values
        *          Array of attribute values.
        *
        * @return  constructed node or <code>null</code> in case of error.
        */
       private DirNode validateNode(String nodeName, String nodeClass,
                                    String[] names, Object[] values)
       {
          ObjectClass objClass = getObjClass(nodeClass);
          if (objClass == null)
          {
             return null;
          }

          DirNode node = new DirNode(objClass, nodeName);

          if (names != null && values != null)
          {
             if (names.length != values.length)
             {
                return null;
             }
             for (int i = 0; i < names.length; i++)
             {
                boolean rc = node.addAttributeValue(names[i], values[i], true);
                if (!rc)
                {
                   return null;
                }
             }
          }
          if (!node.isValid())
          {
             return null;
          }

          return node;
       }

       /**
        * Convenience method which produces array of strings from any given string
        * collection.
        *
        * @param    coll
        *           Source collection.
        *
        * @return   Array of strings.
        */
       private static String[] toArray(Collection<String> coll)
       {
          return coll.toArray(new String[coll.size()]);
       }

       /**
        * Select nodes which start from a given path.
        *
        * @param    baseId
        *           Base node ID.
        *
        * @return   collection of selected nodes.
        */
       private static List<String> selectChildNodes(String baseId,
                                                    Collection<String> source)
       {
          Set<String> ids = new TreeSet<>(source);

          List<String> tmp = new ArrayList<>();

          Iterator<String> iter = ids.iterator();

          String id = null;
          // skip to first id
          while(iter.hasNext())
          {
             id = iter.next();
             if (id.startsWith(baseId))
             {
                break;
             }
             id = null;
          }

          // create copy of moved nodes
          while (id != null)
          {
             tmp.add(id);

             if (iter.hasNext())
             {
                id = iter.next();
                if (!id.startsWith(baseId))
                {
                   id = null;
                }
             }
             else
             {
                id = null;
             }
          }

          return tmp;
       }
    }