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