LdapRemapper.java

/*
** Module   :LdapRemapper.java
** Abstract :LDAP back-end for the Directory.
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
** 001 SIY 20050322   @20581 Created initial version
** 002 SIY 20050406   @21016 Changes required to support writing, 
**                           different mapping modes and map sources, 
**                           etc.
** 003 SIY 20050513   @21196 Commented out printing stack trace in
**                           deleteLdapNodeAttribute, cleaned up comments.
** 004 SIY 20050518   @21234 Reestablish connection on bind() if it is 
**                           failed.
** 005 SIY 20050524   @21263 Fixed moving nodes.  
** 006 SIY 20090514   @42183 Added refresh() method, some minor cleanups.
** 007 EVL 20160222          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 008 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 009 SP  20250417          Code formatting and javadoc fixes.
*/

/*
** 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 java.io.*;
import java.util.*;
import java.util.logging.*;

import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.ldap.*;

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.cfg.ConfigurationException;
import com.goldencode.p2j.schema.SchemaException;
import com.goldencode.p2j.util.logging.*;

/**
 * This class provides a mapping of the P2J Directory tree into LDAP
 * directory.
 * <p>
 * The design assumes that LdapRemapper can be used in very different
 * environments and therefore it must support a number of different approaches
 * which allow mapping between P2J directory as it is seen by the
 * application and underlying LDAP directory.
 * <p>
 * Following cases are possible:
 * <ul>
 * <li>Direct mapping. <br>
 * In this case each P2J node has appropriate underling LDAP node.</li>
 * <li>Partial mapping. <br>
 * In this case some P2J nodes may have no appropriate LDAP nodes. In extreme
 * case only leaf nodes may have appropriate LDAP nodes while all other nodes
 * are only virtual.</li>
 * <li>Partial mapping with some data stored outside LDAP. <br>
 * This case is similar to previous one but even not all leaf nodes are really
 * backed by the LDAP nodes and stored elsewhere.</li>
 * </ul>
 * <p>
 * To support all these different situations following approach is used:
 * implementation maintains P2J directory tree in memory. Each node which is
 * backed by the LDAP has special Object Class "ldapNode". This class
 * represent all information required to properly access LDAP server and map
 * attributes. The mapping is constructed at startup either by loading data
 * from external storage or by scanning LDAP subtree.
 * 
 * @author  SIY
 * @version 1.0
 */
class LdapRemapper
extends RamRemapper
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(LdapRemapper.class);
   
   /** Value which will represent empty strings in LDAP */
   private static final String EMPTY = "--empty--";
   
   /** Mode: mapping saved as attribute in LDAP node */
   private static final int MODE_ATTRIBUTE   = 1;

   /** Mode: mapping is XML file */
   private static final int MODE_FILE        = 2;

   /** Mode: mapping saved as subtree of LDAP tree */
   private static final int MODE_SUBTREE     = 3;

   /** Mode: mapping is XML file at specified URL */
   private static final int MODE_URL         = 4;

   /** Certificate alias */
   private String alias;

   /** Certificate password */
   private String aliasPass;
   
   /** LDAP directory context used to access LDAP data */
   private LdapContext ctx;

   /** LDAP context environment */
   private Hashtable<String, String> env;

   /** Keystore password */
   private String keyPass;

   /** Keystore */
   private String keyStore;

   /** Mapping mapping (file, URL, CN, etc) */
   private String mapping;
   
   /** How to handle mapping field.*/
   private int mode;

   /** Mapping data */
   private SchemaMapping schemaMap;

   /** Truststore password */
   private String trustPass;

   /** Storage for the private keys */
   private String trustStore;

   /**
    * Construct an instance of the LdapRemapper which will work with specified
    * LDAP server.
    * 
    * @param   config
    *          A reference to configuration.
    *
    * @throws  ConfigurationException
    */
   LdapRemapper(BootstrapConfig config) throws ConfigurationException
   {
      super();

      schemaMap = new SchemaMapping();

      mapping = config.getConfigItem("directory", "ldap", "MAPPING");
      String modeStr = config.getConfigItem("directory", "ldap", "MODE");

      if (modeStr == null)
      {
         throw new ConfigurationException("Mapping mode must be defined");
      }
      else if (modeStr.equalsIgnoreCase("FILE")) //Local XML file
      {
         //mapping is a file name
         mode = MODE_FILE;
      }
      else if (modeStr.equalsIgnoreCase("URL")) //External URL to XML data
      {
         //mapping is an URL
         mode = MODE_URL;
      }
      else if (modeStr.equalsIgnoreCase("ATTRIBUTE")) //LDAP attribute
      {
         //mapping is full path to the attribute
         mode = MODE_ATTRIBUTE;
      }
      else if (modeStr.equalsIgnoreCase("SUBTREE")) //Direct mapping
      {
         //mapping is a root of the mapping subtree
         mode = MODE_SUBTREE;
      }
      else
      {
         throw new ConfigurationException("Unknown mapping mode");
      }

      if (mapping == null)
      {
         throw new ConfigurationException("Mapping must be defined");
      }

      env = new Hashtable<String, String>();

      env.put(Context.INITIAL_CONTEXT_FACTORY,
              "com.sun.jndi.ldap.LdapCtxFactory");

      String url = config.getConfigItem("directory", "ldap", "URL");
      if (url != null)
      {
         env.put(Context.PROVIDER_URL, url);
      }
      else
      {
         throw new ConfigurationException("Missing LDAP URL");
      }

      String param = config.getConfigItem("directory", "ldap", "AUTH");
      if (param != null)
      {
         env.put(Context.SECURITY_AUTHENTICATION, param);
      }

      param = config.getConfigItem("directory", "ldap", "PRINCIPAL");
      if (param != null)
      {
         env.put(Context.SECURITY_PRINCIPAL, param);
      }

      param = config.getConfigItem("directory", "ldap", "CREDENTIALS");
      if (param != null)
      {
         env.put(Context.SECURITY_CREDENTIALS, param);
      }
      
      if (url.toUpperCase().startsWith("LDAPS:"))
      {
         //TLS connection

         //Setup socket factory
         env.put("java.naming.ldap.factory.socket",
                 "com.goldencode.p2j.directory.LdapSocketFactory");

         keyStore   = config.getConfigItem("directory", "ldap", "KEYSTORE");
         trustStore = config.getConfigItem("directory", "ldap", "TRUSTSTORE");
         keyPass    = config.getConfigItem("directory", "ldap", "KEYPASSWD");
         trustPass  = config.getConfigItem("directory", "ldap", "TRUSTPASSWD");
         alias      = config.getConfigItem("directory", "ldap", "ALIAS");
         aliasPass  = config.getConfigItem("directory", "ldap", "ALIASPASSWD");

         if (keyStore == null || keyPass == null || alias == null
            || trustStore == null || trustPass == null || aliasPass == null)
         {
            throw new ConfigurationException(
                      "Invalid TLS configuration parameters");
         }
         
         LdapSocketFactory.initSocketFactory(keyStore, keyPass, trustStore, 
                                             trustPass, alias, aliasPass);
      }

      try
      {
         setCtx(new InitialLdapContext(env, null));
      }
      catch (NamingException e)
      {
         LOG.severe("", e);
         return;
      }
   }

   /**
    * No-op implementation. 
    * <p>
    * Current implementation assumes that all external directory changes are
    * reloaded automatically. This is correct, but in some operating modes
    * this might not be enough because mapping information is not reloaded.
    * 
    * @see   com.goldencode.p2j.directory.RamRemapper#refresh()
    */
   @Override
   public boolean refresh()
   {
      return true;
   }

   /**
    * Intercept adding of the value to the attribute.
    * 
    * @param   node
    *          A node from which attribute value is requested.
    * @param   name
    *          Name of the attribute.
    * @param   val
    *          New value which will be added to the attribute.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   protected boolean addAttributeValueExt(RamNode node, String name,
                                          Object val)
   {
      if (!addLdapAttributeValue(node, name, val))
      {
         return false;
      }
      return super.addAttributeValueExt(node, name, val);
   }
   
   /**
    * Intercept adding of the node. All parameters are checked and all what we
    * need to is to add new node to LDAP and call
    * <code>super.addNodeExt()</code>.
    * 
    * @param   parent
    *          A node to which <code>newNode</code> will be added.
    * @param   newNode
    *          New node which need to be added to <code>parent</code>.
    *
    * @return  <code>true</code> if operation was successful and
    *          <code>false</code> otherwise.
    */
   protected boolean addNodeExt(RamNode parent, RamNode newNode)
   {
      if (parent.getRealNode() == null)
      {
         return false;
      }
      
      RamNode ldapNode = addLdapNode(parent, newNode);
      if (ldapNode == null)
      {
         return false;
      }
      
      return super.addNodeExt(parent.getRealNode(), ldapNode);
   }
   
   /**
    * Intercept deletion of the attribute.
    * 
    * @param   node
    *          A node from which attribute value is requested.
    * @param   name
    *          Name of the attribute.
    *
    * @return  <code>true</code> if operation was successful and
    *          <code>false</code> otherwise.
    */
   protected boolean deleteNodeAttributeExt(RamNode node, String name)
   {
      if (!deleteLdapNodeAttribute(node.getRealNode(), name))
      {
         return false;
      }
      return super.deleteNodeAttributeExt(node, name);
   }

   /**
    * Intercept deletion of the node attribute value.
    * 
    * @param   node
    *          A node from which attribute value will be deleted.
    * @param   name
    *          Name of the attribute.
    * @param   index
    *          Attribute value index.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   protected boolean deleteNodeAttributeValueExt(RamNode node, String name,
                                                 int index)
   {
      if (!deleteLdapNodeAttributeValue(node, name, index))
      {
         return false;
      }
      return super.deleteNodeAttributeValueExt(node, name, index);
   }

   /**
    * Intercept removing node.All parameters are checked and all what we need
    * to is to remove child node from LDAP and call
    * <code>super.deleteNodeExt()</code>.
    * 
    * @param   parent
    *          A node to which <code>newNode</code> will be added.
    * @param   child
    *          An ID of the child node to remove.
    *
    * @return  <code>true</code> if operation was successful and
    *          <code>false</code> otherwise.
    */
   protected boolean deleteNodeExt(RamNode parent, String child)
   {
      if (parent.getRealNode() == null)
      {
         return false;
      }
      if (!deleteLdapNode(parent, child))
      {
         return false;
      }
      return super.deleteNodeExt(parent.getRealNode(), child);
   }

   /**
    * Load mapping and initialise directory context to access LDAP.
    * 
    * @throws  Exception
    *          Forwarded from various sources and wrapped into
    *          <code>SchemaException</code>.
    */
   protected void load()
   throws Exception
   {
      //Restart connection
      try
      {
         ctx.reconnect(ctx.getConnectControls());
      }
      catch (NamingException e)
      {
         LOG.warning("", e);
         setCtx(new InitialLdapContext(env, null));
      }

      if (mode == MODE_URL)
      {
         //TODO add reading data from URL
         return;
      }

      //Load XML file
      if (mode == MODE_FILE)
      {
         XmlRemapperIO io = new XmlRemapperIO(this, schemaMap);
         FileInputStream is = new FileInputStream(mapping);
         io.load(is);
         is.close();
         return;
      }

      if (mode == MODE_ATTRIBUTE)
      {
         //Read data from attribute
         String[] src = mapping.split("/", 2);

         if (src == null || src.length != 2)
         {
            throw new SchemaException("Invalid mapping location: " + mapping);
         }

         try
         {
            Attributes attr = getCtx().getAttributes(src[0], new String[] { src[1] });

            String val = (String)attr.get(src[1]).get();
            
            ByteArrayInputStream is;
            is = new ByteArrayInputStream(Base64.base64ToByteArray(val));

            XmlRemapperIO io = new XmlRemapperIO(this, schemaMap);
            
            io.load(is);
            is.close();
         }
         catch (Exception e)
         {
            LOG.log(Level.FINER, "", e);
            throw new SchemaException("Unable to retrieve mapping data", e);
         }
         
         return;
      }

      if (mode == MODE_SUBTREE)
      {
         // mapping is a root of the mapping subtree
         String[] src = mapping.split("/", 2);

         if (src == null || src.length != 2)
         {
            throw new SchemaException("Invalid mapping location : " + mapping);
         }

         try
         {
            Attributes attr = getCtx().getAttributes(src[0], new String[] { src[1] });

            int size = attr.get(src[1]).size();
            
            for (int i = 0; i < size; i++)
            {
               String data = (String)attr.get(src[1]).get(i);

               String[] parsed = data.split("/", 4);

               if (parsed == null || parsed.length != 4)
               {
                  continue;
               }

               //Values are mapped so:
               //parsed[0] = p2jclass
               //parsed[1] = p2jAttribute
               //parsed[2] = ldapClass
               //parsed[3] = ldapAttribute
               
               schemaMap.mapAttribute(parsed[0], parsed[1], parsed[2],
                                      parsed[3]);
               
               schemaMap.mapLdapClassToP2jClass(parsed[2], parsed[0]);
            }

            schemaMap.checkRequiredClasses();
         }
         catch (Exception e)
         {
            LOG.log(Level.FINER, "", e);
            throw new SchemaException("Unable to retrieve schema mapping", e);
         }

         //Restore schema mapping data

         //Build a root node
         RamNode r = new RamNode(getObjClass("ldapNode"), "");

         r.addStringAttributeValue("location", "");
         r.addStringAttributeValue("p2jclass", "container");

         setRoot(r);

         refreshLdapNode(r);
         
         return;
      }
   }

   /**
    * Replacement for the locateNode. It intercepts the result and substitutes
    * node with fake node if located node is of special "ldapNode" Object
    * Class.
    * 
    * @param   id
    *          Node ID
    *
    * @return  Reference to found node or <code>null</code> if error
    *          occurred.
    */
   protected RamNode locateNode(String id)
   {
      RamNode node = super.locateNode(id);
      if (node == null)
      {
         return null;
      }
      if (node.isA("ldapNode"))
      {
         return extractFromLdap(node);
      }
      return node;
   }

   /**
    * Intercept moving node.
    *  
    * @param   parent
    *          A parent node of the node which is about to be moved to new
    *          location.
    * @param   name
    *          Name of the node which will be moved.
    * @param   newParent
    *          A node which will be new parent node if operation will be
    *          successful.
    * @param   newName
    *          New name of the node.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   protected boolean moveNodeExt(RamNode parent, String name,
                                 RamNode newParent, String newName)
   {
      if (newParent.getRealNode() == null ||
         parent.getChild(name) == null || 
         !parent.getChild(name).isA("ldapNode"))
      {
         return false;
      }
      if (!moveLdapNode(parent, name, newParent.getRealNode(), newName))
      {
         return false;
      }
      if (parent.getRealNode() != null)
      {
         parent = parent.getRealNode();
      }
      return super.moveNodeExt(parent, name, newParent.getRealNode(), newName);
   }
   
   /**
    * Save current tree into XML file.
    * 
    * @throws  Exception
    *          If I/O error occurred during operation or
    *          <code>XmlHelper</code> throws an
    *          <code>ParserConfigurationException</code>.
    */
   protected void save()
   throws Exception
   {
      if (mode == MODE_URL || mode == MODE_SUBTREE)
      {
         return;
      }

      if (mode == MODE_FILE)
      {
         XmlRemapperIO io = new XmlRemapperIO(this, schemaMap);
      
         io.save(new FileOutputStream(mapping));
         
         return;
      }
      
      if (mode == MODE_ATTRIBUTE)
      {
         String[] src = mapping.split("/", 2);

         if (src == null || src.length != 2)
         {
            throw new SchemaException("Invalid mapping location " + mapping);
         }
         
         XmlRemapperIO io = new XmlRemapperIO(this, schemaMap);
         
         ByteArrayOutputStream os = new ByteArrayOutputStream();
         
         io.save(os);
         
         os.close();
         
         String val = Base64.byteArrayToBase64(os.toByteArray());

         try
         {
            Attributes attrs;
            attrs = getCtx().getAttributes(src[0], new String[] { src[1] });

            try
            {
               if (val.equals(attrs.get(src[1]).get(0))) //No changes
               {
                  return;
               }
               attrs.get(src[1]).set(0, val);
            }
            catch (Exception e)
            {
               throw new SchemaException("Unable to update mapping", e);
            }

            getCtx().modifyAttributes(src[0], DirContext.REPLACE_ATTRIBUTE, attrs);
         }
         catch (Exception e)
         {
            throw new SchemaException("Unable to update mapping", e);
            
         }
         return;
      }
   }

   /**
    * Intercept changing node attribute value.
    * 
    * @param   node
    *          A node which contains the attribute to change.
    * @param   name
    *          Attribute name.
    * @param   index
    *          Index of the value to retrieve.
    * @param   val
    *          New value for the attribute variable.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   protected boolean setNodeValueExt(RamNode node, String name, Object val,
                                     int index)
   {
      if (!setLdapAttributeValue(node, name, val, index))
      {
         return false;
      }
      return node.setAttributeValue(name, val, index);
   }
   
   /**
    * Add value to the LDAP node attribute.
    * 
    * @param   node
    *          A reference to the <code>RamNode</code> of "ldapNode" object
    *          class.
    * @param   name
    *          Attribute name.
    * @param   val
    *          New attribute value.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   private boolean addLdapAttributeValue(RamNode node, String name, Object val)
   {
      if (node == null)
      {
         return false;
      }

      String location = getLocation(node.getRealNode());
      String attribute = getLdapAttribute(node.getRealNode(), name);
      Attribute tmp = node.genAttribute(name);

      if (location == null || tmp == null)
      {
         return false;
      }

      val = tmp.getStringValue(val);

      if (tmp.isA(AttributeType.ATTR_STRING) && val.equals(""))
      {
         val = EMPTY;
      }

      if (!tmp.getDefinition().isMultiple())
      {
         try
         {
            Attributes tmpAttrs = getCtx().getAttributes(location, new String[]
               {
                  attribute
               });

            if (tmpAttrs.get(attribute) != null && tmpAttrs.get(attribute).size() > 0)
            {
               //Multiple values are not allowed.
               return false;
            }
         }
         catch (Exception e)
         {
            //Attribute is missing, so we can add it safely.
            LOG.log(Level.FINER, "Attribute is missing.", e);
         }
      }

      ModificationItem[] mods = new ModificationItem[1];

      mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute(attribute, val));
      try
      {
         getCtx().modifyAttributes(location, mods);
      }
      catch (NamingException e)
      {
         LOG.info("", e);
         return false;
      }

      return true;
   }

   /**
    * Add new node to LDAP tree.
    * 
    * @param   node
    *          <code>RamNode</code> instance of "ldapNode" Object Class
    *          which contains mapping information.
    * @param   newNode
    *          New node to add.
    *
    * @return  New RamNode instance of "ldapNode" object class or
    *          <code>null</code>
    */
   private RamNode addLdapNode(RamNode node, RamNode newNode)
   {
      // Can't add a node if there is no backing LDAP node
      if (node == null || newNode == null)
      {
         return null;
      }

      String location = getLocation(node.getRealNode());
      if (location == null)
      {
         return null;
      }

      RamNode fakeNode = new RamNode(getObjClass("ldapNode"),
                                     newNode.getName(), null);

      try
      {
         String p2jClass = newNode.getNodeClass().getName();
         String objClass = schemaMap.getLdapClass(p2jClass);
         String name = composeName(location, newNode.getName(), p2jClass);

         if (objClass == null)
         {
            throw new IncompatibleClassChangeError("No LDAP class for " + p2jClass);
         }

         if (name == null)
         {
            throw new IncompatibleClassChangeError("Unable to compose name for class " + p2jClass);
         }
         
         Attributes attributes = new BasicAttributes(true);

         BasicAttribute attribute = new BasicAttribute("objectClass", objClass);
         attributes.put(attribute);

         String[] attrs = schemaMap.getLdapAttributes(objClass);

         if (attrs != null)
         {
            for (int i = 0; i < attrs.length; i++)
            {
               BasicAttribute newAttr = new BasicAttribute(attrs[i]);
               String attrName = schemaMap.getP2jAttribute(p2jClass, attrs[i]);
               Attribute attr = newNode.getAttribute(attrName);

               if (attr != null)
               {
                  String[] values = attr.getValues();
                  
                  for (int j = 0; j < attr.getCount(); j++)
                  {
                     if (attr.isA(AttributeType.ATTR_STRING) && values[j].isEmpty())
                     {
                        newAttr.add(EMPTY);
                     }
                     else
                     {
                        newAttr.add(values[j]);
                     }
                  }

                  attributes.put(newAttr);
               }
            }
         }
         if (!fakeNode.addAttributeValue("location", name, true))
         {
            return null;
         }
         if (!fakeNode.addAttributeValue("p2jclass", p2jClass, true))
         {
            return null;
         }
         if (!fakeNode.isValid())
         {
            return null;
         }
         getCtx().createSubcontext(name, attributes);
      }
      catch (Exception e)
      {
         LOG.info("", e);
         return null;
      }

      return fakeNode;
   }

   /**
    * Compose full name for the given location and node name.
    * 
    * @param   location
    *          Starting location.
    * @param   name
    *          Node name.
    * @param   p2jClass
    *          An P2J Object Class name for the given node name.
    *
    * @return  Full location string.
    */
   private String composeName(String location, String name, String p2jClass)
   {
      String prefix = schemaMap.getLdapAttribute(p2jClass, "objectName");
      if (prefix == null)
      {
         return null;
      }
      if (location == null || location.isEmpty())
      {
         return prefix + "=" + name;
      }
      return prefix + "=" + name + "," + location;
   }

   /**
    * Delete node in LDAP directory.
    * 
    * @param   node
    *          A node of "ldapNode" object class.
    * @param   child
    *          Name of the child node to remove.
    *
    * @return  <code>true</code> if operation was successful and
    *          <code>false</code> otherwise.
    */
   private boolean deleteLdapNode(RamNode node, String child)
   {
      if (node == null || child == null)
      {
         return false;
      }

      String name = getLocation(node.getChild(child));
      if (name == null)
      {
         return false;
      }
      
      try
      {
         getCtx().destroySubcontext(name);
      }
      catch (NamingException e)
      {
         LOG.info("", e);
         return false;
      }

      return true;
   }

   /**
    * Remove attribute from the LDAP node.
    * 
    * @param   node
    *          A node of "ldapNode" Object Class.
    * @param   name
    *          Attribute name.
    *
    * @return  <code>true</code> if operation was successful and
    *          <code>false</code> otherwise.
    */
   private boolean deleteLdapNodeAttribute(RamNode node, String name)
   {
      if (node == null || name == null)
      {
         return false;
      }

      String location = getLocation(node);
      String attribute = getLdapAttribute(node, name);
      if (location == null || attribute == null)
      {
         return false;
      }
      
      ModificationItem[] mods = new ModificationItem[1];

      mods[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
                                     new BasicAttribute(attribute));      
      
      try
      {
         getCtx().modifyAttributes(location, mods);
      }
      catch (NamingException e)
      {
         LOG.log(Level.FINER, "", e);
         return false;
      }
      
      return true;
   }

   /**
    * Delete node attribute value from LDAP node.
    * 
    * @param   node
    *          A node of "ldapNode" Object Class.
    * @param   name
    *          Attribute name.
    * @param   index
    *          Attribute value index.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   private boolean deleteLdapNodeAttributeValue(RamNode node,
                                                String name, int index)
   {
      if (node == null || name == null || index < 0)
      {
         return false;
      }

      String location = getLocation(node.getRealNode());
      String attribute = getLdapAttribute(node.getRealNode(), name);
      Attribute tmp = node.genAttribute(name);
      if (location == null || attribute == null || tmp == null)
      {
         return false;
      }
      
      try
      {
         Attributes attrs = getCtx().getAttributes(location, new String[] {attribute});
         
         if (attrs.get(attribute) == null)   //No such attribute
         {
            return false;
         }
         
         attrs.get(attribute).remove(index);
         if (attrs.get(attribute).size() != 0)
         {
            getCtx().modifyAttributes(location, DirContext.REPLACE_ATTRIBUTE, attrs);
         }
         else
         {
            if(tmp.getDefinition().isMandatory())
            {
               return false;
            }
            getCtx().modifyAttributes(location, DirContext.REMOVE_ATTRIBUTE, attrs);
         }
      }
      catch (Exception e)
      {
         LOG.info("", e);
         return false;
      }
      
      return true;
   }

   /**
    * Extract information from LDAP.
    * 
    * @param   node
    *          Source node of "ldapNode" Object Class.
    *
    * @return  fake RamNode which is used in further operations instead of real
    *          node of "ldapNode" Object Class.
    */
   private RamNode extractFromLdap(RamNode node)
   {
      String location = getLocation(node);
      String p2jClass = getP2jClass(node);
      String ldapClass = schemaMap.getLdapClass(p2jClass);

      if (location == null || p2jClass == null || ldapClass == null)
      {
         return null;
      }

      // Construct a node from known data;
      RamNode fakeNode = new RamNode(getObjClass(p2jClass), node.getName(), node);

      String[] attrNames = schemaMap.getLdapAttributes(ldapClass);
      if (attrNames != null)
      {
         try
         {
            Attributes answer;

            answer = getCtx().getAttributes(location, attrNames);

            NamingEnumeration<? extends javax.naming.directory.Attribute> ae = 
                                                            answer.getAll();
            
            while(ae.hasMore())
            {
               javax.naming.directory.Attribute attr =  ae.next();

               String p2jName = schemaMap.getP2jAttribute(p2jClass, attr.getID());
               if (p2jName == null)
               {
                  continue;
               }

               // Create an attribute

               Attribute p2jAttribute = fakeNode.genAttribute(p2jName);
               if (p2jAttribute == null)
               {
                  continue;
               }

               // Retrieve all values

               for (NamingEnumeration<?> e = attr.getAll(); e.hasMore();)
               {
                  Object obj = e.next();
                  
                  if (p2jAttribute.isA(AttributeType.ATTR_STRING) && obj.equals(EMPTY))
                  {
                     obj = "";
                  }
                  if (!fakeNode.addStringAttributeValue(p2jName, (String)obj))
                  {
                     return null;
                  }
               }
            }
         }
         catch (Exception e)
         {
            LOG.info("", e);
            return null;
         }
      }

      //Check if node need a refresh.
      
      refreshLdapNode(node);
      
      RamNode[] list = node.getChildList();
      for (int i = 0; i < list.length; i++)
      {
         if (!fakeNode.addChild(list[i]))
         {
            return null;
         }
      }

      if (!fakeNode.isValid())
      {
         return null;
      }

      return fakeNode;
   }

   /**
    * Get JNDI LDAP context.
    * 
    * @return  Returns the context.
    */
   private LdapContext getCtx()
   {
      return ctx;
   }

   /**
    * Find a name of the LDAP attribute which corresponds to specified P2J
    * attribute.
    * 
    * @param   node
    *          <code>RamNode</code> of "ldapNode" Object Class.
    * @param   name
    *          Name of P2J attribute.
    *
    * @return  Name of the LDAP attribute.
    */
   private String getLdapAttribute(RamNode node, String name)
   {
      return schemaMap.getLdapAttribute(getP2jClass(node), name);
   }

   /**
    * Get LDAP Object Class Name for child node of specified location.
    * 
    * @param   location
    *          LDAP node location.
    * @param   name
    *          Name of child node of specified location.
    *
    * @return  LDAP Object Class name for specified node.
    */
   private String getLdapNodeClass(String location, String name)
   {
      if (!location.isEmpty())
      {
         name = name + "," + location;
      }

      try
      {
         Attributes answer = getCtx().getAttributes(name, new String[] {"objectClass"});
         
         return (String)answer.get("objectClass").get();
      }
      catch (NamingException e)
      {
         LOG.info("", e);
      }
      
      return null;
   }
   
   /**
    * Convenience method to retrieve information about LDAP context name
    * (location) from specified <code>RamNode</code> instance of "ldapNode"
    * Object Class.
    * 
    * @param   node
    *          Reference to the RamNode of "ldapNode" Object Class.
    *
    * @return  LDAP context name.
    */
   private String getLocation(RamNode node)
   {
      if (node == null || !node.isA("ldapNode"))
      {
         return null;
      }
      
      Attribute location = node.getAttribute("location");
      if (location == null)
      {
         return null;
      }

      return (String) location.getValue(0);
   }

   /**
    * Get name of P2J Object Class associated with node of "ldapNode"
    * Object Class.
    *  
    * @param   node
    *          <code>RamNode</code> of "ldapNode" Object Class.
    *
    * @return  Name of the LDAP Object Class.
    */
   private String getP2jClass(RamNode node)
   {
      if (node == null || !node.isA("ldapNode"))
      {
         return null;
      }
         
      Attribute p2jClassAttr = node.getAttribute("p2jclass");
      if (p2jClassAttr == null || p2jClassAttr.getValues() == null)
      {
         return null;
      }
      
      return (String)p2jClassAttr.getValue(0);
   }
   
   /**
    * Move LDAP node to the new location.
    * 
    * @param   parent
    *          Parent node.
    * @param   name
    *          Node name.
    * @param   newParent
    *          Target parent node.
    * @param   newName
    *          Target name.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   private boolean moveLdapNode(RamNode parent, String name,
                                RamNode newParent, String newName)
   {
      RamNode node = parent.getChild(name);

      String location = getLocation(node);
      String newLocation = getLocation(newParent);
      if (location == null || newLocation == null)
      {
         return false;
      }

      try
      {
         Attribute clsAttr = node.getAttribute("p2jclass");
         
         String newLoc = composeName(newLocation, newName,
                                     clsAttr.getStringValue(0));
         
         getCtx().rename(location, newLoc);

         //Change location
         Attribute locAttr = node.getAttribute("location");

         locAttr.setValue(0, newLoc);
      }
      catch (NamingException e)
      {
         LOG.info("", e);
         return false;
      }

      return true;
   }

   /**
    * Compare existing information about child nodes with stored in mapping
    * and refresh mapping of necessary.
    * 
    * @param   node
    *          Node to refresh.
    */
   private void refreshLdapNode(RamNode node)
   {
      if (node == null || !node.isA("ldapNode"))
      {
         return;
      }

      String location = getLocation(node);
      
      try
      {
         NamingEnumeration<NameClassPair> list = getCtx().list(location);

         Map<String, String> names = new HashMap<>();
         Map<String, String> classes = new HashMap<>();

         while (list.hasMore())
         {
            NameClassPair nc = list.next();

            String name = nc.getName();

            String[] lst = name.split("=", 2);
            if (lst == null || lst.length != 2)
            {
               continue;
            }

            String ldapClass = getLdapNodeClass(location, name);
            if (ldapClass == null)
            {
               continue;
            }

            names.put(lst[1], name);
            classes.put(lst[1], ldapClass);
         }

         //Add missing child nodes
         Set<String> childNames = new TreeSet<>(names.keySet());
         for (Iterator<String> iter = childNames.iterator(); iter.hasNext();)
         {
            String name = iter.next();
            if (node.getChild(name) == null)
            {
               //Add node
               RamNode child = new RamNode(getObjClass("ldapNode"), name, null);

               String ldapClass = classes.get(name);
               if (ldapClass == null)
               {
                  continue;
               }
               
               String p2jClass = schemaMap.getP2jClass(ldapClass);
               if (p2jClass == null)
               {
                  continue;
               }
               
               String childLoc = names.get(name);
               
               if (!location.isEmpty())
               {
                  childLoc = childLoc + "," + location;
               }

               do
               {
                  if (!child.addAttributeValue("location", childLoc, true))
                  {
                     break;
                  }
                  if (!child.addAttributeValue("p2jclass", p2jClass, true))
                  {
                     break;
                  }
                  node.addChild(child);
                  refreshLdapNode(child);
               }
               while (false);
            }
            
            //if (recursive)
            //   refreshLdapNode(node.getChild(name), recursive);
         }

         //Now iterate over existing nodes and if node is not in names
         //and its type is "ldapNode" then remove it.

         RamNode[] childNodes = node.getChildList();
         for (int i = 0; i < childNodes.length; i++)
         {
            String name = childNodes[i].getName();

            if (names.get(name) == null && childNodes[i].isA("ldapNode"))
            {
               node.removeChild(name);
            }
         }
      }
      catch (Exception e)
      {
         LOG.info("", e);
         return;
      }
   }

   /**
    * Set JNDI LDAP context.
    * 
    * @param   ctx
    *          The context to set.
    */
   private void setCtx(LdapContext ctx)
   {
      this.ctx = ctx;
   }

   /**
    * Set value of the attribute of LDAP node.
    * 
    * @param   node
    *          A node which contains the attribute to change.
    * @param   name
    *          Attribute name.
    * @param   index
    *          Index of the value to retrieve.
    * @param   val
    *          New value for the attribute variable.
    *
    * @return  <code>true</code> is operation is successful and
    *          <code>false</code> otherwise.
    */
   private boolean setLdapAttributeValue(RamNode node, String name,
                                         Object val, int index)
   {
      String location = getLocation(node.getRealNode());
      String attribute = getLdapAttribute(node.getRealNode(), name);
      Attribute tmp = node.genAttribute(name);

      if (location == null || attribute == null || tmp == null)
      {
         return false;
      }

      val = tmp.getStringValue(val);

      if (tmp.isA(AttributeType.ATTR_STRING) && val.equals(""))
      {
         val = EMPTY;
      }

      try
      {
         Attributes attrs = getCtx().getAttributes(location, new String[]
            {
               attribute
            });

         try
         {
            if (index >= attrs.get(attribute).size())
            {
               return false;
            }

            if (!val.equals(attrs.get(attribute).get(index)))
               attrs.get(attribute).set(index, val);
         }
         catch (Exception e)
         {
            return false;
         }

         getCtx().modifyAttributes(location, DirContext.REPLACE_ATTRIBUTE, 
                                   attrs);
      }
      catch (Exception e)
      {
         LOG.info("", e);
         return false;
      }

      return true;
   }
}