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