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><node type="type"></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>
* <?xml version="1.0"?>
* <node type="client">
* <net>
* <server host="p2jserver"/>
* <server port="3333"/>
* <dispatcher threads="4"/>
* </net>
* <security>
* <truststore filename="trust.store"/>
* <truststore alias="mainserver"/>
* <keystore filename="key.store"/>
* </security>
* </node>
* </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();
}
}