BootstrapConfig.java

/*
** Module   : BootstrapConfig.java
** Abstract : reads bootstrap configuration file(s) and makes config items
**            available upon request
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 NVS 20050217   @19883 Created initial version. This reads and
**                           decrypts specified XML file(s) and provides
**                           methods that search the in memory structure
**                           for specific values.
** 002 NVS 20050217   @19936 Relocated readPassphrase method to util
**                           package.
** 003 NVS 20050225   @19958 Added new methods: isServer(), isClient().
** 004 NVS 20050228   @20074 Some code cleanup
** 005 NVS 20050418   @20759 Added new methods listCategories, listGroups
**                           and listKeys.
** 006 ECF 20050421   @20804 Import p2j.xml package to use XmlHelper.
** 007 NVS 20050829   @22391 Added new methods: setConfigItem(),
**                           isTypeSet(), setClient() and setServer().
**                           Changed the encryption requirements. Now the
**                           file is treated as a plain XML if no password
**                           is provided. No configuration file is treated
**                           as a valid case, too. An empty object is
**                           created. This change is in support of the 
**                           production level client and server drivers.
** 008 GES 20060815   @28608 Allow subsequent calls to setClient() or
**                           setServer() to modify the server flag. This
**                           is needed to enable the special mode of the
**                           server driver where it connects as a client.
** 009 GES 20061215   @31702 Major code and doc cleanup. Removed some
**                           unnecessary interfaces and complexity.
** 010 GES 20061218   @31717 Added helpers to read values and return them
**                           as a specific type (and with a default value
**                           if the value is not specified in the cfg).
** 011 GES 20070110   @31773 Match interface changes. Added helpers to
**                           read boolean values and to read filename/url
**                           specifications and return an input stream.
** 012 GES 20070117   @31877 Added convenience constructors.
** 013 GES 20090514   @42202 Import changes.
** 014 LMR 20101217          Changed getBoolean(), getInt(), getInputStream()
**                           and getString() to not throw 
**                           ConfigurationException any more: the sensible
**                           behavior is to gracefully return the default
**                           value that was provided in case a 
**                           misconfiguration is detected; getConfigItem() on
**                           the other hand is different: it is a low level
**                           API through which the raw data can be read and
**                           this API must report on cases of 
**                           misconfiguration.  
** 015 CA  20130707          Added a c'tor to instantiate a new config by copying another instance.
** 016 CA  20140206          Added copyConfigItems API.
** 017 SBI 20151119          Implemented toString().
** 018 VVT 20200203          Check added for missing resource case in getInputStream()
** 019 IAS 20210325          Added type parameters. Retain settings order (for debugging).
**     GES 20210827          Improved toString() to report additional information about the instance.
** 020 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 021 TT  20230622          Created a Boolean version of getBoolean().
** 022 TJD 20240123          Java 17 compatibility updates
** 023 GBB 20240709          Method processOverrides moved here from CommonDriver. Adding methods to set 
**                           and get ConfigItem values.
** 024 SBI 20250123          Fixed ArrayIndexOutOfBounds exception for processOverrides if list of options
**                           has empty elements.
*/

/*
** 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.cfg;

import java.io.*;
import java.net.*;
import java.util.*;
import java.security.*;
import java.security.spec.*;
import java.util.logging.*;

import javax.crypto.*;
import javax.crypto.spec.*;

import com.goldencode.p2j.util.logging.*;
import org.w3c.dom.*;

import com.goldencode.util.*;
import com.goldencode.p2j.util.*;

/**
 * Collection of convenient methods to read, encrypt, decrypt and parse
 * bootstrap configuration XML files. The files are organized as three level
 * trees and are optionally stored encrypted to prevent tampering.
 * <p>
 * Provides a command line driver for config file encryption and decryption.
 * <p>
 * The root element of a valid bootstrap configuration file is always
 * <code>&lt;node type="type"&gt;</code> where type is either client or
 * server. 
 * <p>
 * The rest of the file is not checked and can be made of any elements. Only
 * those elements that are organized in three levels, can be queried or set.
 * <p>
 * The first level of elements under the root are considered categories. They
 * may have attributes, but those can't be accessed.
 * <p>
 * The second level of elements are considered groups. Groups may have
 * accessible atributes and those attributes are considered keys.
 * <p>
 * Each key has a value. These values are the only part of the tree that can
 * be read or written.
 * <p> 
 * Here is a sample bootstrap configuration file: 
 * <pre>
 *   &lt;?xml version="1.0"?&gt;
 *      &lt;node type="client"&gt;
 *         &lt;net&gt; 
 *            &lt;server host="p2jserver"/&gt;
 *            &lt;server port="3333"/&gt; 
 *            &lt;dispatcher threads="4"/&gt; 
 *         &lt;/net&gt;
 *         &lt;security&gt;
 *            &lt;truststore filename="trust.store"/&gt;
 *            &lt;truststore alias="mainserver"/&gt;
 *            &lt;keystore filename="key.store"/&gt;
 *         &lt;/security&gt;
 *      &lt;/node&gt;
 * </pre>
 * <p>
 * Internally, the contents of the file is represented as a tree of maps. The
 * root map names defined categories and contains references to group maps
 * one map per category.
 * <p>
 * Group maps name defined groups and contain references to key maps. The
 * latter contain the value definitions for keys.
 * <p>
 * Based on this 3 part naming scheme (category, group, key), values can be
 * retrieved using {@link #getConfigItem} and can be assigned using 
 * {@link #setConfigItem}.
 */
public class BootstrapConfig
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(BootstrapConfig.class);
   
   /** Encryption algorithm <code>salt</code> parameter. */
   private static final byte[] salt = 
   {
      (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c,
      (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99
   };

   /** Encryption algorithm <code>iterations</code>parameter. */
   private static final int count = 20;
   
   /** JSSE encryption algorithm name. */
   private static final String ALGORITHM = "PBEWithMD5AndDES";

   /** The collection of known categories. */
   private Map<String, Map<String, Map<String, String>>> categories = null;
   
   /** Client or server configuration type indicator. */
   private boolean isServer = true;
   
   /** Shared configuration filename which was loaded. */
   private String sharedFile = null;
   
   /** Private configuration filename which was loaded. */
   private String privateFile = null;
   
   /**
    * Initialize a new instance as a copy of the given config.
    *  
    * @param    config
    *           The config instance to copy.
    */
   public BootstrapConfig(BootstrapConfig config)
   {
      this.isServer = config.isServer;
      this.categories = new HashMap<>(config.categories);
   }
   
   /**
    * Constructs an instance with no backing configuration file.  To make
    * this instance useful, subsequent calls to {@link #setConfigItem} will
    * be needed to load values.
    * 
    * @throws   ConfigurationException
    *           Configuration exception
    */
   public BootstrapConfig()
   throws ConfigurationException
   {
      this(null, null, null, null);
   }
   
   /**
    * Constructs an instance, loads the associated configuration file, 
    * decrypts it if passwords are given, parses the content and makes it 
    * available for subsequent queries.
    * <p>
    * No secondary configuration file will be supported in this case.
    *
    * @param    file
    *           The filename of the primary or only configuration file. May
    *           be <code>null</code> if no configuration file is to be used.
    * @param    passphrase
    *           The passphrase used for decryption of the primary file. May
    *           be <code>null</code> if no decryption is needed.
    * 
    * @throws   ConfigurationException
    *           Configuration exception
    */
   public BootstrapConfig(String file, char[] passphrase)
   throws ConfigurationException
   {
      this(file, passphrase, null, null);
   }
   
   /**
    * Constructs an instance, loads the associated configuration files, 
    * decrypts them if passwords are given, parses the content and makes it
    * available for subsequent queries.
    * <p>
    * For client configurations, either one or two files can be given. In
    * case of two files, both have to specify the node type as client. For
    * server configurations only one file can be given and it is expected
    * to have the node type of server.
    * <p>
    * The advantage of using 2 files is that one may include shared
    * definitions for a large number of users where the second file would be
    * specific to the given user.
    *
    * @param    sharedFile
    *           The filename of the primary or only configuration file. May
    *           be <code>null</code> if no configuration file is to be used.
    * @param    sharedPassphrase
    *           The passphrase used for decryption of the primary file. May
    *           be <code>null</code> if no decryption is needed.
    * @param    privateFile
    *           The filename of the secondary configuration file or
    *           <code>null</code> if not to be used.
    * @param    privatePassphrase
    *           The passphrase used for decryption of the secondary file. May
    *           be <code>null</code> if no decryption is needed. 
    * 
    * @throws   ConfigurationException
    *           Configuration exception
    */
   public BootstrapConfig(String sharedFile,
                          char[] sharedPassphrase,
                          String privateFile,
                          char[] privatePassphrase)
   throws ConfigurationException
   {
      categories = new LinkedHashMap<>();

      // load the primary file and inspect its node type
      if (sharedFile == null)
         return;

      Element root = loadXml(sharedFile, sharedPassphrase);
      String  type = root.getAttribute("type");

      if ("client".equalsIgnoreCase(type))
      {
         isServer = false;
      }
      else
      {
         if (!"server".equalsIgnoreCase(type))
         {
            throw new ConfigurationException("Invalid root node type: " + type);
         }
      }

      // extract items from the first file
      walkXml(root);
      
      this.sharedFile = sharedFile;

      // check if the secondary file is specified
      if (privateFile == null)
         return;
      
      // check to see if it is a server config
      if (isServer)
      {
         throw new ConfigurationException("Server configuration must come in a single file.");
      }
      
      // load the secondary file and inspect its node type
      root = loadXml(privateFile, privatePassphrase);
      
      type = root.getAttribute("type");

      // check to see if it is a client config
      if (!"client".equalsIgnoreCase(type))
      {
         throw new ConfigurationException("Node type " + type + " mismatched with 'client' node.");
      }

      // extract items from the second file
      walkXml(root); 
      
      this.privateFile = privateFile;
   }
   
   /**
    * Utility method to load and optionally decrypt an XML configuration
    * file.
    * 
    * @param    filename
    *           The name of an (optionally encrypted) XML file.
    * @param    passphrase
    *           The passphrase to be used for decryption of the file. Use
    *           <code>null</code> if the file is unencrypted. 
    * 
    * @return   The root node for the loaded document.
    * 
    * @throws   ConfigurationException
    *           Configuration exception
    */
   private static Element loadXml(String filename, char[] passphrase)
   throws ConfigurationException
   {
      Element root = null;
      
      try
      {
         Document dom = null;
         FileInputStream fis = new FileInputStream(filename);

         if (passphrase != null)
         {
            Cipher cipher = getCipher(passphrase, false);
            CipherInputStream  cis = new CipherInputStream(fis, cipher); 
   
            dom = XmlHelper.parse(cis);
            cis.close();
         }
         else
         {
            dom = XmlHelper.parse(fis);
            fis.close();
         }
            
         root = dom.getDocumentElement();

         // check the root node
         if (!root.getTagName().toUpperCase().equals("NODE"))
            throw new ConfigurationException("Invalid root node: " + 
                                             root.getTagName());
         if (root.getAttribute("type").length() == 0)
            throw new ConfigurationException("Unspecified root node type");
      }
      
      catch (Exception exc)
      {
         throw new ConfigurationException(
            "Error loading bootstrap configuration file " + filename, exc);
      }
      
      return root;
   }

   /**
    * Queries the configuration type.
    *
    * @return   <code>true</code> if this is a server configuration
    */
   public boolean isServer()
   {
      return isServer;
   }

   /**
    * Sets the configuration type.
    *
    * @param    type
    *           <code>true</code> to mark this as a server, <code>false</code>
    *           to represent a client.
    */
   public void setServer(boolean type)
   {
      isServer = type;
   }

   /**
    * Enumerates all defined categories.
    *
    * @return   The array of all category names or <code>null</code>.
    */
   public String[] listCategories()
   {
      Set<String> s = categories.keySet();

      return (s.size() == 0) ? null : (String[]) s.toArray(new String[0]);
   }

   /**
    * Enumerates all defined groups for a category.
    *
    * @param    cname
    *           The category name.
    *
    * @return   The array of all group names or <code>null</code>.
    */
   public String[] listGroups(String cname)
   {
      // find category
      Map<String, Map<String, String>> cmap = categories.get(cname.toUpperCase());
      
      if (cmap == null)
         return null;

      Set<String> s = cmap.keySet();
      
      return (s.size() == 0) ? null : (String[]) s.toArray(new String[0]);
   }

   /**
    * Enumerates all defined keys for a group in a category.
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    *
    * @return   The array of all key names or <code>null</code>.
    */
   public String[] listKeys(String cname, String gname)
   {
      // find category
      Map<String, Map<String, String>> cmap = categories.get(cname.toUpperCase());
      
      if (cmap == null)
         return null;

      // find group
      String gn = gname.toUpperCase();
      Map<String, String> gmap = cmap.get(gn);
      
      if (gmap == null)
         return null;

      Set<String> s = gmap.keySet();
      
      return (s.size() == 0) ? null : (String[]) s.toArray(new String[0]);
   }

   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    *
    * @param    clientConfig
    *           The client config.
    * @param    def
    *           The default value to return if the specified value does not exist.
    *
    * @return   The text value of the specified key or the default value if
    *           undefined.
    */
   public String getString(ConfigItem<String> clientConfig, String def)
   {
      return getString(clientConfig.category(), clientConfig.group(), clientConfig.key(), def);
   }
   
   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    def
    *           The default value to return if the specified value does not
    *           exist.
    *
    * @return   The text value of the specified key or the default value if
    *           undefined.
    */
   public String getString(String cname,
                           String gname,
                           String kname,
                           String def)
   {
      String result = null;
      try
      {
         result = getConfigItem(cname, gname, kname);
      }
      catch (ConfigurationException e)
      {
         // the sensible default behavior is to ignore configuration errors
      }
      return (result == null) ? def : result;
   }

   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a base-10 integer and returned.
    *
    * @param    clientConfig
    *           The client config.
    * @param    def
    *           The default value to return if the specified value does not exist.
    *
    * @return   The integer value of the specified key or the default value
    *           if undefined.
    */
   public int getInt(ConfigItem<Integer> clientConfig, int def)
   {
      return getInt(clientConfig.category(), clientConfig.group(), clientConfig.key(), def);
   }

   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a base-10 integer and returned.
    *
    * @param    clientConfig
    *           The client config.
    * @param    def
    *           The default value to return if the specified value does not exist.
    *
    * @return   The integer value of the specified key or the default value
    *           if undefined.
    */
   public Integer getInt(ConfigItem<Integer> clientConfig, Integer def)
   {
      return getInt(clientConfig.category(), clientConfig.group(), clientConfig.key(), def);
   }
   
   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a base-10 integer and returned.
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    def
    *           The default value to return if the specified value does not
    *           exist.
    *
    * @return   The integer value of the specified key or the default value
    *           if undefined.
    */
   public int getInt(String cname,
                     String gname,
                     String kname,
                     int    def)
   {
      String value = null;
      try
      {
         value = getConfigItem(cname, gname, kname);
      }
      catch (ConfigurationException e)
      {
         // the sensible default behavior is to ignore configuration errors
      }
      
      int result = -1;
      
      try
      {
         if (value != null)
         {
            result = Integer.parseInt(value, 10);
         }
      }
      catch (NumberFormatException nfe)
      {
         value = null;
      }
      
      return (value == null) ? def : result;
   }

   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a base-10 integer and returned.
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    def
    *           The default value to return if the specified value does not
    *           exist.
    *
    * @return   The integer value of the specified key or the default value
    *           if undefined.
    */
   public Integer getInt(String  cname,
                         String  gname,
                         String  kname,
                         Integer def)
   {
      String value = null;
      try
      {
         value = getConfigItem(cname, gname, kname);
      }
      catch (ConfigurationException e)
      {
         // the sensible default behavior is to ignore configuration errors
      }

      int result = -1;

      try
      {
         if (value != null)
         {
            result = Integer.parseInt(value, 10);
         }
      }
      catch (NumberFormatException nfe)
      {
         value = null;
      }

      return (value == null) ? def : Integer.valueOf(result);
   }
   
   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a <code>boolean</code> literal (the
    * value must be the text "true" or "false").
    *
    * @param    clientConfig
    *           The client config.
    * @param    def
    *           The default value to return if the specified value does not exist.
    *
    * @return   The boolean value of the specified key or the default value
    *           if undefined.
    */
   public Boolean getBoolean(ConfigItem<Boolean> clientConfig, Boolean def)
   {
      return getBoolean(clientConfig.category(), clientConfig.group(), clientConfig.key(), def);
   }
   
   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a <code>boolean</code> literal (the
    * value must be the text "true" or "false").
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    def
    *           The default value to return if the specified value does not
    *           exist.
    *
    * @return   The boolean value of the specified key or the default value
    *           if undefined.
    */
   public boolean getBoolean(String  cname,
                             String  gname,
                             String  kname,
                             boolean def)
   {
      String value = null;
      try
      {
         value = getConfigItem(cname, gname, kname);
      }
      catch (ConfigurationException e)
      {
         // the sensible default behavior is to ignore configuration errors
      }
      return (value == null) ? def : Boolean.valueOf(value).booleanValue();
   }

   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be parsed as a <code>boolean</code> literal (the
    * value must be the text "true" or "false").
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    def
    *           The default value to return if the specified value does not
    *           exist.
    *
    * @return   The boolean value of the specified key or the default value
    *           if undefined.
    */
   public Boolean getBoolean(String  cname,
                             String  gname,
                             String  kname,
                             Boolean def)
   {
      String value = null;
      try
      {
         value = getConfigItem(cname, gname, kname);
      }
      catch (ConfigurationException e)
      {
         // the sensible default behavior is to ignore configuration errors
      }
      return (value == null) ? def : Boolean.valueOf(value);
   }
   
   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, which look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    * <p>
    * Any value found will be used as a simple filename first.  If a file
    * system resource can be accessed via this name, then that input stream
    * will be returned.  Otherwise, that value will be assumed to be a URL
    * and the system's classloader will be used to access the resource as an
    * input stream.
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    def
    *           The default input stream specification (as a simple filename
    *           or as a URL from which to load as a system resource) to use
    *           for creating an input stream if the specified value does not
    *           exist.
    *
    * @return   The input stream as specified by the value found or by the
    *           default value if undefined.  <code>null</code> is returned
    *           if there is no stream that can be created to represent the
    *           value found or the default value provided.
    */
   public InputStream getInputStream(String cname,
                                     String gname,
                                     String kname,
                                     String def)
   {
      String value = getString(cname, gname, kname, def);
      
      if (value == null)
      {
         // don't bother with anything else, there is no specification to use
         return null;
      }
      
      InputStream is = null;
      
      try
      {
         File file = new File(value);
         
         if (file.exists())
         {
            is = new FileInputStream(value);
         }
         else
         {
            URL url = ClassLoader.getSystemResource(value);
            if (url == null)
            {
               return null;
            }
            is = url.openStream();
         }
      }
      
      catch (Exception exc)
      {
         // allow null to return below
      }
      
      return is;
   }
   
   /**
    * Queries the value for a given key within the specified category and
    * group. Resolves recursive references to another keys.
    * <p>
    * Recursive references are specially coded values, whick look like 
    * "#category.group.key". They start with '#' character and contain two
    * dots. An attempt is made to query the referenced key, which in turn may
    * be a reference. However, no more than 5 references are allowed in the
    * chain.
    *
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    *
    * @return   The text value of the specified key or <code>null</code> if
    *           undefined.
    *
    * @throws   ConfigurationException
    *           If recursive references are too deeply nested.
    */
   public String getConfigItem(String cname, String gname, String kname)
   throws ConfigurationException
   {
      int depth = 0;
      String cn = cname.toUpperCase();
      String gn = gname.toUpperCase();
      String kn = kname.toUpperCase();
      String value = null;
      String vv = null;
      int i = 0;

      while (depth < 5)
      {
         // find category
         Map<String, Map<String, String>> cmap = categories.get(cn);
         if (cmap == null)
            return null;

         // find group
         Map<String, String> gmap = cmap.get(gn);
         if (gmap == null)
            return null;

         // find key
         if (!gmap.containsKey(kn))
            return null;

         value = gmap.get(kn);
         
         // check for a reference
         if (!value.startsWith("#"))
             return value;

         vv = value.substring(1);
         i = vv.indexOf('.');
         if (i == -1)
             return value;
         cn = vv.substring(0, i);

         vv = vv.substring(i + 1);
         i = vv.indexOf('.');
         if (i == -1)
             return value;
         gn = vv.substring(0, i);

         kn = vv.substring(i + 1);

         depth ++;
         cn = cn.toUpperCase();
         gn = gn.toUpperCase();
         kn = kn.toUpperCase();
      }

      throw new ConfigurationException("Too deep nesting for " + 
                cname + "." + gname + "." + kname);
   }
   
   /**
    * Adds or sets a value for a triplet {category, group, key}. Any existing
    * value gets replaced with the new one.
    * <p>
    * All names are uppercased before use.
    * 
    * @param    clientConfig
    *           The client config.
    * @param    kvalue
    *           The value to be stored.
    */
   public void setConfigItem(ConfigItem clientConfig, String kvalue)
   {
      setConfigItem(clientConfig.category(), clientConfig.group(), clientConfig.key(), kvalue);
   }
   
   /**
    * Adds or sets a value for a triplet {category, group, key}. Any existing
    * value gets replaced with the new one.
    * <p>
    * All names are uppercased before use.
    * 
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    kvalue
    *           The value to be stored.
    */
   public void setConfigItem(String cname,
                             String gname,
                             String kname,
                             String kvalue)
   {
      addConfigItem(cname, gname, kname, kvalue, true);
   }

   /**
    * Copy the given configuration items from the specified source.
    * 
    * @param    items
    *           A list of items, specified using the "category:group:key" format.
    * @param    source
    *           The source from which to copy the configuration.
    *
    * @throws   ConfigurationException
    *           If an item is not set ({@code null} or empty), doesn't follow the categ:group:key
    *           format or recursive references are too deeply nested.
    */
   public void copyConfigItems(String[] items, BootstrapConfig source)
   throws ConfigurationException
   {
      for (int i = 0; i < items.length; i++)
      {
         String item = items[i];
         
         if (item == null || items[i].length() == 0)
         {
            final String msg = "Item on index %d is not set.";
            throw new ConfigurationException(String.format(msg, i));
         }
         
         String[] names = item.split(":");
         
         if (names.length != 3)
         {
            final String msg = "Item %s on index %d is malformed.";
            throw new ConfigurationException(String.format(msg, item, i));
         }
         
         String val = source.getConfigItem(names[0], names[1], names[2]);
         if (val != null)
         {
            addConfigItem(names[0], names[1], names[2], val, true);
         }
      }
   }

   /**
    * Add any new values and replace any duplicated values in the
    * configuration from a list of strings.  These strings can have a long 
    * form of "category:group:key=value" or in the short form of "key=value" 
    * (which uses the most recently specified category and group).
    *
    * @param    list
    *           The list of override strings.
    *           
    * @return   <code>true</code> if all overrides successfully processed, <code>false</code> otherwise.
    */
   public boolean processOverrides(List<String> list)
   {
      Iterator<String> iter   = list.iterator();
      String           curcat = null;
      String           curgrp = null;

      boolean isSuccessful = true;
      while (iter.hasNext())
      {
         // get and parse the next string
         String over  = iter.next();
         String cat   = curcat;
         String grp   = curgrp;
         int    eq    = over.indexOf('=');
         if (eq == -1)
         {
            continue;
         }
         String front = over.substring(0, eq);    // [[cat:]grp:]key
         String value = over.substring(eq + 1);   // value
         String key   = null;
         int    co    = front.indexOf(':');

         if (co != -1)
         {
            grp    = front.substring(0, co);
            curgrp = grp;
            key    = front.substring(co + 1);
         }
         else
         {
            cat = curcat;
            grp = curgrp;
            key = front;
         }

         co = key.indexOf(':');

         if (co != -1)
         {
            front  = key;
            cat    = grp;
            curcat = grp;
            grp    = front.substring(0, co);
            curgrp = grp;
            key    = front.substring(co + 1);
         }

         if (cat == null || cat.length() == 0)
         {
            LOG.log(Level.SEVERE, 
                    "Category MUST be specified. Attempt to resolve '%s:%s:%s'.",
                    cat,
                    grp,
                    key);
            isSuccessful = false;
            continue;
         }

         if (grp == null || grp.length() == 0)
         {
            LOG.log(Level.SEVERE,
                    "Group MUST be specified. Attempt to resolve '%s:%s:%s'.",
                    cat,
                    grp,
                    key);
            isSuccessful = false;
            continue;
         }

         setConfigItem(cat, grp, key, value);
      }
      return isSuccessful;
   }

   /**
    * Utility method to add a value for a tripplet {category, group, key}
    * into internal maps.
    * <p>
    * All names are uppercased before use. Duplicating keys are ignored.
    * 
    * @param    cname
    *           The category name.
    * @param    gname
    *           The group name.
    * @param    kname
    *           The key name.
    * @param    kvalue
    *           The value to be stored.
    * @param    force
    *           <code>true</code> to set the value regardless of whether the
    *           specified category + group + key already exists.
    *
    * @return   <code>true</code> if added successfully, <code>false</code>
    *           if the key is already defined (and <code>force</code> is
    *           also <code>false</code> since otherwise the set will always
    *           occur).
    */
   private boolean addConfigItem(String  cname,
                                 String  gname,
                                 String  kname,
                                 String  kvalue,
                                 boolean force)
   {
      String cn = cname.toUpperCase();
      String gn = gname.toUpperCase();
      String kn = kname.toUpperCase();
      
      // find or create category
      Map<String, Map<String, String>> cmap = categories.get(cn);
      if (cmap == null)
      {
         cmap = new LinkedHashMap<>();
         categories.put(cn, cmap);
      }

      // find or create group
      Map<String, String> gmap = cmap.get(gn);
      if (gmap == null)
      {
         gmap = new LinkedHashMap<>();
         cmap.put(gn, gmap);
      }
      
      // check and create key
      if (!force && gmap.containsKey(kn))
         return false;

      gmap.put(kn, kvalue);
      return true;
   }

   /**
    * Utility method to walk an in memory DOM document and extract all
    * categories, groups, keys and values.
    * 
    * @param    root
    *           The root element of a loaded XML document.
    */
   private void walkXml(Element root)
   {
      NodeList cl = null;
      NodeList gl = null;
      NamedNodeMap al = null; 
      Node cn = null;
      Node gn = null;
      Node an = null;
      
      // list top level nodes which are categories
      cl = root.getChildNodes();
      if (cl == null)
         return;
      
      int i = 0;
      int j = 0;
      int k = 0;
      
      // walk the list of nodes and process elements
      for (i = 0; i < cl.getLength(); i ++)
      {
         cn = cl.item(i);
         if (cn.getNodeType() != Node.ELEMENT_NODE)
            continue;

         // list all groups defined within this category
         gl = cn.getChildNodes();

         // walk the list of groups and process attributes
         for (j = 0; j < gl.getLength(); j ++)
         {
            gn = gl.item(j);
            if (gn.getNodeType() != Node.ELEMENT_NODE)
               continue;

            // list all defined keys
            al = gn.getAttributes();
            
            // walk the list of keys and process values
            for (k = 0; k < al.getLength(); k ++)
            {
               an = al.item(k);
               
               // add new item to the configuration
               addConfigItem(cn.getNodeName(),
                             gn.getNodeName(),
                             an.getNodeName(),
                             an.getNodeValue(),
                             false);
            }
         }
      }
   }
   
   /**
    * Initialize and return an appropriate cipher instance.
    *
    * @param    passphrase
    *           The password on which the encryption or decryption will be
    *           based.
    * @param    mode
    *           <code>true</code> to encrypt, <code>false</code> to decrypt.
    *
    * @return   The properly initialized cipher.
    * 
    * @throws   NoSuchAlgorithmException
    *           No such algorithm exception
    * @throws   InvalidKeySpecException
    *           Invalid key spec exception
    * @throws   InvalidKeyException
    *           Invalid key exception
    * @throws   NoSuchPaddingException
    *           No such padding exception
    * @throws   InvalidAlgorithmParameterException
    *           Invalid algorithm parameter exception
    */
   private static Cipher getCipher(char[] passphrase, boolean mode)
   throws NoSuchAlgorithmException,
          InvalidKeySpecException,
          InvalidKeyException,
          NoSuchPaddingException,
          InvalidAlgorithmParameterException
   {
      PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count);
      PBEKeySpec pbeKeySpec = new PBEKeySpec(passphrase);
      SecretKeyFactory keyFac = SecretKeyFactory.getInstance(ALGORITHM);
      SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
      Cipher cipher = Cipher.getInstance(ALGORITHM);
      cipher.init(mode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE,
                  pbeKey,
                  pbeParamSpec);
      
      return cipher;
   }
   
   /**
    * Serves as a command line encryption/decryption utility that transforms
    * the input file into the output file.
    * <p>
    * Syntax:
    * <pre>
    *    java BootstrapConfig [encrypt | decrypt] infile outfile
    * </pre>
    * <p>
    * As this is a command line tool, the user is prompted for a passphrase.
    * <p>
    * This method employs the javax.crypto package's "PBEWithMD5AndDES" 
    * algorithm.
    * 
    * @param    args
    *           The command line arguments.
    */
   public static void main(String[] args)
   {
      // check syntax 
      if (args.length != 3 || (!args[0].equals("encrypt") &&
                               !args[0].equals("decrypt")))
      {
         LOG.severe("Syntax:" + System.lineSeparator() +
                       "   java BootstrapConfig [encrypt | decrypt] infile outfile");
         System.exit(-1);
      }

      // parse arguments
      boolean mode = args[0].equals("encrypt");
      
      try
      {
         char[] pw     = Utils.prompt("Enter encryption password:  ");
         Cipher cipher = getCipher(pw, mode);
         Cipher transp = new NullCipher();

         FileInputStream    fis = new FileInputStream(args[1]);
         FileOutputStream   fos = new FileOutputStream(args[2]);
         CipherInputStream  cis = null; 
         CipherOutputStream cos = null;

         if (mode)
         {
            cis = new CipherInputStream(fis, transp);
            cos = new CipherOutputStream(fos, cipher);
         }
         else
         {
            cis = new CipherInputStream(fis, cipher);
            cos = new CipherOutputStream(fos, transp);
         }

         byte[] b = new byte[8];
         int i = cis.read(b);
         
         while (i != -1)
         {
            cos.write(b, 0, i);
            i = cis.read(b);
         }
         
         cis.close();
         cos.close();
      }
      
      catch (Exception e)
      {
         LOG.severe("Exception", e);
      }
   }

   /**
    * Prints the bootstrap configuration.
    * 
    * @return   The state of this instance including formatted lines with key = value per line.
    */
   @Override
   public String toString()
   {
      StringBuilder buff = new StringBuilder();
      
      // render the global state of the instance 
      buff.append(String.format("sharedFile = %s; privateFile = %s; isServer = %b; configuraton values =\n   {\n",
                                (sharedFile == null) ? "none" : sharedFile,
                                (privateFile == null) ? "none" : privateFile,
                                isServer));
      
      for (String c : categories.keySet())
      {
         Map<String, Map<String, String>> groups = categories.get(c);
         
         for (String g : groups.keySet())
         {
            Map<String, String> keys = groups.get(g);
            
            for (String k : keys.keySet())
            {
               buff.append(String.format("      %s:%s:%s = %s\n", c, g, k, keys.get(k)));
            }
         }
      }
      
      buff.append("   }");
      
      return buff.toString();
   }
}