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;
   }
}