# DirectoryCopy.java

    /*
    ** Module   : DirectoryCopy.java
    ** Abstract : batch mode directory tree editor
    **
    ** Copyright (c) 2005-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
    ** 001 SIY 20050505   @21190 Created initial version
    ** 002 SIY 20090529   @42504 Accommodated to changes in DirectoryService API.
    ** 003 NVS 20090824   @43729 Extended the set of command line, offline directory
    **                           utilities with these new utilities:
    **                           - remove - recursively removes a branch
    **                           - clear - deletes attributes conditionally or not
    **                           - set - adds or sets attribute's value
    ** 004 NVS 20090826   @43772 Javadoc fix
    ** 005 NVS 20090915   @43900 Added another command line, offline directory
    **                           utility selcopy for filtered copying of selected
    **                           subnodes between directories.
    ** 006 NVS 20090924   @44027 Gradual attribute value creation used in set
    **                           utility. The validity of the attribute us checked
    **                           once the value is set.
    ** 007 GES 20101117          Switched to new static schema loader that reads
    **                           from well-known resources rather than a manually
    **                           defined configuration file.
    ** 008 EVL 20160225          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 009 IAS 20160331          Potential NPE fix
    ** 010 HC  29170612          Fixed several NPEs, removed references to AdminUtils.
    ** 011 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    */
    /*
    ** 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.cfg.*;
    import com.goldencode.p2j.util.logging.*;

    import java.util.*;

    /**
     * This class implements a utility which provides batch mode directory editing.
     * The utility does not depend on the type of the each back-end.  It can be used
     * as a backup utility or for making modifications.
     * <p>
     * Known limitations: <br>
     * <ol>
     * <li>Both back-ends must be completely configured and ready to use. In
     * particular, if LDAP back-end uses <code>subtree</code> mapping mode then
     * mapping data must be already present in LDAP tree.</li>
     * <li>The utility performs copy, not backup, so it does not change nor
     * remove existing nodes in the destination unless they are overwritten by
     * existing nodes in the source. Therefore destination back-end tree must be
     * empty if exact copy of the source is required.</li>
     * </ol>
     */
    class DirectoryCopy
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(DirectoryCopy.class);

       /** Reference to command line parameters */
       private String[] args;

       /** utility statistics */
       private int skipped = 0;

       /** utility statistics */
       private int found   = 0;

       /** utility statistics */
       private int changed = 0;

       /** utility statistics */
       private int errors  = 0;

       /**
        * Construct an instance of DirectoryCopy for given command line
        * parameters.
        *
        * @param   args
        *          Command line parameters.
        */
       public DirectoryCopy(String[] args)
       {
          this.args = args;
       }

       /**
        * Shortcut for the System.out.println()
        *
        * @param   msg
        *          Message to print to standard output.
        */
       static void say(String msg)
       {
          System.out.println(msg);
       }

       /**
        * Shortcut for the System.out.print()
        *
        * @param   msg
        *          Message to print to standard output.
        */
       static void say_(String msg)
       {
          System.out.print(msg);
       }

       /**
        * Protected Directory Copy utility.
        *
        * @throws  ConfigurationException
        *          In case of configuration error.
        */
       private void protCopy()
       throws ConfigurationException
       {
          if (args.length != 4)
          {
             say("Usage: java DirectoryCopy protcopy <source-xml> <source-pw> "
                 + "<target-xml> <target-pw>");
             return;
          }

          SchemaLoad.initSchema();

          String cfg1  = args[0];
          String cfg2  = args[2];
          char[] pass1 = args[1].toCharArray();
          char[] pass2 = args[3].toCharArray();

          BootstrapConfig c1 = new BootstrapConfig(cfg1, pass1, null, null);
          BootstrapConfig c2 = new BootstrapConfig(cfg2, pass2, null, null);

          Remapper src = DirectoryService.genRemapper(c1);
          Remapper dst = DirectoryService.genRemapper(c2);

          say("Starting copying...");

          boolean res = true;

          do
          {
             res = src.bind();

             if (!res)
             {
                say("Bind to source failed");
                break;
             }

             res = dst.bind();

             if (!res)
             {
                say("Bind to destination failed");
                break;
             }

             res = DirectoryService.copySubtree(src, "", dst, "", true);

             if (res)
                dst.update();
          }
          while (false);

          boolean rc;
          rc = src.unbind();

          if (!rc)
             say("Unbinding from source failed");

          rc = dst.unbind();

          if (!rc)
             say("Unbinding from destination failed");

          say(res ? "Done." : "Failed.");
       }

       /**
        * Directory Copy utility.
        *
        * @throws  ConfigurationException
        *          In case of configuration error.
        */
       private void copy()
       throws ConfigurationException
       {
          // check arguments
          if (args.length != 4)
          {
             say("Usage: java DirectoryCopy copy <source-xml> <source-path> "
                 + "<target-xml> <target-path>");
             return;
          }

          // Empty BootstrapConfigs
          BootstrapConfig bcs = new BootstrapConfig(null, null, null, null);
          BootstrapConfig bct = new BootstrapConfig(null, null, null, null);

          // directory package configuration
          bcs.setServer(true);
          bcs.setConfigItem("directory", "backend", "type",     "xml");
          bcs.setConfigItem("directory", "xml",     "filename", args[0]);

          bct.setServer(true);
          bct.setConfigItem("directory", "backend", "type",     "xml");
          bct.setConfigItem("directory", "xml",     "filename", args[2]);

          // initialize directory schema
          SchemaLoad.initSchema();

          // instantiating directory tree backends
          Remapper rs = DirectoryService.genRemapper(bcs);
          Remapper rt = DirectoryService.genRemapper(bct);

          boolean rc = true;

          // binding to the directories
          rc = rs.bind();

          if (!rc)
          {
             say("Binding to source directory failed");
             return;
          }

          rc = rt.bind();

          if (!rc)
          {
             say("Binding to target directory failed");
             rs.unbind();
             return;
          }

          // copy the branch between directories
          rc = DirectoryService.copySubtree(rs, args[1], rt, args[3], true);
          if (rc)
          {
             rc = rt.update();
             if (!rc)
             {
                say("committing failed");
             }
          }
          else
          {
             say("copy operation failed");
          }

          // unbinding from the directories
          rc = rs.unbind();

          if (!rc)
          {
             say("Unbinding from source directory failed");
          }

          rc = rt.unbind();

          if (!rc)
          {
             say("Unbinding from target directory failed");
          }
       }

       /**
        * Directory Branch Removal utility.
        *
        * @throws  ConfigurationException
        *          In case of configuration error.
        */
       private void remove()
       throws ConfigurationException
       {
          // check arguments
          if (args.length != 2)
          {
             say("Usage: java DirectoryCopy remove <target-xml> <target-path>");
             return;
          }

          // Empty BootstrapConfig
          BootstrapConfig bct = new BootstrapConfig(null, null, null, null);

          // directory package configuration
          bct.setServer(true);
          bct.setConfigItem("directory", "backend", "type",     "xml");
          bct.setConfigItem("directory", "xml",     "filename", args[0]);

          // initialize directory schema
          SchemaLoad.initSchema();

          // instantiating directory tree backend
          Remapper rt = DirectoryService.genRemapper(bct);

          boolean rc = true;

          // binding to the directory
          rc = rt.bind();

          if (!rc)
          {
             say("Binding to target directory failed");
             return;
          }

          // check the branch for existence
          if (rt.getNodeClassName(args[1]) == null)
          {
             say("no such node in the target directory");
             rt.unbind();
             return;
          }

          // remove the branch
          rc = removeDirectoryNode(rt, args[1]);
          if (rc)
          {
             rc = rt.update();
             if (!rc)
             {
                say("committing failed");
             }
          }
          else
          {
             say("remove operation failed");
          }

          // unbinding from the directories
          rc = rt.unbind();

          if (!rc)
          {
             say("Unbinding from target directory failed");
          }
       }

       /**
        * Attribute Value Clear utility. It scans the specified branch
        * of the directory, selects all nodes of the given class and deletes
        * the specified attribute entirely or just the specified value.
        *
        */
       private void clearNodeWorker(Remapper rt, String nodeId, String nclass,
                                    String nattr, String val, boolean clearAll)
       {
          boolean rc = true;

          String nodeClass = rt.getNodeClassName(nodeId);

          if (!nodeClass.equalsIgnoreCase(nclass))
          {
             say("skipping " + nodeId + " " + nodeClass);
             skipped++;
             return;
          }

          say_("checking " + nodeId + " " + nodeClass);
          found++;

          // enumarate existing attributes
          Attribute atv = null;

          Attribute[] att = DirectoryService.getNodeAttributes(rt, nodeId);
          if (att == null || att.length == 0)
          {
             say(", skipping");
             skipped++;
             return;
          }

          for (int j = 0; j < att.length; j++)
          {
             if (att[j].getName().equalsIgnoreCase(nattr))
             {
                atv = att[j];
                break;
             }
          }

          if (atv == null)
          {
             say(", skipping");
             skipped++;
             return;
          }

          if (clearAll)
          {
             // whole attribute deleted unconditionally
             changed++;

             rc = rt.deleteNodeAttribute(nodeId, nattr);
             if (rc)
             {
                say(", cleared");
             }
             else
             {
                errors++;
                say(", clearing failed");
             }
          }
          else
          {
             // conditional deletion of a specific value
             String[] values = atv.getValues();
             List<String> valList = values == null ? Collections.emptyList() : Arrays.asList(values);

             int iv = valList.indexOf(val);
             if (iv == -1)
             {
                say(", skipping");
                skipped++;
             }
             else
             {
                changed++;

                rc = rt.deleteNodeAttributeValue(nodeId, nattr, iv);
                if (rc)
                {
                   say(String.format(", cleared at index %d", iv));
                }
                else
                {
                   errors++;
                   say(String.format(", clearing at index %d failed", iv));
                }
             }
          }
       }

       /**
        * Attribute Value Clear utility. It scans the specified branch
        * of the directory, selects all nodes of the given class and deletes
        * the specified attribute entirely or just the specified value.
        */
       private void clearWorker(Remapper rt, String node, String nclass,
                                String nattr, String val, boolean clearAll)
       {
          // process this node first
          clearNodeWorker(rt, node, nclass, nattr, val, clearAll);

          // enumerate immediate children
          String[] nodes = rt.enumerateNodes(node);
          if (nodes == null)
          {
             return;
          }

          // scan the children
          for (int i = 0; i < nodes.length; i++)
          {
             clearWorker(rt, node + "/" + nodes[i], nclass, nattr, val, clearAll);
          }
       }

       /**
        * Attribute Value Clear utility. It scans the specified branch
        * of the directory, selects all nodes of the given class and deletes
        * the specified attribute entirely or just the specified value.
        *
        * @throws  ConfigurationException
        *          In case of configuration error.
        */
       private void clear(String[] args)
       throws ConfigurationException
       {
          // check arguments
          if (args.length != 4 && args.length != 5)
          {
             say("Usage: java DirectoryCopy clear <target-xml> <target-path> " +
                 "node-class attr-name [attr-value]");
             return;
          }

          // true when the whole attribute is to be cleared
          boolean clearAll = args.length == 4;

          // Empty BootstrapConfig
          BootstrapConfig bct = new BootstrapConfig(null, null, null, null);

          // directory package configuration
          bct.setServer(true);
          bct.setConfigItem("directory", "backend", "type",     "xml");
          bct.setConfigItem("directory", "xml",     "filename", args[0]);

          // initialize directory schema
          SchemaLoad.initSchema();

          // instantiating directory tree backend
          Remapper rt = DirectoryService.genRemapper(bct);

          boolean rc = true;

          // binding to the directory
          rc = rt.bind();

          if (!rc)
          {
             say("Binding to target directory failed");
             return;
          }

          // check the branch for existence
          if (rt.getNodeClassName(args[1]) == null)
          {
             say("no starting node in the target directory");
             rt.unbind();
             return;
          }

          // check the node class validity
          String[] allClasses = rt.getClassNames();
          List<String> classList = allClasses == null ? null : Arrays.asList(allClasses);
          if (classList == null || !classList.contains(args[2]))
          {
             say("no such node class: " + args[2]);
             rt.unbind();
             return;
          }

          // check the attribute name validity
          AttributeDefinition[] attrs = rt.getClassDefinition(args[2]);
          if (attrs == null || attrs.length == 0)
          {
             say("class " + args[2] + " has no attributes");
             rt.unbind();
             return;
          }

          AttributeDefinition attr = null;

          for (int i = 0; i < attrs.length; i++)
          {
             if (attrs[i].getName().equalsIgnoreCase(args[3]))
             {
                attr = attrs[i];
                break;
             }
          }

          if (attr == null)
          {
             say("class " + args[2] + " has no attribute " + args[3]);
             say("valid attribute names are:");
             for (int i = 0; i < attrs.length; i++)
             {
                say("   " + attrs[i].getName());
             }
             rt.unbind();
             return;
          }

          // process the subtree
          String[] nodes = rt.enumerateNodes(args[1]);
          if (nodes == null)
          {
             say("no children at all");
             return;
          }
          clearWorker(rt, args[1], args[2], args[3], clearAll ? null : args[4],
                      clearAll);

          say(String.format("Nodes skipped %d, checked %d, changed %d, errors %d",
                            skipped, found, changed, errors));

          if (found == 0)
          {
             say("no children of class " + args[2]);
             rt.unbind();
             return;
          }

          if (changed == 0)
          {
             say("no nodes were modified");
             rt.unbind();
             return;
          }

          // committing changes
          if (rc)
          {
             rc = rt.update();
             if (!rc)
             {
                say("committing failed");
             }
             else
             {
                say("success");
             }
          }
          else
          {
             say("some clear operations failed");
          }

          // unbinding from the directories
          rc = rt.unbind();

          if (!rc)
          {
             say("Unbinding from target directory failed");
          }
       }

       /**
        * Attribute Value Set utility. It scans the specified branch
        * of the directory, selects all nodes of the given class and adds or sets
        * the specified attribute with specified value.
        *
        * @throws  ConfigurationException
        *          In case of configuration error.
        */
       private void set(String[] args)
       throws ConfigurationException
       {
          // check arguments
          if (args.length != 5 && args.length != 6)
          {
             say("Usage: java DirectoryCopy set <target-xml> <target-path> " +
                 "node-class attr-name attr-value [value-index]");
             return;
          }

          int index = 0;
          if (args.length == 6)
          {
             try
             {
                index = Integer.parseInt(args[5]);
             }
             catch (NumberFormatException nfx)
             {
                say("invalid index value");
                return;
             }
          }

          // Empty BootstrapConfig
          BootstrapConfig bct = new BootstrapConfig(null, null, null, null);

          // directory package configuration
          bct.setServer(true);
          bct.setConfigItem("directory", "backend", "type",     "xml");
          bct.setConfigItem("directory", "xml",     "filename", args[0]);

          // initialize directory schema
          SchemaLoad.initSchema();

          // instantiating directory tree backend
          Remapper rt = DirectoryService.genRemapper(bct);

          boolean rc = true;

          // binding to the directory
          rc = rt.bind();

          if (!rc)
          {
             say("Binding to target directory failed");
             return;
          }

          // check the branch for existence
          if (rt.getNodeClassName(args[1]) == null)
          {
             say("no starting node in the target directory");
             rt.unbind();
             return;
          }

          // check the node class validity
          String[] allClasses = rt.getClassNames();
          List<String> classList = allClasses == null ? null : Arrays.asList(allClasses);
          if (classList == null || !classList.contains(args[2]))
          {
             say("no such node class: " + args[2]);
             rt.unbind();
             return;
          }

          // check the attribute name validity
          AttributeDefinition[] attrs = rt.getClassDefinition(args[2]);
          if (attrs == null || attrs.length == 0)
          {
             say("class " + args[2] + " has no attributes");
             rt.unbind();
             return;
          }

          AttributeDefinition attr = null;

          for (int i = 0; i < attrs.length; i++)
          {
             if (attrs[i].getName().equalsIgnoreCase(args[3]))
             {
                attr = attrs[i];
                break;
             }
          }

          if (attr == null)
          {
             say("class " + args[2] + " has no attribute " + args[3]);
             say("valid attribute names are:");
             for (int i = 0; i < attrs.length; i++)
             {
                say("   " + attrs[i].getName());
             }
             rt.unbind();
             return;
          }

          // instantiate the new value of the attribute
          Attribute data = new Attribute(attr.genNodeAttribute(), new Object[] {});
          boolean valueValid = data.addStringValue(args[4]);
          if (!valueValid || !data.isValid())
          {
             say("incompatible value for the attribute");
             rt.unbind();
             return;
          }
          Object attrValue = data.getValue(0);

          // process the subtree
          String[] nodes = rt.enumerateNodes(args[1]);
          if (nodes == null)
          {
             say("no children at all");
             rt.unbind();
             return;
          }
          setWorker(rt, args[1], args[2], args[3], attr.getType(), attrValue,
                    index);

          say(String.format("Nodes skipped %d, checked %d, changed %d, errors %d",
                            skipped, found, changed, errors));

          if (found == 0)
          {
             say("no children of class " + args[2]);
             rt.unbind();
             return;
          }

          if (changed == 0)
          {
             say("no nodes were modified");
             rt.unbind();
             return;
          }

          // committing changes
          if (rc)
          {
             rc = rt.update();
             if (!rc)
             {
                say("committing failed");
             }
             else
             {
                say("success");
             }
          }
          else
          {
             say("some clear operations failed");
          }

          // unbinding from the directories
          rc = rt.unbind();

          if (!rc)
          {
             say("Unbinding from target directory failed");
          }
       }

       /**
        * Selective directory copy. Takes two existing directories: the source and
        * tge target, the path to scan and the filtering attribute name and value.
        * <p>
        * The filtering is done in the target directory. The path is enumerated and
        * all the nodes of the given class are checked for the given subnodes'
        * attribure value. If a node passes filtering, then the specified subnodes
        * of the node in the source directory are copied to replace the existing
        * subnodes in the target, if any.
        * <p>
        * Typical use would be: find all user account definitions in the target
        * directory, having the user-num attribute 0, and copy from the source
        * directory some account extensions like created-date, user-num, user-level,
        * key.
        *
        * @throws  ConfigurationException
        *          In case of configuration error.
        */
       private void selectiveCopy(String[] args)
       throws ConfigurationException
       {
          // check arguments
          if (args.length != 8 && args.length != 9)
          {
             //                                         0             1
             say("Usage: java DirectoryCopy selcopy <source-xml> <target-xml> "  +
             //     2                3           4             5          6
                 "<path>\n       node-class subnode-class subnode-name attr-name " +
             //      7                  8
                 "attr-value [quoted-subnodes-to-copy]");
             return;
          }

          // Empty BootstrapConfigs
          BootstrapConfig bcs = new BootstrapConfig(null, null, null, null);
          BootstrapConfig bct = new BootstrapConfig(null, null, null, null);

          // directory package configuration
          bcs.setServer(true);
          bcs.setConfigItem("directory", "backend", "type",     "xml");
          bcs.setConfigItem("directory", "xml",     "filename", args[0]);

          bct.setServer(true);
          bct.setConfigItem("directory", "backend", "type",     "xml");
          bct.setConfigItem("directory", "xml",     "filename", args[1]);

          // initialize directory schema
          SchemaLoad.initSchema();

          // instantiating directory tree backends
          Remapper rs = DirectoryService.genRemapper(bcs);
          Remapper rt = DirectoryService.genRemapper(bct);

          boolean rc = true;

          // binding to the directories
          rc = rs.bind();

          if (!rc)
          {
             say("Binding to source directory failed");
             return;
          }

          rc = rt.bind();

          if (!rc)
          {
             say("Binding to target directory failed");
             rs.unbind();
             return;
          }

          // check the branch for existence
          if (rs.getNodeClassName(args[2]) == null)
          {
             say("no starting node in the source directory");
             rs.unbind();
             rt.unbind();
             return;
          }

          if (rt.getNodeClassName(args[2]) == null)
          {
             say("no starting node in the target directory");
             rs.unbind();
             rt.unbind();
             return;
          }

          // check the node, subnode class validity
          String[] allClasses = rs.getClassNames();
          List<String> classList = allClasses == null ? null : Arrays.asList(allClasses);
          if (classList == null || !classList.contains(args[3]))
          {
             say("no such node class: " + args[3]);
             rs.unbind();
             rt.unbind();
             return;
          }

          if (!classList.contains(args[4]))
          {
             say("no such node class: " + args[4]);
             rs.unbind();
             rt.unbind();
             return;
          }

          allClasses = rt.getClassNames();
          classList = allClasses == null ? null : Arrays.asList(allClasses);
          if (classList == null || !classList.contains(args[3]))
          {
             say("no such node class: " + args[3]);
             rs.unbind();
             rt.unbind();
             return;
          }

          if (!classList.contains(args[4]))
          {
             say("no such node class: " + args[4]);
             rs.unbind();
             rt.unbind();
             return;
          }

          // check the attribute name validity
          AttributeDefinition[] attrs = rs.getClassDefinition(args[4]);
          if (attrs == null || attrs.length == 0)
          {
             say("class " + args[4] + " has no attributes");
             rs.unbind();
             rt.unbind();
             return;
          }

          AttributeDefinition attr = null;

          for (int i = 0; i < attrs.length; i++)
          {
             if (attrs[i].getName().equalsIgnoreCase(args[6]))
             {
                attr = attrs[i];
                break;
             }
          }

          if (attr == null)
          {
             say("class " + args[4] + " has no attribute " + args[6]);
             say("valid attribute names are:");
             for (int i = 0; i < attrs.length; i++)
             {
                say("   " + attrs[i].getName());
             }
             rs.unbind();
             rt.unbind();
             return;
          }

          attrs = rt.getClassDefinition(args[4]);
          if (attrs == null || attrs.length == 0)
          {
             say("class " + args[4] + " has no attributes");
             rs.unbind();
             rt.unbind();
             return;
          }

          for (int i = 0; i < attrs.length; i++)
          {
             if (attrs[i].getName().equalsIgnoreCase(args[6]))
             {
                attr = attrs[i];
                break;
             }
          }

          if (attr == null)
          {
             say("class " + args[4] + " has no attribute " + args[6]);
             say("valid attribute names are:");
             for (int i = 0; i < attrs.length; i++)
             {
                say("   " + attrs[i].getName());
             }
             rs.unbind();
             rt.unbind();
             return;
          }

          // process copy-list
          String[] cpys = null;

          if (args.length == 9 && args[8].length() > 0)
          {
             cpys = args[8].trim().split("(\\p{Space})+");
             if (cpys.length == 0)
             {
                cpys = null;
             }
          }

          // selectively copy nodes between directories
          rc = selectiveCopyWorker(rs, rt, args[2], args[3], args[4], args[5],
                                   args[6], args[7], cpys);

          say(String.format("Nodes skipped %d, checked %d, changed %d, errors %d",
                            skipped, found, changed, errors));

          if (found == 0)
          {
             say("no nodes of the class " + args[3] + " found");
             rs.unbind();
             rt.unbind();
             return;
          }

          if (changed == 0)
          {
             say("no matching values found");
             rs.unbind();
             rt.unbind();
             return;
          }

          if (changed > errors)
          {
             // not all changes failed; there is something to save
             rc = rt.update();
             if (!rc)
             {
                say("committing failed");
             }
             else
             {
                say("success");
             }
          }

          if (errors > 0)
          {
             say("some copy operations failed");
          }

          // unbinding from the directories
          rc = rs.unbind();

          if (!rc)
          {
             say("Unbinding from source directory failed");
          }

          rc = rt.unbind();

          if (!rc)
          {
             say("Unbinding from target directory failed");
          }
       }

       /**
        * Selective copy worker.
        *
        * @return <code>true</code> if success
        */
       private boolean selectiveCopyWorker(Remapper rs, Remapper rt, String path,
                                           String nclass, String sclass,
                                           String subnode, String attrName,
                                           String val, String[] cpys)
       {
          // enumerate the nodes under the given path in target
          String[] nodes = rt.enumerateNodes(path);
          if (nodes == null)
          {
             say("no children at all");
             return true;
          }

          // filter the nodes by class
          for (int i = 0; i < nodes.length; i++)
          {
             String nodeId = path + "/" + nodes[i];
             String nodeClass = rt.getNodeClassName(nodeId);

             if (!nodeClass.equalsIgnoreCase(nclass))
             {
                say("skipping " + nodeId + " " + nodeClass);
                skipped++;
                continue;
             }

             say_("checking " + nodeId + " " + nodeClass);
             found++;

             // check if there is named subnode of the given class
             String subNodeId = nodeId + "/" + subnode;
             String subNodeClass = rt.getNodeClassName(subNodeId);
             if (subNodeClass == null || !subNodeClass.equalsIgnoreCase(sclass))
             {
                say(", skipping (no subnode)");
                skipped++;
                continue;
             }

             // enumarate existing attributes
             Attribute atv = null;

             Attribute[] att = DirectoryService.getNodeAttributes(rt, subNodeId);
             if (att == null || att.length == 0)
             {
                say(", skipping (no subnode attributes)");
                skipped++;
                continue;
             }

             for (int j = 0; j < att.length; j++)
             {
                if (att[j].getName().equalsIgnoreCase(attrName))
                {
                   atv = att[j];
                   break;
                }
             }

             if (atv == null)
             {
                say(", skipping (no " + attrName + " attribute");
                skipped++;
                continue;
             }

             // filtering by a specific value
             String value = atv.getStringValue(0);
             if (value == null)
             {
                say(", skipping (no values)");
                skipped++;
                continue;
             }

             if (!val.equalsIgnoreCase(value))
             {
                say(", skipping (value mismatch)");
                skipped++;
                continue;
             }

             // found a match; copy listed subnodes from the source
             say(", copying");

             if (cpys == null)
             {
                continue;
             }

             for (int j = 0; j < cpys.length; j++)
             {
                say_("                " + cpys[j]);
                changed++;
                boolean rc = DirectoryService.copySubtree(
                             rs, nodeId + "/" + cpys[j],
                             rt, nodeId + "/" + cpys[j], true);
                if (rc)
                {
                   say(", OK");
                }
                else
                {
                   say(", failed");
                   errors++;
                }
             }
          }

          return errors == 0;
       }

       /**
        * Attribute Value Set worker. It scans the specified branch
        * of the directory, selects all nodes of the given class and deletes
        * the specified attribute entirely or just the specified value.
        */
       private void setWorker(Remapper rt, String node, String nclass,
                                String nattr, int attrType, Object val, int index)
       {
          // process this node first
          setNodeWorker(rt, node, nclass, nattr, attrType, val, index);

          // enumerate immediate children
          String[] nodes = rt.enumerateNodes(node);
          if (nodes == null)
          {
             return;
          }

          // scan the children
          for (int i = 0; i < nodes.length; i++)
          {
             setWorker(rt, node + "/" + nodes[i], nclass, nattr, attrType, val,
                       index);
          }
       }

       /**
        * Attribute Value Set node worker. It scans the specified branch
        * of the directory, selects all nodes of the given class and deletes
        * the specified attribute entirely or just the specified value.
        *
        */
       private void setNodeWorker(Remapper rt, String nodeId, String nclass,
                                  String nattr, int attrType, Object val,
                                  int index)
       {
          boolean rc = true;

          String nodeClass = rt.getNodeClassName(nodeId);

          if (!nodeClass.equalsIgnoreCase(nclass))
          {
             say("skipping " + nodeId + " " + nodeClass);
             skipped++;
             return;
          }

          say_("checking " + nodeId + " " + nodeClass);
          found++;

          // enumerate existing attributes
          Attribute atv = null;

          Attribute[] att = DirectoryService.getNodeAttributes(rt, nodeId);
          if (att != null && att.length > 0)
          {
             for (int j = 0; j < att.length; j++)
             {
                if (att[j].getName().equalsIgnoreCase(nattr))
                {
                   atv = att[j];
                   break;
                }
             }
          }

          // conditional setting of a specific value
          String[] values = new String[] {};
          if (atv != null)
          {
             values = atv.getValues();
          }

          if (index > values.length)
          {
             say(String.format(", skipping - only %d values", values.length));
             skipped++;
          }
          else
          {
             changed++;
             boolean add = index == values.length;

             switch(attrType)
             {
                case AttributeType.ATTR_INTEGER:
                   int ival = ((Integer)val).intValue();
                   if (add)
                   {
                      rc = rt.addNodeInteger(nodeId, nattr, ival);
                   }
                   else
                   {
                      rc = rt.setNodeInteger(nodeId, nattr, index, ival);
                   }
                   break;

                case AttributeType.ATTR_BOOLEAN:
                   boolean bval = ((Boolean)val).booleanValue();
                   if (add)
                   {
                      rc = rt.addNodeBoolean(nodeId, nattr, bval);
                   }
                   else
                   {
                      rc = rt.setNodeBoolean(nodeId, nattr, index, bval);
                   }
                   break;

                case AttributeType.ATTR_STRING:
                   if (add)
                   {
                      rc = rt.addNodeString(nodeId, nattr, (String)val);
                   }
                   else
                   {
                      rc = rt.setNodeString(nodeId, nattr, index, (String)val);
                   }
                   break;

                case AttributeType.ATTR_DOUBLE:
                   double dval = ((Double)val).doubleValue();
                   if (add)
                   {
                      rc = rt.addNodeDouble(nodeId, nattr, dval);
                   }
                   else
                   {
                      rc = rt.setNodeDouble(nodeId, nattr, index, dval);
                   }
                   break;

                case AttributeType.ATTR_BYTEARRAY:
                   if (add)
                   {
                      rc = rt.addNodeByteArray(nodeId, nattr, (byte[])val);
                   }
                   else
                   {
                      rc = rt.setNodeByteArray(nodeId, nattr, index, (byte[])val);
                   }
                   break;

                case AttributeType.ATTR_BITFIELD:
                   if (add)
                   {
                      rc = rt.addNodeBitField(nodeId, nattr, (BitField)val);
                   }
                   else
                   {
                      rc = rt.setNodeBitField(nodeId, nattr, index, (BitField)val);
                   }
                   break;

                case AttributeType.ATTR_BITSELECTOR:
                   if (add)
                   {
                      rc = rt.addNodeBitSelector(nodeId, nattr, (BitSelector)val);
                   }
                   else
                   {
                      rc = rt.setNodeBitSelector(nodeId, nattr, index,
                                                 (BitSelector)val);
                   }
                   break;

                case AttributeType.ATTR_DATE:
                   if (add)
                   {
                      rc = rt.addNodeDate(nodeId, nattr, (DateValue)val);
                   }
                   else
                   {
                      rc = rt.setNodeDate(nodeId, nattr, index, (DateValue)val);
                   }
                   break;

                case AttributeType.ATTR_TIME:
                   if (add)
                   {
                      rc = rt.addNodeTime(nodeId, nattr, (TimeValue)val);
                   }
                   else
                   {
                      rc = rt.setNodeTime(nodeId, nattr, index, (TimeValue)val);
                   }
                   break;
             }

             if (rc)
             {
                say(", set");
             }
             else
             {
                errors++;
                say(", setting failed");
             }
          }
       }

       /**
        * Removes the specified directory node, including all child nodes.
        *
        * @param    rm
        *           A bound directory remapper
        *
        * @param    nodeId
        *           ID of node to remove
        *
        * @return   <code>true</code> if removed the node successfully
        */
       private static boolean removeDirectoryNode(Remapper rm, String nodeId)
       {
          // enumerate and remove all children first, recursively
          String[] child = rm.enumerateNodes(nodeId);
          if (child == null)
          {
             return false;
          }

          for (int i = 0; i < child.length; i++)
          {
             if (!removeDirectoryNode(rm, nodeId + "/" + child[i]))
             {
                return false;
             }
          }

          // remove the specified node now
          return rm.deleteNode(nodeId);
       }

       /**
        * Application entry point. Since more than one operation cab be performed,
        * the operation has to be specified with the first argument.
        *
        * @param   args
        *          Command line parameters. The first one specifies the utility
        *          and the rest is utility specific. The currently implemented
        *          utilities are:
        *          <ul>
        *             <li>protcopy - protected copy which copies the contents of
        *                            the source encrypted directory to the target
        *                            encrypted directory
        *             <li>copy - works with unencrypted directories; copies a
        *                        branch of the source directory to some insertion
        *                        point in the target directory
        *             <li>remove - works with unencrypted directories; removes a
        *                        branch of the target directory
        *             <li>clear - works with unencrypted directories; removes an
        *                         attribute or one of its values
        *             <li>set - works with unencrypted directories; sets the given
        *                         attribute to the given value
        *             <li>selcopy - works with unencrypted directories; selectively
        *                           copies some subnodes from the source directory
        *                           to the target directory
        *          </ul>
        */
       public static void main(String[] args)
       {
          // inspect the first argument and identify the operation
          int argn = args.length;
          int oper = 0;
          boolean fail = false;

          if (argn == 0)
          {
             fail = true;
          }
          else
          {
             if (args[0].equalsIgnoreCase("protcopy"))
             {
                oper = 1;
             }
             else if (args[0].equalsIgnoreCase("copy"))
             {
                oper = 2;
             }
             else if (args[0].equalsIgnoreCase("remove"))
             {
                oper = 3;
             }
             else if (args[0].equalsIgnoreCase("clear"))
             {
                oper = 4;
             }
             else if (args[0].equalsIgnoreCase("set"))
             {
                oper = 5;
             }
             else if (args[0].equalsIgnoreCase("selcopy"))
             {
                oper = 6;
             }
             else
             {
                fail = true;
             }
          }

          if (fail)
          {
             say("A valid operation should be specified");
             say("Valid operations are:");
             say("   protcopy");
             say("   copy");
             say("   remove");
             say("   clear");
             say("   set");
             say("   selcopy");
             say("Type  <operation ?> for more help");
             System.exit(1);
          }

          // prepare the parameters for the operation
          argn--;
          String[] pars = new String[argn];
          System.arraycopy(args, 1, pars, 0, argn);

          // instantiate the class
          DirectoryCopy dir = new DirectoryCopy(pars);

          // invoke the utility
          try
          {
             switch(oper)
             {
                case 1:
                   dir.protCopy();
                   break;

                case 2:
                   dir.copy();
                   break;

                case 3:
                   dir.remove();
                   break;

                case 4:
                   dir.clear(pars);
                   break;

                case 5:
                   dir.set(pars);
                   break;

                case 6:
                   dir.selectiveCopy(pars);
                   break;
             }
          }
          catch (ConfigurationException e)
          {
             LOG.severe("", e);
          }
       }
    }