DirectoryEdit.java

/*
** Module   : DirectoryEdit.java
** Abstract : general purpose interactive directory editor
**
** Copyright (c) 2005-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 NVS 20050428   @21012 Created the initial version which can handle
**                           editing attribute values and printing.
** 002 NVS 20050505   @21125 The default timeout was too short for LDAP
**                           directory accesses. Raised it to 60 seconds.
**                           Rewritten the top level menu. Now the bind()
**                           operation is done right away and unbind() is
**                           done when exiting the application.
** 003 NVS 20050518   @21228 Changed top menu to allow empty batch path,
**                           which is the directory root.
** 004 SIY 20050520   @21265 Rewritten with different menu model, finished
**                           remaining unimplemented items, added missing 
**                           imported methods, many cleanups and changes
**                           in the formatting and comments.
** 005 NVS 20050527   @21315 Renamed to DirectoryEdit and promoted to the status of utility.
** 006 NVS 20050902   @22447 New directory based EIR registration puts 
**                           limitations on exported names. "directory" 
**                           group exports are renamed to comply.
** 007 NVS 20050914   @22706 DirectoryEdit is a TTY-based utility and as
**                           such has to suspend CHARVA on entry.
** 008 NVS 20050920   @22758 Fixed empty command processing and CHARVA detection.
** 009 GES 20070111   @31803 Removed unnecessary use of notifiable. This
**                           class is currently non-functional but is
**                           being retained for future re-enablement.
** 010 GES 20070115   @31840 Match minor interface change in base class.
** 011 EVL 20070609   @34005 Adding explicit import of the class
**                           com.goldencode.p2j.net.Queue to eliminate
**                           conflict with the same class from java.util
**                           package to be able to compile for Java 6.
** 012 ECF 20071106   @35904 Replaced Queue with Session.
** 013 ECF 20150715          Replace StringBuffer with StringBuilder.
** 014 ECF 20150828          Enable Java 8.
** 015 TJD 20220504          Java 11 compatibility minor changes
*/
/*
** 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.test;

import java.io.*;
import java.util.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.directory.Base64;
import com.goldencode.p2j.net.*;

/**
 * General purpose interactive directory editor.
 * <p>
 * Note that the use of <code>HighLevelObject</code> is not needed (and is
 * a significant amount of useless code) once we move the directory interfaces
 * into a <code>RemoteObject</code> approach.  Much of this code also exists
 * inside the <code>DirectoryEdit</code> class.  Very nasty.
 */
public class DirectoryEdit
{
   /** Current attribute name */
   String attrName = null;
   
   /** Locked branch */
   String branch = null;

   /** instance of HLO class that provides methods for directory access */
   ServerDirectory dir = null;

   /** Node menu definition/implementation */
   MenuItem[] nodeMenu = new MenuItem[]
   {
      new MenuItem(" ~.             Print menu")
         { void run(String[] args) { menuNodePrint(args); }},

      new MenuItem(" ~l             List attributes and nodes")
         { void run(String[] args) { menuNodeList(args); }},

      new MenuItem(" ~t             List attribute definitions")
         { void run(String[] args) { menuNodeListDefs(args); }},
         
      new MenuItem(" ~E node        Edit node")
         { void run(String[] args) { menuNodeEdit(args); }},

      new MenuItem(" ~C node [class] Create child node")
         { void run(String[] args) { menuNodeCreate(args); }},

      new MenuItem(" ~D node        Delete child node")
         { void run(String[] args) { menuNodeDelete(args); }},

      new MenuItem(" ~M node1 node2 Move node")
         { void run(String[] args) { menuNodeMove(args); }},

      new MenuItem(" ~e attribute   Edit node attribute")
         { void run(String[] args) { menuNodeEditAttribute(args); }},

      new MenuItem(" ~c attribute   Create node attribute")
         { void run(String[] args) { menuNodeCreateAttribute(args); }},

      new MenuItem(" ~d attribute   Delete node attribute")
         { void run(String[] args) { menuNodeDeleteAttribute(args); }},

      new MenuItem(" ~q             Quit")
         { void run(String[] args) { menuGoTop(args); }},
   };

   /** Top level menu definition/implementation */
   MenuItem[] topMenu = new MenuItem[]
   {
      new MenuItem(" ~.        Print menu")
         { void run(String[] args) { menuNodePrint(args); }},

      new MenuItem(" ~b node   Open batch for node")
         { void run(String[] args) { menuTopBatch(args); }},

      new MenuItem(" ~b        Back to batch node")
         { void run(String[] args) { menuNodePrint(args); }},

      new MenuItem(" ~c        Commit batch")
         { void run(String[] args) { menuTopCommit(args); }},

      new MenuItem(" ~r        Rollback batch")
         { void run(String[] args) { menuTopRollback(args); }},

      new MenuItem(" ~q        Quit")
         { void run(String[] args) { menuTopQuit(args); }},
   };

   /** Attribute editing menu */
   private MenuItem[] attrMenu = new MenuItem[]
   {
      new MenuItem(" ~.        Print menu")
         { void run(String[] args) { menuNodePrint(args); }}, 
      new MenuItem(" ~l        List values")
         { void run(String[] args) { menuAttrList(args); }}, 
      new MenuItem(" ~e n      Edit value n")
         { void run(String[] args) { menuAttrEdit(args); }}, 
      new MenuItem(" ~d n      Delete value n")
         { void run(String[] args) { menuAttrDelete(args); }}, 
      new MenuItem(" ~a        Add new value")
         { void run(String[] args) { menuAttrAdd(args); }}, 
      new MenuItem(" ~q        Quit")
         { void run(String[] args) { menuGoTop(args); }},
   };

   /**
    * Construct an instance.
    * 
    * @param   session
    *          Reference to <code>Session</code>.
    */
   public DirectoryEdit(Session session)
   {
      dir = new ServerDirectory(session);
   }

   /**
    * A shortcut for getting user input.
    * 
    * @return  User input as a string. The new line character is not part of
    *          string.
    */
   static String in()
   {
      StringBuilder sb = new StringBuilder();
      char ch = 0;

      while (true)
      {
         try
         {
            ch = (char)System.in.read();
         }
         catch (IOException x)
         {
            break;
         }
         if (ch == '\n')
            break;
         sb.append(ch);
      }

      return new String(sb);
   }

   /** A shortcut for the System.out.println() */
   static void say()
   {
      System.out.println();
   }

   /**
    * A shortcut for the System.out.println(String).
    * 
    * @param   string
    *          String to print.
    */
   static void say(String string)
   {
      System.out.println(string);
   }

   /**
    * Adds a value to node's attribute.
    * 
    * @param   node
    *          Node name.
    * @param   attr
    *          attribute name.
    * @param   newVal
    *          new value to be set.
    * @return  <code>true</code> if operation was successful.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   boolean addValue(String node, String attr, String newVal)
   throws Exception
   {
      //int i;
      String className = getNodeClass(node);

      NodeAttribute nAttr = dir.getClassNodeAttribute(className, attr);

      if (nAttr == null)
         return false;

      int type = nAttr.getType();
      boolean res = false;

      switch (type)
      {
         case AttributeType.ATTR_INTEGER :
            res = dir.addNodeInteger(node, attr, Integer.parseInt(newVal));
            break;

         case AttributeType.ATTR_BOOLEAN :
            Boolean boVal = Boolean.valueOf(newVal);
            boolean bVal = boVal.booleanValue();
            res = dir.addNodeBoolean(node, attr, bVal);
            break;

         case AttributeType.ATTR_STRING :
            res = dir.addNodeString(node, attr, newVal);
            break;

         case AttributeType.ATTR_DOUBLE :
            res = dir.addNodeDouble(node, attr, Double.parseDouble(newVal));
            break;

         case AttributeType.ATTR_BYTEARRAY :
            res = dir.addNodeByteArray(node, attr,
                                       Base64.base64ToByteArray(newVal));
            break;

         case AttributeType.ATTR_BITFIELD :
            res = dir.addNodeBitField(node, attr, BitField.valueOf(newVal));
            break;

         case AttributeType.ATTR_BITSELECTOR :
            res = dir.addNodeBitSelector(
                                         node,
                                         attr,
                                         (BitSelector)BitSelector.valueOf(newVal));
            break;

         case AttributeType.ATTR_DATE :
            res = dir.addNodeDate(node, attr, DateValue.valueOf(newVal));
            break;

         case AttributeType.ATTR_TIME :
            res = dir.addNodeTime(node, attr, TimeValue.valueOf(newVal));
            break;

         default:
            ;
      }

      return res;
   }

   /**
    * Combine current branch ID and command arguments into resulting node ID.
    * <p>
    * If command arguments are missing then <code>null</code> is returned
    * indicating error, unless returning base branch ID is forced by setting
    * <code>force</code> parameter to <code>true</code>.
    * 
    * @param   args
    *          Command arguments.
    * @param   force
    *          If set to <code>true</code> then resulting ID is returned
    *          regardless from the presence of the parameters.
    * @return  Resulting node ID.
    */
   String getFullId(String[] args, boolean force)
   {
      String node = branch;

      if (args != null && args.length > 1)
      {
         node += "/" + args[1];

         return node;
      }

      return (force) ? node : null;
   }

   /**
    * Get node class name without leading prefix.
    * 
    * @param   node
    *          Node name.
    * @return  Node object class name.
    */
   String getNodeClass(String node)
   {
      try
      {
         String className = dir.getNodeClass(node);
         
         if (className != null)
            className = className.substring(className.lastIndexOf("/") + 1);
         return className;
      }
      catch (Exception e)
      {
         return null;
      }
   }

   /**
    * Gets the number of values of node's attribute.
    * 
    * @param   node
    *          Node name.
    * @param   attr
    *          Attribute name.
    * @return  Number of values in the specified attribute of specified node.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   int getNumValues(String node, String attr)
   throws Exception
   {
      //int i;
      String className = getNodeClass(node);

      NodeAttribute nAttr = dir.getClassNodeAttribute(className, attr);

      if (nAttr == null)
         return 0;

      int type = nAttr.getType();

      // getting values
      Object[] values = null;

      switch (type)
      {
         case AttributeType.ATTR_INTEGER :
            values = dir.getNodeIntegers(node, attr);
            break;

         case AttributeType.ATTR_BOOLEAN :
            values = dir.getNodeBooleans(node, attr);
            break;

         case AttributeType.ATTR_STRING :
            values = dir.getNodeStrings(node, attr);
            break;

         case AttributeType.ATTR_DOUBLE :
            values = dir.getNodeDoubles(node, attr);
            break;

         case AttributeType.ATTR_BYTEARRAY :
            values = dir.getNodeByteArrays(node, attr);
            break;

         case AttributeType.ATTR_BITFIELD :
            values = dir.getNodeBitFields(node, attr);
            break;

         case AttributeType.ATTR_BITSELECTOR :
            values = dir.getNodeBitSelectors(node, attr);
            break;

         case AttributeType.ATTR_DATE :
            values = dir.getNodeDates(node, attr);
            break;

         case AttributeType.ATTR_TIME :
            values = dir.getNodeTimes(node, attr);
            break;

         default:
            ;
      }

      if (values == null)
         return 0;

      return values.length;
   }
   
   /**
    * Check note if it is a terminal node and does not allow creation of child
    * nodes.
    * 
    * @param   nodeId
    *          Node ID.
    * @return  <code>true</code> if node is a terminal node.
    */
   boolean isNodeTerminal(String nodeId)
   {
      try
      {
         String nodeClass = dir.getNodeClass(nodeId);
         
         if (nodeClass != null)
         {
            Boolean[] res = dir.getNodeBooleans(nodeClass, "terminal");
            
            if (res != null && res[0].booleanValue())
               return true;
         }
      }
      catch (Exception e)
      {
      }
      
      return false;
   }

   /**
    * Lists the batch branch of the directory.
    * 
    * @param   startingPoint
    *          Path (id) of the node to print
    * @throws  Exception
    *          Forwarded from various methods.
    */
   void listNodes(String startingPoint)
   throws Exception
   {
      printNodeAttrs(startingPoint, 1);
      printChildren(startingPoint, 1);
   }

   /**
    * Attribute menu - add attribute value implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuAttrAdd(String[] args)
   {
      if (args == null)
      {
         //Do nothing, just make Eclipse happy.
      }
      
      System.out.print("Enter new attribute value:");
      String value = in();

      try
      {
         if (addValue(branch, attrName, value))
            say("Value added successfully");
         else
            say("Operation failed");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Attribute menu - delete attribute value implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuAttrDelete(String[] args)
   {
      if (args.length != 2)
      {
         say("Value index needed");
         return;
      }
      
      try
      {
         int n = Integer.parseInt(args[1]);

         if (n < 0 || n > (getNumValues(branch, attrName) - 1))
         {
            say("Invalid index");
            return;
         }

         if (dir.deleteNodeAttributeValue(branch, attrName, n))
            say("Value deleted successfully");
         else
            say("Operation failed");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Attribute menu - edit attribute value implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuAttrEdit(String[] args)
   {
      if (args.length != 2)
      {
         say("Value index needed");
         return;
      }
      
      try
      {
         int n = Integer.parseInt(args[1]);

         if (n < 0 || n > (getNumValues(branch, attrName) - 1))
         {
            say("Invalid index");
            return;
         }
         
         String value = in();
         
         if (setValue(branch, attrName, n, value))
            say("Value changed successfully");
         else
            say("Operation failed");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Attribute menu - list attribute value implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuAttrList(String[] args)
   {
      if (args == null)
      {
         //Do nothing, just make Eclipse happy.
      }
      
      try
      {
         printAttrDef(branch, attrName);
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Return to previous level of menu.
    * 
    * @param   args
    *          Command arguments (ignored).
    */
   void menuGoTop(String[] args)
   {
      if (args != null && args.length > 1)
         say("Redundant parameters ignored");
      
      say("Returning to previous menu");
      
      throw new StopMenu();
   }

   /**
    * Node menu - create object implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeCreate(String[] args)
   {
      String fullId = getFullId(args, false);
      
      if (fullId == null)
      {
         say("Node ID is required");
         return;
      }
      
      if (isNodeTerminal(branch))
      {
         say("Node it terminal. Menu item is disabled.");
      }

      try
      {
         String objClass;
         
         if (args.length > 2)
            objClass = args[2];
         else
         {
            System.out.print("Enter object class name>");
            System.out.flush();
            objClass = in();
         }
         
         if (!nodeExists("/meta/class/" + objClass))
         {
            say("Object class does not exists");
            return;
         }
         
         NodeAttribute[] list = dir.getClassNodeAttributes(objClass);
         
         Vector attrs = new Vector();
         Vector unfilled = new Vector();
         
         if (list != null)
         {
            //Check for presence of the mandatory attributes
            for (int i = 0; i < list.length; i++)
            {
               if(list[i].isMandatory())
               {
                  Object value = null;
                  Attribute attr = null;
                  
                  while (true)
                  {
                     //say("Object class defines mandatory attribute \"" + 
                     //    list[i].getName() + "\" (" +  + ")");
                     printDef(list[i], "Required attribute: ");
                     System.out.print("Enter value>");
                     System.out.flush();
                     
                     String str = in();
                     
                     value = stringToAttrValue(list[i], str);
                     
                     if (value != null)
                     {
                        attr = new Attribute(list[i], new Object[] {value});
                        
                        if (attr.isValid())
                           break;
                     }
                     
                     say("Entered value is invalid.");
                  }

                  attrs.add(attr);
               }
               else
               {
                  unfilled.add(list[i]);
               }
            }
            
            if (unfilled.size() > 0)
            {
               say("Enter <name value> pairs to add value to the attribute.");
               say("Empty string breaks the loop.");
               say("Following optional attributes can be added now:");
               
               for (int i = 0; i < unfilled.size(); i++)
               {
                  printDef((NodeAttribute)unfilled.get(i), "" + i + ") ");
               }
               say();
               
               while (true)
               {
                  Object value;
                  System.out.print("Enter name and value for attribute>");
                  System.out.flush();
                  
                  String[] params = in().split(" ", 2);
                  
                  if (params[0].equals(""))
                     break;
                  
                  String val = null;
                  
                  if (params.length < 2)
                     val = "";   //Empty value
                  else
                     val = params[1];
                  
                  //Try to locate attribute in the array of already filled 
                  //ones.
                  
                  Attribute attr = null;
                  boolean found = false;
                  
                  for (int i = 0; i < attrs.size(); i++)
                  {
                     Attribute tmpAttr =(Attribute)attrs.get(i);
                     
                     if(tmpAttr.getName().equals(params[0]))
                     {
                        value = stringToAttrValue(tmpAttr.getDefinition(), 
                                                  val);
                        
                        attr = new Attribute(tmpAttr);
                        
                        if (attr.addValue(value) && attr.isValid())
                        {
                           say("Added new value to attribute [" + params[0] + 
                               "].");
                           attrs.set(i, attr);
                        }
                        else
                           say("Entered value is invalid.");

                        found = true;
                        break;
                     }
                  }
                  
                  if (found)
                     continue;

                  //Try to locate attribute definition
                  
                  for (int i = 0; i < list.length; i++)
                  {
                     if(list[i].getName().equals(params[0]))
                     {
                        value = stringToAttrValue(list[i], val);
                        
                        if (value != null)
                        {
                           attr = new Attribute(list[i], new Object[] 
                                                                    {value});
                           
                           if (attr.isValid())
                           {
                              say("Added new attribute [" + params[0] + "].");
                              attrs.add(attr);
                           }
                           else
                              say("Entered value is invalid.");

                           found = true;
                           break;
                        }
                     }
                  }
                  
                  if (found)
                     continue;
                  
                  say("Unknown attribute name [" + params[0] + "].");
               }
            }
         }
         
         boolean rc = false;
         
         if (attrs.size() == 0)
            rc = dir.addNode(fullId, objClass, null);
         else
            rc = dir.addNode(fullId, objClass, 
                             (Attribute[])attrs.toArray(new Attribute[0]));

         if (rc)
            say("Node created successfully.");
         else
            say("Node creation failed.");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - create attribute implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeCreateAttribute(String[] args)
   {
      if (args.length != 2)
      {
         say("Attribute name is required.");
         return;
      }
      
      System.out.print("Enter attribute value>");
      String value = in();

      try
      {
         if (addValue(branch, args[1], value))
            say("Attribute value added successfully.");
         else
            say("Operation failed.");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - delete object implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeDelete(String[] args)
   {
      String fullId = getFullId(args, false);

      if (fullId == null)
      {
         say("Node ID is required.");
         return;
      }

      try
      {
         if (!nodeExists(fullId))
            say(args[1] + " not found.");
         else
         {
            boolean rc = dir.deleteNode(fullId);

            if (rc)
               say("Node deleted successfully.");
            else
               say("Operation failed.");
         }
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - delete attribute implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeDeleteAttribute(String[] args)
   {
      if (args.length != 2)
      {
         say("Attribute name required.");
         return;
      }
      
      try
      {
         if (!printAttrDef(branch, args[1]))
         {
            say(args[1] + " not found.");
            return;
         }

         if (dir.deleteNodeAttribute(branch, args[1]))
            say("Attribute deleted successfully.");
         else
            say("Operation failed.");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }      
   }

   /**
    * Node menu - edit object implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeEdit(String[] args)
   {
      try
      {
         String nodeId = getFullId(args, false);

         if (nodeId == null)
         {
            say("Node ID is required.");
            return;
         }

         if (!nodeExists(nodeId))
         {
            say(nodeId + " not found.");
            return;
         }

         String save = branch;
         branch = nodeId;
         runNodeMenu();
         branch = save;
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - edit attribute implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeEditAttribute(String[] args)
   {
      if (args.length != 2)
      {
         say("Attribute name required.");
         return;
      }
      
      try
      {
         String className = getNodeClass(branch);
         
         if (className == null)
         {
            say("Unable to determine node class name.");
            return;
         }
         
         NodeAttribute nAttr = dir.getClassNodeAttribute(className, args[1]);
 
         if (nAttr == null)
         {
            say("No such attribute.");
            return;
         }
         
         attrName = args[1];
         
         run(branch + "." + args[1] + " menu:", attrMenu, null);

         attrName = null;
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - list attributes and objects implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeList(String[] args)
   {
      try
      {
         String nodeId = getFullId(args, true);
         listNodes(nodeId);
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - list definitions of attributes implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeListDefs(String[] args)
   {
      if (args == null)
      {
         //Do nothing, just make Eclipse happy.
      }
      
      try
      {
         printAttrDefs(branch);
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - move node implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodeMove(String[] args)
   {
      if (args.length != 3)
      {
         say("Source node ID and destination node ID are required.");
         return;
      }

      try
      {
         String fromID = getFullId(args, false);
         String toID = args[2];

         if (!toID.startsWith("/"))
            toID = branch + "/" + toID;

         boolean rc = dir.moveNode(fromID, toID);

         say("Node moving " + (rc ? "succeeded" : "failed"));
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Node menu - print menu implementation.
    * 
    * @param   args
    *          Parsed command.
    */
   void menuNodePrint(String[] args)
   {
      //Do nothing
      if (args == null)
         return;
   }

   /**
    * Top menu - open batch implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuTopBatch(String[] args)
   {
      if (args == null)
      {
         say("Invalid input.");
         return;
      }

      if (branch != null)
      {
         runNodeMenu();
         return;
      }
      
      boolean rc = false;
      
      if (args.length > 1)
         branch = args[1];
      else
      {
         if (args[0].length() < 2)
         {
            say("Node ID is required.");
            return;
         }
         
         branch = args[0].substring(2).trim();
      }
      
      say("Opening batch for " + (branch.equals("") ? "root":branch));
      
      try
      {
         rc = dir.openBatch(branch);
         
         if (rc)
            runNodeMenu();
         else
            say("Batch open failed.");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
      
      if (!rc)
         branch = null;
   }

   /**
    * Top menu - commit batch implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuTopCommit(String[] args)
   {
      if (args == null)
      {
         //Do nothing, just make Eclipse happy.
      }

      if (branch == null)
      {
         say("Batch is not open.");
         return;
      }

      try
      {
         if (dir.closeBatch(true))
            say("Changes committed successfully.");
         else        
            say("Operation failed.");
      }
      catch (StopMenu s)
      {
         throw s;
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }

      branch = null;
   }

   /**
    * Top menu - quit implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuTopQuit(String[] args)
   {
      if (args == null)
      {
         //Do nothing, just make Eclipse happy.
      }

      if (branch != null)
      {
         say("Batch is still open. Commit or rollback it before exit.");
         return;
      }

      say("Exiting.");

      throw new StopMenu();
   }

   /**
    * Top menu - rollback batch implementation.
    * 
    * @param   args
    *          Command arguments.
    */
   void menuTopRollback(String[] args)
   {
      if (args == null)
      {
         //Do nothing, just make Eclipse happy.
      }

      if (branch == null)
      {
         say("Batch is not open.");
         return;
      }

      try
      {
         if (dir.closeBatch(false))
            say("All changes are rolled back.");
         else
            say("Operation failed.");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }

      branch = null;
   }

   /**
    * Check existence of the node.
    * 
    * @param   nodeId
    * @return  <code>true</code> is specified node exists.
    * @throws  Exception
    *          Forwarded from underlying call.
    */
   boolean nodeExists(String nodeId)
   throws Exception
   {
      return (dir.getNodeClass(nodeId) != null);
   }

   /**
    * Prints information about node's attribute definition.
    * 
    * @param   node
    *          Node name.
    * @param   attr
    *          Attribute name.
    * @return  <code>true</code> if operation was successful.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   boolean printAttrDef(String node, String attr)
   throws Exception
   {
      int i;
      StringBuilder buf = new StringBuilder(80);

      String className = getNodeClass(node);
      
      if (className == null)
         return false;

      NodeAttribute nAttr = dir.getClassNodeAttribute(className, attr);

      if (nAttr == null)
         return false;

      printDef(nAttr, null);

      // getting values
      Object[] values = null;

      switch (nAttr.getType())
      {
         case AttributeType.ATTR_INTEGER :
            values = dir.getNodeIntegers(node, attr);
            break;

         case AttributeType.ATTR_BOOLEAN :
            values = dir.getNodeBooleans(node, attr);
            break;

         case AttributeType.ATTR_STRING :
            values = dir.getNodeStrings(node, attr);
            break;

         case AttributeType.ATTR_DOUBLE :
            values = dir.getNodeDoubles(node, attr);
            break;

         case AttributeType.ATTR_BYTEARRAY :
            values = dir.getNodeByteArrays(node, attr);
            break;

         case AttributeType.ATTR_BITFIELD :
            values = dir.getNodeBitFields(node, attr);
            break;

         case AttributeType.ATTR_BITSELECTOR :
            values = dir.getNodeBitSelectors(node, attr);
            break;

         case AttributeType.ATTR_DATE :
            values = dir.getNodeDates(node, attr);
            break;

         case AttributeType.ATTR_TIME :
            values = dir.getNodeTimes(node, attr);
            break;

         default:
            ;
      }

      say("Values:");
      for (i = 0; values != null && i < values.length; i++)
      {
         buf.append("   " + i + ": ");
         buf.append(values[i].toString());
         say(buf.toString());
      }
      return true;
   }

   /**
    * Print definitions of attributes for the specified node.
    * 
    * @param   node
    *          Node ID.
    */
   void printAttrDefs(String node)
   {
      try
      {
         String className = getNodeClass(node);

         if (className == null)
         {
            say("Unable to determine node class");
            return;
         }

         NodeAttribute[] attrs = dir.getClassNodeAttributes(className);

         say("Node [" + node + "] object class is \"" + className +"\".");

         if (attrs == null || attrs.length == 0)
         {
            say("Object class does not define attributes.");
            return;
         }

         say("Object class defines following attributes:");
         
         for (int i = 0; i < attrs.length; i++)
         {
            printDef(attrs[i], "   " + i + ") ");
         }
         say();
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Prints information about node's children nodes.
    * 
    * @param   node
    *          Node name.
    * @param   level
    *          Nesting/indentation level.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   void printChildren(String node, int level)
   throws Exception
   {
      String[] nodes = dir.enumerateNodes(node);
      int numNodes = nodes == null ? 0 : nodes.length;
      StringBuilder sb = new StringBuilder();
      int i = 0;

      for (i = 0; i < level; i++)
         sb.append(" ");
      
      say(sb.toString() + "Child nodes:");

      for (i = 0; i < numNodes; i++)
      {
         say(sb.toString() + i + ") " + nodes[i]);
      }
   }

   /**
    * Print instance of <code>NodeAttribute</code> in human-readable form.
    * 
    * @param   nAttr
    *          Instance of the <code>NodeAttribute</code> to print.
    * @param   prefix
    *          Optional prefix to print before attribute definition.
    */
   void printDef(NodeAttribute nAttr, String prefix)
   {
      StringBuilder buf = new StringBuilder(80);
      
      int type = nAttr.getType();
      
      if (prefix != null)
         buf.append(prefix);
      
      buf.append("\"" + nAttr.getName() + "\" is ");
      buf.append(AttributeType.toString(type));
      buf.append(",");

      if (nAttr.isMandatory())
         buf.append(" mandatory,");
      else
         buf.append(" optional,");

      if (nAttr.isMultiple())
         buf.append(" multi-valued");
      else
         buf.append(" single-valued");

      say(buf.toString());
   }

   /**
    * Prints information about a single node.
    * 
    * @param   path
    *          Path (id) of the parent node.
    * @param   node
    *          Node name.
    * @param   level
    *          Nesting/indentation level.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   void printNode(String path, String node, int level)
   throws Exception
   {
      int i;
      StringBuilder buf = new StringBuilder(80);

      for (i = 0; i < level; i++)
         buf.append("  ");

      buf.append(node);
      buf.append(" {class = <");

      node = path + "/" + node;

      String name = dir.getNodeClass(node);

      buf.append(name);
      buf.append(">}");

      say(buf.toString());

      printNodeAttrs(node, level);
      printChildren(node, level);
   }

   /**
    * Prints information about node's attributes.
    * 
    * @param   node
    *          Node name.
    * @param   level
    *          Nesting/indentation level.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   void printNodeAttrs(String node, int level)
   throws Exception
   {
      int i;
      StringBuilder buf = new StringBuilder(80);

      for (i = 0; i < level; i++)
         buf.append("  ");

      String prefix = buf.toString();
      Attribute[] attr = dir.getNodeAttributes(node);

      if (attr != null)
      {
         for (i = 0; i < attr.length; i++)
         {
            Object val = attr[i].getValue(0);

            if (val == null)
               continue;

            if (val instanceof byte[])
               val = Base64.byteArrayToBase64((byte[])val);

            StringBuilder buf2 = new StringBuilder();

            buf2.append(prefix);
            buf2.append("  #attribute ");
            buf2.append(attr[i].getName());
            buf2.append(" = {");
            buf2.append(val.toString());

            for (int j = 1; val != null; j++)
            {
               val = attr[i].getValue(j);

               if (val != null)
               {
                  if (val instanceof byte[])
                     val = Base64.byteArrayToBase64((byte[])val);
                  buf2.append(", ");
                  buf2.append(val.toString());
               }
            }

            buf2.append("}");

            say(buf2.toString());
         }
      }
   }

   /** Main entry point */
   void run()
   {
      try
      {
         if (!dir.bind())
         {
            say("Binding to directory failed");
            return;
         }
         run("Top menu:", topMenu, null);

         if (!dir.unbind())
            say("Unbinding from directory failed");
      }
      catch (Exception e)
      {
         e.printStackTrace(System.out);
      }
   }

   /**
    * Menu list processing routine.
    * 
    * @param   text
    *          Message which will be printed before menu.
    * @param   menu
    *          List of menu items.
    * @param   comment
    *          Additional comment message to display after menu.
    */
   void run(String text, MenuItem[] menu, String comment)
   {
      try
      {
         while (true)
         {
            //Draw menu

            say();
            say(text);

            for (int i = 0; i < menu.length; i++)
               say(menu[i].getText());

            say();
            if (comment != null)
            {
               say(comment);
               say();
            }
            
            System.out.print("Enter command>");
            System.out.flush();

            boolean found = false;

            String input = in();
            if (input.length() == 0)
                  continue;

            char ch = input.charAt(0);

            for (int i = 0; i < menu.length; i++)
            {
               if (menu[i].getHotKey() == ch)
               {
                  found = true;
                  String[] res = input.split(" ");
                  
                  res[0] = input;
                  
                  menu[i].run(res);
               }
            }

            if (!found)
               say("Invalid command [" + input + "]");
         }
      }
      catch (StopMenu e)
      {
      }
   }

   /**
    * Prepare message and call node menu.
    */
   void runNodeMenu()
   {
      String message = null;
      
      if (isNodeTerminal(branch))
         message = "Node is terminal, menu item 'C node [class]' is disabled";

      run((branch.equals("") ? "root" : branch) + " node menu:", nodeMenu,
          message);
   }

   /**
    * Sets a value of node's attribute.
    * 
    * @param   node
    *          Node name.
    * @param   attr
    *          Attribute name.
    * @param   n
    *          Value index.
    * @param   newVal
    *          New value to be set.
    * @return  <code>true</code> if operation was successful.
    * @throws  Exception
    *          Forwarded from various methods.
    */
   boolean setValue(String node, String attr, int n, String newVal)
   throws Exception
   {
      //int i;
      String className = getNodeClass(node);

      NodeAttribute nAttr = dir.getClassNodeAttribute(className, attr);

      if (nAttr == null)
         return false;

      int type = nAttr.getType();
      boolean res = false;

      switch (type)
      {
         case AttributeType.ATTR_INTEGER :
            res = dir.setNodeInteger(node, attr, n, Integer.parseInt(newVal));
            break;

         case AttributeType.ATTR_BOOLEAN :
            Boolean boVal = Boolean.valueOf(newVal);
            boolean bVal = boVal.booleanValue();
            res = dir.setNodeBoolean(node, attr, n, bVal);
            break;

         case AttributeType.ATTR_STRING :
            res = dir.setNodeString(node, attr, n, newVal);
            break;

         case AttributeType.ATTR_DOUBLE :
            res = dir.setNodeDouble(node, attr, n, Double.parseDouble(newVal));
            break;

         case AttributeType.ATTR_BYTEARRAY :
            res = dir.setNodeByteArray(node, attr, n,
                                       Base64.base64ToByteArray(newVal));
            break;

         case AttributeType.ATTR_BITFIELD :
            res = dir.setNodeBitField(node, attr, n, BitField.valueOf(newVal));
            break;

         case AttributeType.ATTR_BITSELECTOR :
            res = dir.setNodeBitSelector(
                                         node,
                                         attr,
                                         n,
                                         (BitSelector)BitSelector.valueOf(newVal));
            break;

         case AttributeType.ATTR_DATE :
            res = dir.setNodeDate(node, attr, n, DateValue.valueOf(newVal));
            break;

         case AttributeType.ATTR_TIME :
            res = dir.setNodeTime(node, attr, n, TimeValue.valueOf(newVal));
            break;

         default:
            ;
      }

      return res;
   }
   
   /**
    * Convert given string value into attribute value using attribute type
    * information.
    * 
    * @param   list
    *          Attribute definition list.
    * @param   str
    *          Source string.
    * @return  Converted value.
    */
   Object stringToAttrValue(NodeAttribute list, String str)
   {
      Object value = null;
      
      switch(list.getType())
      {
         case AttributeType.ATTR_STRING :
            value = str; 
            break;

         case AttributeType.ATTR_BYTEARRAY :
            value = Base64.base64ToByteArray(str);
            break;

         case AttributeType.ATTR_INTEGER :
            value = Integer.valueOf(str, 10);
            break;

         case AttributeType.ATTR_BOOLEAN :
            value = Boolean.valueOf(str);
            break;

         case AttributeType.ATTR_DOUBLE :
            value = Double.valueOf(str);
            break;

         case AttributeType.ATTR_BITFIELD :
            value = BitField.valueOf(str);
            break;

         case AttributeType.ATTR_BITSELECTOR :
            value = BitSelector.valueOf(str);
            break;

         case AttributeType.ATTR_DATE :
            value = DateValue.valueOf(str);
            break;

         case AttributeType.ATTR_TIME :
            value = TimeValue.valueOf(str);
            break;
      }
      return value;
   }

   /**
    * Simple command line harness.
    */
   public static void main(String[] args)
   {
      // TODO: this application is non-functional at this time, we need to
      //       1. prepare the bootstrap config
      //       2. make our connection and get our queue
      //       3. start the queue
      
      Session session = null;
      
      try
      {
         DirectoryEdit app = new DirectoryEdit(session);

         try
         {
            app.run();
         }
         
         catch (Exception x)
         {
            say("Exception in topMenu(): " + x);
            x.printStackTrace(System.err);
         }
      }
      
      catch (Exception e)
      {
         e.printStackTrace(System.out);
         System.exit(-1);
      }
   }

   /**
    * HLO which will map server entry points into local object.
    */
   public static class ServerDirectory
   extends HighLevelObject
   {
      /** Group of exported methods which will be used for testing */
      static final String GROUP = "directory";
      
      /** RoutingKey for addNodeBitField() method */
      private RoutingKey addNodeBitFieldKey = null;

      /** RoutingKey for addNodeBitSelector() method */
      private RoutingKey addNodeBitSelectorKey = null;

      /** RoutingKey for addNodeBoolean() method */
      private RoutingKey addNodeBooleanKey = null;

      /** RoutingKey for addNodeByteArray() method */
      private RoutingKey addNodeByteArrayKey = null;

      /** RoutingKey for addNodeDate() method */
      private RoutingKey addNodeDateKey = null;

      /** RoutingKey for addNodeDouble() method */
      private RoutingKey addNodeDoubleKey = null;

      /** RoutingKey for addNodeInteger() method */
      private RoutingKey addNodeIntegerKey = null;

      /** RoutingKey for addNode() method */
      private RoutingKey addNodeKey = null;

      /** RoutingKey for addNodeString() method */
      private RoutingKey addNodeStringKey = null;

      /** RoutingKey for addNodeTime() method */
      private RoutingKey addNodeTimeKey = null;

      /** RoutingKey for bind() method */
      private RoutingKey bindKey = null;

      /** RoutingKey for closeBatch() method */
      private RoutingKey closeBatchKey = null;

      /** RoutingKey for deleteNodeAttribute() method */
      private RoutingKey deleteNodeAttributeKey = null;

      /** RoutingKey for deleteNodeAttributeValue() method */
      private RoutingKey deleteNodeAttrValueKey = null;

      /** RoutingKey for deleteNode() method */
      private RoutingKey deleteNodeKey = null;

      /** RoutingKey for enumerateNodes() method */
      private RoutingKey enumNodesKey = null;

      /** RoutingKey for getClassNodeAttribute() method */
      private RoutingKey getClassNodeAttrKey = null;

      /** RoutingKey for getClassNodeAttributes() method */
      private RoutingKey getClassNodeAttrsKey = null;
            
      /** RoutingKey for getNodeAttributes() method */
      private RoutingKey getNodeAttrsKey = null;

      /** RoutingKey for getNodeBitFields() method */
      private RoutingKey getNodeBitFieldsKey = null;

      /** RoutingKey for getNodeBitSelectors() method */
      private RoutingKey getNodeBitSelectorsKey = null;

      /** RoutingKey for getNodeBooleans() method */
      private RoutingKey getNodeBooleansKey = null;

      /** RoutingKey for getNodeByteArrays() method */
      private RoutingKey getNodeByteArraysKey = null;

      /** RoutingKey for getNodeClass() method */
      private RoutingKey getNodeClassKey = null;

      /** RoutingKey for getNodeDates() method */
      private RoutingKey getNodeDatesKey = null;

      /** RoutingKey for getNodeDoubles() method */
      private RoutingKey getNodeDoublesKey = null;

      /** RoutingKey for getNodeIntegers() method */
      private RoutingKey getNodeIntegersKey = null;

      /** RoutingKey for getNodeStrings() method */
      private RoutingKey getNodeStringsKey = null;

      /** RoutingKey for getNodeTimes() method */
      private RoutingKey getNodeTimesKey = null;

      /** RoutingKey for moveNode() method */
      private RoutingKey moveNodeKey = null;

      /** RoutingKey for openBatch() method */
      private RoutingKey openBatchKey = null;

      /** RoutingKey for setNodeBitField() method */
      private RoutingKey setNodeBitFieldKey = null;

      /** RoutingKey for setNodeBitSelector() method */
      private RoutingKey setNodeBitSelectorKey = null;

      /** RoutingKey for setNodeBoolean() method */
      private RoutingKey setNodeBooleanKey = null;

      /** RoutingKey for setNodeByteArray() method */
      private RoutingKey setNodeByteArrayKey = null;

      /** RoutingKey for setNodeDate() method */
      private RoutingKey setNodeDateKey = null;

      /** RoutingKey for setNodeDouble() method */
      private RoutingKey setNodeDoubleKey = null;

      /** RoutingKey for setNodeInteger() method */
      private RoutingKey setNodeIntegerKey = null;

      /** RoutingKey for setNodeString() method */
      private RoutingKey setNodeStringKey = null;

      /** RoutingKey for setNodeTime() method */
      private RoutingKey setNodeTimeKey = null;

      /** RoutingKey for unbind() method */
      private RoutingKey unbindKey = null;

      /**
       * Construct an HLO using provided instance of the <code>Queue</code>.
       * 
       * @param   session
       *          Properly initialized session.
       */
      ServerDirectory(Session session)
      {
         super(session, 60000);
      }

      /**
       * Add new node of specified class with specified ID. For node
       * initialization list of attributes and corresponding list of attribute
       * values should be provided. The presence of the mandatory attributes
       * in the lists is obligatory.
       * 
       * @param   nodeId
       *          Node name.
       * @param   nodeClass
       *          Node class name.
       * @param   data
       *          A list of the attributes.
       * @return  <code>true</code> if operation was successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNode(String nodeId, String nodeClass, Attribute[] data)
      throws Exception
      {
         if (addNodeKey == null)
            addNodeKey = getKey(GROUP, "addnode");
         if (addNodeKey == null)
            throw new Exception("unresolvable remote export");
         
         Object[] parms = new Object[] { nodeId, nodeClass, data };

         Object result = transact(addNodeKey, parms);
         return ((Boolean)result).booleanValue();
      }

      
      /**
       * Add new <code>BitField</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeBitField(String nodeId, String name,
                                     BitField value)
      throws Exception
      {
         if (addNodeBitFieldKey == null)
            addNodeBitFieldKey = getKey(GROUP, "addnodebitfield");
         if (addNodeBitFieldKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, value
            };

         Object result = transact(addNodeBitFieldKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>BitSelector</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeBitSelector(String nodeId, String name,
                                        BitSelector value)
      throws Exception
      {
         if (addNodeBitSelectorKey == null)
            addNodeBitSelectorKey = getKey(GROUP, "addnodebitselector");
         if (addNodeBitSelectorKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, value
            };

         Object result = transact(addNodeBitSelectorKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>boolean</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeBoolean(String nodeId, String name, boolean value)
      throws Exception
      {
         if (addNodeBooleanKey == null)
            addNodeBooleanKey = getKey(GROUP, "addnodeboolean");
         if (addNodeBooleanKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Boolean.valueOf(value)
            };

         Object result = transact(addNodeBooleanKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>byte[]</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeByteArray(String nodeId, String name, byte[] value)
      throws Exception
      {
         if (addNodeByteArrayKey == null)
            addNodeByteArrayKey = getKey(GROUP, "addnodebytearray");
         if (addNodeByteArrayKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, value
            };

         Object result = transact(addNodeByteArrayKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>DateValue</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeDate(String nodeId, String name, DateValue value)
      throws Exception
      {
         if (addNodeDateKey == null)
            addNodeDateKey = getKey(GROUP, "addnodedate");
         if (addNodeDateKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, value
            };

         Object result = transact(addNodeDateKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>Double</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeDouble(String nodeId, String name, double value)
      throws Exception
      {
         if (addNodeDoubleKey == null)
            addNodeDoubleKey = getKey(GROUP, "addnodedouble");
         if (addNodeDoubleKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Double.valueOf(value)
            };

         Object result = transact(addNodeDoubleKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>int</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeInteger(String nodeId, String name, int value)
      throws Exception
      {
         if (addNodeIntegerKey == null)
            addNodeIntegerKey = getKey(GROUP, "addnodeinteger");
         if (addNodeIntegerKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(value)
            };

         Object result = transact(addNodeIntegerKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>String</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeString(String nodeId, String name, String value)
      throws Exception
      {
         if (addNodeStringKey == null)
            addNodeStringKey = getKey(GROUP, "addnodestring");
         if (addNodeStringKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, value
            };

         Object result = transact(addNodeStringKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Add new <code>TimeValue</code> value to the attribute.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean addNodeTime(String nodeId, String name, TimeValue value)
      throws Exception
      {
         if (addNodeTimeKey == null)
            addNodeTimeKey = getKey(GROUP, "addnodetime");
         if (addNodeTimeKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, value
            };

         Object result = transact(addNodeTimeKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Binds to the directory.
       * 
       * @return  <code>true</code> is operation is successful and
       *         <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean bind()
      throws Exception
      {
         if (bindKey == null)
            bindKey = getKey(GROUP, "bind");
         if (bindKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] {};
      Object result = transact(bindKey, parms);

      return ((Boolean)result).booleanValue();
   }

      /**
       * Closes the currently open editing batch.
       * 
       * @param   disposition
       *          true if committing, false otherwise
       * @return  true or false
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean closeBatch(boolean disposition)
      throws Exception
      {
         if (closeBatchKey == null)
            closeBatchKey = getKey(GROUP, "closebatch");
         if (closeBatchKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               Boolean.valueOf(disposition)
            };
         Object result = transact(closeBatchKey, parms);

         return ((Boolean)result).booleanValue();
      }

      /**
       * Remove specified node from the directory (unless node has children).
       * 
       * @param   nodeId
       *          Node identifier.
       * @return  <code>true</code> if operation was successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean deleteNode(String nodeId)
      throws Exception
      {
         if (deleteNodeKey == null)
            deleteNodeKey = getKey(GROUP, "deletenode");
         if (deleteNodeKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId
            };

         Object result = transact(deleteNodeKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Remove whole node attribute.
       * 
       * @param   nodeId
       *          Node name.
       * @param   name
       *          Attribute name.
       * @return  <code>true</code> if operation was successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean deleteNodeAttribute(String nodeId, String name)
      throws Exception
      {
         if (deleteNodeAttributeKey == null)
            deleteNodeAttributeKey = getKey(GROUP, "deletenodeattribute");
         if (deleteNodeAttributeKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name
            };

         Object result = transact(deleteNodeAttributeKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Remove particular value from the node attribute. Note that removing
       * last value from the attribute deletes entire attribute unless
       * attribute is marked mandatory.
       * 
       * @param   nodeId
       *          Node name.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Attribute value index.
       * @return  <code>true</code> if operation was successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean deleteNodeAttributeValue(String nodeId, String name,
                                              int index)
      throws Exception
      {
         if (deleteNodeAttrValueKey == null)
            deleteNodeAttrValueKey = getKey(GROUP, "deletenodeattributevalue");
         if (deleteNodeAttrValueKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name, Integer.valueOf(index) };

         Object result = transact(deleteNodeAttrValueKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Enumerates all children nodes of a node.
       * 
       * @param   nodeId
       *          Node name
       * @return  Array of children node IDs
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public String[] enumerateNodes(String nodeId)
      throws Exception
      {
         if (enumNodesKey == null)
            enumNodesKey = getKey(GROUP, "enumeratenodes");
         if (enumNodesKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId };

         Object result = transact(enumNodesKey, parms);

         return (String[])result;
      }

      /**
       * Gets NodeAttribute structure for the specified attribute.
       * 
       * @param   className
       *          Object Class name.
       * @param   name
       *          Object Class attribute name.
       * @return  An instance of <code>NodeAttribute</code> which describes
       *          specified attribute or <code>null</code> if no such object
       *          class or attribute name.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public NodeAttribute getClassNodeAttribute(String className, String name)
      throws Exception
      {
         if (getClassNodeAttrKey == null)
            getClassNodeAttrKey = getKey(GROUP, "getclassnodeattribute");
         if (getClassNodeAttrKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { className, name };

         Object result = transact(getClassNodeAttrKey, parms);

         return (NodeAttribute)result;
      }

      /**
       * Generate an array of <code>NodeAttribute</code> structures for the
       * all attributes defined for the node class. Note that since these
       * instances of <code>NodeAttribute</code> are not attached to
       * particular instance of <code>Attribute</code>, information about
       * number of the values assigned to the attribute is incorrect. The
       * purpose of this method is to create new attributes.
       * 
       * @param   className
       *          Object Class name.
       * @return  An array of <code>NodeAttribute</code> instances which
       *          describe attributes of node or <code>null</code> if no such
       *          object class or attributes defined for the class.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public NodeAttribute[] getClassNodeAttributes(String className)
      throws Exception
      {
         if (getClassNodeAttrsKey == null)
            getClassNodeAttrsKey = getKey(GROUP, "getclassnodeattributes");
         if (getClassNodeAttrsKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]  { className };

         Object result = transact(getClassNodeAttrsKey, parms);

         return (NodeAttribute[])result;
      }

      
      /**
       * Reads all attributes of a node.
       * 
       * @param   nodeId
       *          Node name
       * @return  Array of node attributes
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public Attribute[] getNodeAttributes(String nodeId)
      throws Exception
      {
         if (getNodeAttrsKey == null)
            getNodeAttrsKey = getKey(GROUP, "getnodeattributes");
         if (getNodeAttrsKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId };

         Object result = transact(getNodeAttrsKey, parms);

         return (Attribute[])result;
      }

      /**
       * Gets all <code>BitField</code> attribute values for specified node
       * ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public BitField[] getNodeBitFields(String nodeId, String name)
      throws Exception
      {
         if (getNodeBitFieldsKey == null)
            getNodeBitFieldsKey = getKey(GROUP, "getnodebitfields");
         if (getNodeBitFieldsKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeBitFieldsKey, parms);

         return (BitField[])result;
      }

      /**
       * Gets all <code>BitSelector</code> attribute values for specified
       * node ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public BitSelector[] getNodeBitSelectors(String nodeId, String name)
      throws Exception
      {
         if (getNodeBitSelectorsKey == null)
            getNodeBitSelectorsKey = getKey(GROUP, "getnodebitselectors");
         if (getNodeBitSelectorsKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeBitSelectorsKey, parms);

         return (BitSelector[])result;
      }

      /**
       * Gets all <code>Boolean</code> attribute values for specified node
       * ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public Boolean[] getNodeBooleans(String nodeId, String name)
      throws Exception
      {
         if (getNodeBooleansKey == null)
            getNodeBooleansKey = getKey(GROUP, "getnodebooleans");
         if (getNodeBooleansKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeBooleansKey, parms);

         return (Boolean[])result;
      }

      /**
       * Gets all <code>ByteArray</code> attribute values for specified node
       * ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public byte[][] getNodeByteArrays(String nodeId, String name)
      throws Exception
      {
         if (getNodeByteArraysKey == null)
            getNodeByteArraysKey = getKey(GROUP, "getnodebytearrays");
         if (getNodeByteArraysKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeByteArraysKey, parms);

         return (byte[][])result;
      }

      /**
       * Queries the object class name of a node.
       * 
       * @param   nodeId
       *          Node name
       * @return  Class name of this node
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public String getNodeClass(String nodeId)
      throws Exception
      {
         if (getNodeClassKey == null)
            getNodeClassKey = getKey(GROUP, "getnodeclass");
         if (getNodeClassKey == null)
            throw new Exception("unresolvable remote export");
         
         Object[] parms = new Object[] { nodeId };

         Object result = transact(getNodeClassKey, parms);

         return (String)result;
      }

      /**
       * Gets all <code>Date</code> attribute values for specified node ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public DateValue[] getNodeDates(String nodeId, String name)
      throws Exception
      {
         if (getNodeDatesKey == null)
            getNodeDatesKey = getKey(GROUP, "getnodedates");
         if (getNodeDatesKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeDatesKey, parms);

         return (DateValue[])result;
      }

      /**
       * Gets all <code>Double</code> attribute values for specified node
       * ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public Double[] getNodeDoubles(String nodeId, String name)
      throws Exception
      {
         if (getNodeDoublesKey == null)
            getNodeDoublesKey = getKey(GROUP, "getnodedoubles");
         if (getNodeDoublesKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeDoublesKey, parms);

         return (Double[])result;
      }

      /**
       * Gets all <code>Integer</code> attribute values for specified node
       * ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public Integer[] getNodeIntegers(String nodeId, String name)
      throws Exception
      {
         if (getNodeIntegersKey == null)
            getNodeIntegersKey = getKey(GROUP, "getnodeintegers");
         if (getNodeIntegersKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeIntegersKey, parms);

         return (Integer[])result;
      }

      /**
       * Gets all <code>String</code> attribute values for specified node
       * ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public String[] getNodeStrings(String nodeId, String name)
      throws Exception
      {
         if (getNodeStringsKey == null)
            getNodeStringsKey = getKey(GROUP, "getnodestrings");
         if (getNodeStringsKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeStringsKey, parms);

         return (String[])result;
      }

      /**
       * Gets all <code>Time</code> attribute values for specified node ID.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @return  Values of the attribute
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public TimeValue[] getNodeTimes(String nodeId, String name)
      throws Exception
      {
         if (getNodeTimesKey == null)
            getNodeTimesKey = getKey(GROUP, "getnodetimes");
         if (getNodeTimesKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name };

         Object result = transact(getNodeTimesKey, parms);

         return (TimeValue[])result;
      }

      /**
       * Renames the existing object, specified by nodeId, or moves it to a
       * different location. The parent object of the newId should exist and
       * allow for children creation.
       * 
       * @param   nodeId
       *          An ID of the node to move.
       * @param   newId
       *          The desired new location of the node.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean moveNode(String nodeId, String newId)
      throws Exception
      {
         if (moveNodeKey == null)
            moveNodeKey = getKey(GROUP, "movenode");
         if (moveNodeKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, newId };

         Object result = transact(moveNodeKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Opens an editing batch.
       * 
       * @param   nodeId
       *          Node specifying directory branch to open
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean openBatch(String nodeId)
      throws Exception
      {
         if (openBatchKey == null)
            openBatchKey = getKey(GROUP, "openbatch");
         if (openBatchKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId };
         Object result = transact(openBatchKey, parms);

         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>BitField</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeBitField(String nodeId, String name, int index,
                                     BitField value)
      throws Exception
      {
         if (setNodeBitFieldKey == null)
            setNodeBitFieldKey = getKey(GROUP, "setnodebitfield4");
         if (setNodeBitFieldKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name, Integer.valueOf(index), 
                                         value };

         Object result = transact(setNodeBitFieldKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>BitSelector</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeBitSelector(String nodeId, String name,
                                        int index, BitSelector value)
      throws Exception
      {
         if (setNodeBitSelectorKey == null)
            setNodeBitSelectorKey = getKey(GROUP, "setnodebitselector4");
         if (setNodeBitSelectorKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name, Integer.valueOf(index), 
                                         value};

         Object result = transact(setNodeBitSelectorKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>boolean</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeBoolean(String nodeId, String name, int index,
                                    boolean value)
      throws Exception
      {
         if (setNodeBooleanKey == null)
            setNodeBooleanKey = getKey(GROUP, "setnodeboolean4");
         if (setNodeBooleanKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(index), Boolean.valueOf(value)
            };

         Object result = transact(setNodeBooleanKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>byte[]</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeByteArray(String nodeId, String name, int index,
                                      byte[] value)
      throws Exception
      {
         if (setNodeByteArrayKey == null)
            setNodeByteArrayKey = getKey(GROUP, "setnodebytearray4");
         if (setNodeByteArrayKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(index), value
            };

         Object result = transact(setNodeByteArrayKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>DateValue</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeDate(String nodeId, String name, int index,
                                 DateValue value)
      throws Exception
      {
         if (setNodeDateKey == null)
            setNodeDateKey = getKey(GROUP, "setnodedate4");
         if (setNodeDateKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] { nodeId, name, Integer.valueOf(index), 
                                          value };

         Object result = transact(setNodeDateKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>double</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeDouble(String nodeId, String name, int index,
                                   double value)
      throws Exception
      {
         if (setNodeDoubleKey == null)
            setNodeDoubleKey = getKey(GROUP, "setnodedouble4");
         if (setNodeDoubleKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(index), Double.valueOf(value)
            };

         Object result = transact(setNodeDoubleKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>int</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeInteger(String nodeId, String name, int index,
                                    int value)
      throws Exception
      {
         if (setNodeIntegerKey == null)
            setNodeIntegerKey = getKey(GROUP, "setnodeinteger4");
         if (setNodeIntegerKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(index), Integer.valueOf(value)
            };

         Object result = transact(setNodeIntegerKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>String</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeString(String nodeId, String name, int index,
                                   String value)
      throws Exception
      {
         if (setNodeStringKey == null)
            setNodeStringKey = getKey(GROUP, "setnodestring4");
         if (setNodeStringKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(index), value
            };

         Object result = transact(setNodeStringKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Set <code>TimeValue</code> attribute value for specified node ID,
       * attribute name and index.
       * 
       * @param   nodeId
       *          Node identifier.
       * @param   name
       *          Attribute name.
       * @param   index
       *          Index of the value to retrieve.
       * @param   value
       *          New value for the attribute variable.
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean setNodeTime(String nodeId, String name, int index,
                                 TimeValue value)
      throws Exception
      {
         if (setNodeTimeKey == null)
            setNodeTimeKey = getKey(GROUP, "setnodetime4");
         if (setNodeTimeKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[]
            {
               nodeId, name, Integer.valueOf(index), value
            };

         Object result = transact(setNodeTimeKey, parms);
         return ((Boolean)result).booleanValue();
      }

      /**
       * Unbinds from directory service.
       * 
       * @return  <code>true</code> is operation is successful and
       *          <code>false</code> otherwise.
       * @throws  Exception
       *          Forwarded from the remote method.
       */
      public boolean unbind()
      throws Exception
      {
         if (unbindKey == null)
            unbindKey = getKey(GROUP, "unbind");
         if (unbindKey == null)
            throw new Exception("unresolvable remote export");

         Object[] parms = new Object[] {};
         Object result = transact(unbindKey, parms);
   
         return ((Boolean)result).booleanValue();
      }
   }

   /**
    * Single menu item. 
    */
   abstract class MenuItem
   {
      /** Menu item selection key */
      char hotkey;

      /** Menu item text */
      String text;

      /**
       * Construct an instance with given menu text. Menu item text must
       * contain a menu item selection key marked by leading tilde (~)
       * character. For example: <br>
       * <code>First menu item - ~a</code> will mark <b>a </b> as selection
       * key.
       * 
       * @param   text
       *          Menu item text.
       */
      MenuItem(String text)
      {
         prepareText(text);
      }

      /**
       * Get menu item selection key.
       * 
       * @return  Menu item selection key.
       */
      public char getHotKey()
      {
         return hotkey;
      }

      /**
       * Get menu item text with making tilde removed.
       * 
       * @return  Menu item text.
       */
      public String getText()
      {
         return text;
      }

      /**
       * Worker routing which is executed when menu item is selected.
       * 
       * @param   args
       *          Array of tokens retrieved from the user input.
       */
      abstract void run(String[] args);

      /**
       * Process menu item text and retrieve selection key. Then marker (~) is
       * removed.
       * 
       * @param   str
       *          Menu item text.
       * @throws  RuntimeException
       *          If menu item does not contain proper marker.
       */
      private void prepareText(String str)
      {
         int pos = str.indexOf('~');

         if (pos < 0 || pos >= (str.length() - 1))
            throw new RuntimeException("Menu item [" + str
               + "] does not have hot key.");

         hotkey = str.charAt(pos + 1);
         text = str.substring(0, pos) + str.substring(pos + 1);
      }
   }

   /**
    * Special exception used to break menu processing loop. <br>
    * Since only few (usually one from the entire list) menu item throws this
    * exception <code>RuntimeException</code> is used as a base because it
    * does not need to be declared in each method.
    */
   static class StopMenu
   extends RuntimeException
   {
      /**
       * Construct an empty exception.
       */
      StopMenu()
      {
         super();
      }
   }
}