Properties.java
// Copyright (c) ZeroC, Inc.
package com.zeroc.Ice;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PushbackInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A property set used to configure Ice and Ice applications. Properties are key/value pairs, with
* both keys and values being strings. By convention, property keys should have the form
* <em>application-name</em>[.<em>category</em>[.<em>sub-category</em>]].<em>name</em>.
*/
public final class Properties {
static class PropertyValue {
public PropertyValue(String v, boolean u) {
value = v;
used = u;
}
public PropertyValue clone() {
return new PropertyValue(value, used);
}
public String value;
public boolean used;
}
/** Constructs an empty property set. */
public Properties() {}
/**
* Creates a property set initialized with a list of opt-in prefixes.
*
* @param optInPrefixes A list of prefixes that are opt-in.
*/
public Properties(List<String> optInPrefixes) {
_optInPrefixes.addAll(optInPrefixes);
}
/**
* Creates a property set initialized from an argument vector.
*
* @param args A command-line argument vector, possibly containing options to set properties. If
* the command-line options include a <code>--Ice.Config</code> option, the corresponding
* configuration files are parsed. If the same property is set in a configuration file and
* in the argument vector, the argument vector takes precedence.
*/
public Properties(String[] args) {
this(args, null, null);
}
/**
* Creates a property set initialized from an argument vector and return the remaining
* arguments.
*
* @param args A command-line argument vector, possibly containing options to set properties. If
* the command-line options include a <code>--Ice.Config</code> option, the corresponding
* configuration files are parsed. If the same property is set in a configuration file and
* in the argument vector, the argument vector takes precedence.
* @param remainingArgs If non null, the given list will contain on return the command-line
* arguments that were not used to set properties.
*/
public Properties(String[] args, List<String> remainingArgs) {
this(args, null, remainingArgs);
}
/**
* Creates a property set initialized from an argument vector.
*
* @param args A command-line argument vector, possibly containing options to set properties. If
* the command-line options include a <code>--Ice.Config</code> option, the corresponding
* configuration files are parsed. If the same property is set in a configuration file and
* in the argument vector, the argument vector takes precedence.
* @param defaults Default values for the property set. Settings in configuration files and
* <code>
* args</code> override these defaults.
*/
public Properties(String[] args, Properties defaults) {
this(args, defaults, null);
}
/**
* Creates a property set initialized from an argument vector and return the remaining
* arguments.
*
* @param args A command-line argument vector, possibly containing options to set properties. If
* the command-line options include a <code>--Ice.Config</code> option, the corresponding
* configuration files are parsed. If the same property is set in a configuration file and
* in the argument vector, the argument vector takes precedence.
* @param defaults Default values for the property set. Settings in configuration files and
* <code>
* args</code> override these defaults.
* @param remainingArgs If non null, the given list will contain on return the command-line
* arguments that were not used to set properties.
*/
public Properties(String[] args, Properties defaults, List<String> remainingArgs) {
if (defaults != null) {
//
// NOTE: we can't just do a shallow copy of the map as the map values
// would otherwise be shared between the two Properties object.
//
for (Map.Entry<String, PropertyValue> p : defaults._properties.entrySet()) {
_properties.put(p.getKey(), p.getValue().clone());
}
_optInPrefixes.addAll(defaults._optInPrefixes);
}
boolean loadConfigFiles = false;
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("--Ice.Config")) {
String line = args[i];
if (line.indexOf('=') == -1) {
line += "=1";
}
parseLine(line.substring(2));
loadConfigFiles = true;
String[] arr = new String[args.length - 1];
System.arraycopy(args, 0, arr, 0, i);
if (i < args.length - 1) {
System.arraycopy(args, i + 1, arr, i, args.length - i - 1);
}
args = arr;
}
}
if (!loadConfigFiles) {
//
// If Ice.Config is not set, load from ICE_CONFIG (if set)
//
loadConfigFiles = !_properties.containsKey("Ice.Config");
}
if (loadConfigFiles) {
loadConfig();
}
args = parseIceCommandLineOptions(args);
if (remainingArgs != null) {
remainingArgs.clear();
if (args.length > 0) {
remainingArgs.addAll(Arrays.asList(args));
}
}
}
/**
* Get a property by key. If the property is not set, an empty string is returned.
*
* @param key The property key.
* @return The property value.
* @see #setProperty
*/
public synchronized String getProperty(String key) {
PropertyValue pv = _properties.get(key);
if (pv != null) {
pv.used = true;
return pv.value;
} else {
return "";
}
}
/**
* Get an Ice property by key. If the property is not set, its default value is returned.
*
* @param key The property key.
* @return The property value or the default value.
* @throws PropertyException If the property is not a known Ice property.
* @see #setProperty
*/
public synchronized String getIceProperty(String key) {
PropertyValue pv = _properties.get(key);
if (pv != null) {
pv.used = true;
return pv.value;
} else {
return getDefaultProperty(key);
}
}
/**
* Get a property by key. If the property is not set, the given default value is returned.
*
* @param key The property key.
* @param value The default value to use if the property does not exist.
* @return The property value or the default value.
* @see #setProperty
*/
public synchronized String getPropertyWithDefault(String key, String value) {
PropertyValue pv = _properties.get(key);
if (pv != null) {
pv.used = true;
return pv.value;
} else {
return value;
}
}
/**
* Get a property as an integer. If the property is not set, 0 is returned.
*
* @param key The property key.
* @return The property value interpreted as an integer.
* @throws PropertyException If the property value is not a valid integer.
* @see #setProperty
*/
public int getPropertyAsInt(String key) {
return getPropertyAsIntWithDefault(key, 0);
}
/**
* Get an Ice property as an integer. If the property is not set, its default value is returned.
*
* @param key The property key.
* @return The property value interpreted as an integer, or the default value.
* @throws PropertyException If the property is not a known Ice property or the value is
* not a valid integer.
* @see #setProperty
*/
public synchronized int getIcePropertyAsInt(String key) {
String defaultValueString = getDefaultProperty(key);
int defaultValue = 0;
if (defaultValueString != "") {
// These defaults are assigned by us and are guaranteed to be integers.
defaultValue = Integer.parseInt(defaultValueString);
}
return getPropertyAsIntWithDefault(key, defaultValue);
}
/**
* Get a property as an integer. If the property is not set, the given default value is
* returned.
*
* @param key The property key.
* @param value The default value to use if the property does not exist.
* @return The property value interpreted as an integer, or the default value.
* @throws PropertyException If the property value is not a valid integer.
* @see #setProperty
*/
public synchronized int getPropertyAsIntWithDefault(String key, int value) {
PropertyValue pv = _properties.get(key);
if (pv != null) {
pv.used = true;
try {
return Integer.parseInt(pv.value);
} catch (NumberFormatException ex) {
throw new PropertyException(
"property '" + key + "' has an invalid integer value: '" + pv.value + "'");
}
}
return value;
}
/**
* Get a property as a list of strings. The strings must be separated by whitespace or comma. If
* the property is not set, an empty list is returned. The strings in the list can contain
* whitespace and commas if they are enclosed in single or double quotes. If quotes are
* mismatched, an empty list is returned. Within single quotes or double quotes, you can escape
* the quote in question with a backslash, e.g. O'Reilly can be written as O'Reilly, "O'Reilly"
* or 'O\'Reilly'.
*
* @param key The property key.
* @return The property value interpreted as a list of strings.
* @see #setProperty
*/
public String[] getPropertyAsList(String key) {
return getPropertyAsListWithDefault(key, null);
}
/**
* Get an Ice property as a list of strings. The strings must be separated by whitespace or
* comma. If the property is not set, its default list is returned. The strings in the list can
* contain whitespace and commas if they are enclosed in single or double quotes. If quotes are
* mismatched, the default list is returned. Within single quotes or double quotes, you can
* escape the quote in question with a backslash, e.g. O'Reilly can be written as O'Reilly,
* "O'Reilly" or 'O\'Reilly'.
*
* @param key The property key.
* @return The property value interpreted as list of strings, or the default value.
* @throws PropertyException If the property is not a known Ice property.
* @see #setProperty
*/
public synchronized String[] getIcePropertyAsList(String key) {
String[] defaultList = StringUtil.splitString(getDefaultProperty(key), ", \t\r\n");
return getPropertyAsListWithDefault(key, defaultList);
}
/**
* Get a property as a list of strings. The strings must be separated by whitespace or comma. If
* the property is not set, the default list is returned. The strings in the list can contain
* whitespace and commas if they are enclosed in single or double quotes. If quotes are
* mismatched, the default list is returned. Within single quotes or double quotes, you can
* escape the quote in question with a backslash, e.g. O'Reilly can be written as O'Reilly,
* "O'Reilly" or 'O\'Reilly'.
*
* @param key The property key.
* @param value The default value to use if the property is not set.
* @return The property value interpreted as list of strings, or the default value.
* @see #setProperty
*/
public synchronized String[] getPropertyAsListWithDefault(String key, String[] value) {
if (value == null) {
value = new String[0];
}
PropertyValue pv = _properties.get(key);
if (pv != null) {
pv.used = true;
String[] result = StringUtil.splitString(pv.value, ", \t\r\n");
if (result == null) {
Util.getProcessLogger()
.warning(
"mismatched quotes in property "
+ key
+ "'s value, returning default value");
return value;
}
if (result.length == 0) {
result = value;
}
return result;
} else {
return value;
}
}
/**
* Get all properties whose keys begins with <em>prefix</em>. If <em>prefix</em> is an empty
* string, then all properties are returned.
*
* @param prefix The prefix to search for (empty string if none).
* @return The matching property set.
*/
public synchronized Map<String, String> getPropertiesForPrefix(String prefix) {
HashMap<String, String> result = new HashMap<>();
for (Map.Entry<String, PropertyValue> p : _properties.entrySet()) {
String key = p.getKey();
if (prefix.isEmpty() || key.startsWith(prefix)) {
PropertyValue pv = p.getValue();
pv.used = true;
result.put(key, pv.value);
}
}
return result;
}
/**
* Set a property. To unset a property, set it to the empty string.
*
* @param key The property key.
* @param value The property value.
* @see #getProperty
*/
public void setProperty(String key, String value) {
//
// Trim whitespace
//
if (key != null) {
key = key.trim();
}
if (key == null || key.isEmpty()) {
throw new InitializationException("Attempt to set property with empty key");
}
// Check if the property is in an Ice property prefix. If so, check that it's a valid
// property.
PropertyArray propertyArray = findIcePropertyArray(key);
if (propertyArray != null) {
if (propertyArray.isOptIn()
&& _optInPrefixes.stream().noneMatch(propertyArray.name()::equals)) {
throw new PropertyException(
"unable to set '"
+ key
+ "': property prefix '"
+ propertyArray.name()
+ "' is opt-in and must be explicitly enabled");
}
Property prop =
findProperty(key.substring(propertyArray.name().length() + 1), propertyArray);
if (prop == null) {
throw new PropertyException("unknown Ice property: " + key);
}
// If the property is deprecated, log a warning
if (prop.deprecated()) {
Util.getProcessLogger().warning("setting deprecated property: " + key);
}
}
synchronized (this) {
//
// Set or clear the property.
//
if (value != null && !value.isEmpty()) {
PropertyValue pv = _properties.get(key);
if (pv != null) {
pv.value = value;
} else {
pv = new PropertyValue(value, false);
}
_properties.put(key, pv);
} else {
_properties.remove(key);
}
}
}
/**
* Get a sequence of command-line options that is equivalent to this property set. Each element
* of the returned sequence is a command-line option of the form <code>
* --<em>key</em>=<em>value</em>
* </code>.
*
* @return The command line options for this property set.
*/
public synchronized String[] getCommandLineOptions() {
String[] result = new String[_properties.size()];
int i = 0;
for (Map.Entry<String, PropertyValue> p : _properties.entrySet()) {
result[i++] = "--" + p.getKey() + "=" + p.getValue().value;
}
assert (i == result.length);
return result;
}
/**
* Convert a sequence of command-line options into properties. All options that begin with
* <code>
* --<em>prefix</em>.</code> are converted into properties. If the prefix is empty, all options
* that begin with <code>--</code> are converted to properties.
*
* @param prefix The property prefix, or an empty string to convert all options starting with
* <code>--</code>.
* @param options The command-line options.
* @return The command-line options that do not start with the specified prefix, in their
* original order.
*/
public String[] parseCommandLineOptions(String prefix, String[] options) {
if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) != '.') {
prefix += '.';
}
prefix = "--" + prefix;
ArrayList<String> result = new ArrayList<>();
for (String opt : options) {
if (opt.startsWith(prefix)) {
if (opt.indexOf('=') == -1) {
opt += "=1";
}
parseLine(opt.substring(2));
} else {
result.add(opt);
}
}
return result.toArray(new String[0]);
}
/**
* Convert a sequence of command-line options into properties. All options that begin with one
* of the following prefixes are converted into properties: <code>--Ice</code>, <code>--IceBox
* </code> , <code>--IceGrid</code>, <code>--Ice.SSL</code>, <code>--IceStorm</code>, and <code>
* --Glacier2</code>.
*
* @param options The command-line options.
* @return The command-line options that do not start with one of the listed prefixes, in their
* original order.
*/
public String[] parseIceCommandLineOptions(String[] options) {
String[] args = options;
for (PropertyArray props : PropertyNames.validProps) {
args = parseCommandLineOptions(props.name(), args);
}
return args;
}
/**
* Load properties from a file.
*
* @param file The property file.
*/
public void load(String file) {
if (System.getProperty("os.name").startsWith("Windows")
&& (file.startsWith("HKCU\\") || file.startsWith("HKLM\\"))) {
try {
java.lang.Process process =
Runtime.getRuntime().exec(new String[]{"reg", "query", file});
process.waitFor();
if (process.exitValue() != 0) {
throw new InitializationException(
"Could not read Windows registry key '" + file + "'");
}
java.io.InputStream is = process.getInputStream();
StringWriter sw = new StringWriter();
int c;
while ((c = is.read()) != -1) {
sw.write(c);
}
String[] result = sw.toString().split("\n");
for (String line : result) {
int pos = line.indexOf("REG_SZ");
if (pos != -1) {
setProperty(
line.substring(0, pos).trim(),
line.substring(pos + 6, line.length()).trim());
continue;
}
pos = line.indexOf("REG_EXPAND_SZ");
if (pos != -1) {
String name = line.substring(0, pos).trim();
line = line.substring(pos + 13, line.length()).trim();
while (true) {
int start = line.indexOf('%', 0);
int end = line.indexOf('%', start + 1);
//
// If there isn't more %var% break the loop
//
if (start == -1 || end == -1) {
break;
}
String envKey = line.substring(start + 1, end);
String envValue = System.getenv(envKey);
if (envValue == null) {
envValue = "";
}
envKey = "%" + envKey + "%";
do {
line = line.replace(envKey, envValue);
} while (line.indexOf(envKey) != -1);
}
setProperty(name, line);
continue;
}
}
} catch (LocalException ex) {
throw ex;
} catch (Exception ex) {
throw new InitializationException(
"Could not read Windows registry key `" + file + "'", ex);
}
} else {
PushbackInputStream is = null;
try {
java.io.InputStream f = Util.openResource(getClass().getClassLoader(), file);
if (f == null) {
throw new FileException("failed to open '" + file + "'");
}
//
// Skip UTF-8 BOM if present.
//
byte[] bom = new byte[3];
is = new PushbackInputStream(f, bom.length);
int read = is.read(bom, 0, bom.length);
if (read < 3
|| bom[0] != (byte) 0xEF
|| bom[1] != (byte) 0xBB
|| bom[2] != (byte) 0xBF) {
if (read > 0) {
is.unread(bom, 0, read);
}
}
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
parse(br);
} catch (IOException ex) {
throw new FileException("Cannot read '" + file + "'", ex);
} finally {
if (is != null) {
try {
is.close();
} catch (Throwable ex) {
// Ignore.
}
}
}
}
}
/**
* Create a copy of this property set.
*
* @return A copy of this property set.
*/
public synchronized Properties _clone() {
Properties clonedProperties = new Properties(_optInPrefixes);
//
// NOTE: we can't just do a shallow copy of the map as the map values
// would otherwise be shared between the two Properties objects.
//
// _properties = new java.util.HashMap<String, PropertyValue>(props._properties);
for (Map.Entry<String, PropertyValue> p : _properties.entrySet()) {
clonedProperties._properties.put(p.getKey(), p.getValue().clone());
}
return clonedProperties;
}
/**
* Gets the properties that were never read.
*
* @return a list of unused properties
*/
public synchronized List<String> getUnusedProperties() {
List<String> unused = new ArrayList<>();
for (Map.Entry<String, PropertyValue> p : _properties.entrySet()) {
PropertyValue pv = p.getValue();
if (!pv.used) {
unused.add(p.getKey());
}
}
return unused;
}
private void parse(BufferedReader in) {
try {
String line;
while ((line = in.readLine()) != null) {
parseLine(line);
}
} catch (IOException ex) {
throw new SyscallException(ex);
}
}
private void parseLine(String line) {
String key = "";
String value = "";
int state = ParseStateKey;
String whitespace = "";
String escapedspace = "";
boolean finished = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
switch (state) {
case ParseStateKey: {
switch (c) {
case '\\':
if (i < line.length() - 1) {
c = line.charAt(++i);
switch (c) {
case '\\':
case '#':
case '=':
key += whitespace;
whitespace = "";
key += c;
break;
case ' ':
if (!key.isEmpty()) {
whitespace += c;
}
break;
default:
key += whitespace;
whitespace = "";
key += '\\';
key += c;
break;
}
} else {
key += whitespace;
key += c;
}
break;
case ' ':
case '\t':
case '\r':
case '\n':
if (!key.isEmpty()) {
whitespace += c;
}
break;
case '=':
whitespace = "";
state = ParseStateValue;
break;
case '#':
finished = true;
break;
default:
key += whitespace;
whitespace = "";
key += c;
break;
}
break;
}
case ParseStateValue: {
switch (c) {
case '\\':
if (i < line.length() - 1) {
c = line.charAt(++i);
switch (c) {
case '\\':
case '#':
case '=':
value += value.isEmpty() ? escapedspace : whitespace;
whitespace = "";
escapedspace = "";
value += c;
break;
case ' ':
whitespace += c;
escapedspace += c;
break;
default:
value += value.isEmpty() ? escapedspace : whitespace;
whitespace = "";
escapedspace = "";
value += '\\';
value += c;
break;
}
} else {
value += value.isEmpty() ? escapedspace : whitespace;
value += c;
}
break;
case ' ':
case '\t':
case '\r':
case '\n':
if (!value.isEmpty()) {
whitespace += c;
}
break;
case '#':
finished = true;
break;
default:
value += value.isEmpty() ? escapedspace : whitespace;
whitespace = "";
escapedspace = "";
value += c;
break;
}
break;
}
}
if (finished) {
break;
}
}
value += escapedspace;
if ((state == ParseStateKey && !key.isEmpty())
|| (state == ParseStateValue && key.isEmpty())) {
Util.getProcessLogger().warning("invalid config file entry: \"" + line + "\"");
return;
} else if (key.isEmpty()) {
return;
}
setProperty(key, value);
}
private void loadConfig() {
String value = getIceProperty("Ice.Config");
if (value.isEmpty() || "1".equals(value)) {
try {
value = System.getenv("ICE_CONFIG");
if (value == null) {
value = "";
}
} catch (java.lang.SecurityException ex) {
value = "";
}
}
if (!value.isEmpty()) {
for (String file : value.split(",")) {
load(file.trim());
}
_properties.put("Ice.Config", new PropertyValue(value, true));
}
}
/**
* Find a property by key in a property array.
*
* @param key The property key.
* @param propertyArray The property array to search.
* @return The property if found, null otherwise.
*/
static Property findProperty(String key, PropertyArray propertyArray) {
for (Property prop : propertyArray.properties()) {
String pattern = prop.pattern();
// If the key is an exact match, return the property unless it has a property class
// which is prefix only. If the key is a regex match, return the property. A property
// cannot have a property class and use regex.
if (key.equals(pattern)) {
if (prop.propertyArray() != null && prop.propertyArray().prefixOnly()) {
return null;
}
return prop;
} else if (prop.usesRegex() && key.matches(pattern)) {
return prop;
}
// If the property has a property class, check if the key is a prefix of the property.
if (prop.propertyArray() != null) {
// Check if the key is a prefix of the property.
// The key must be:
// - shorter than the property pattern
// - the property pattern must start with the key
// - the pattern character after the key must be a dot
if (key.length() > pattern.length()
&& key.startsWith(pattern)
&& key.charAt(pattern.length()) == '.') {
String substring = key.substring(pattern.length() + 1);
// Check if the suffix is a valid property. If so, return it. If it's not,
// continue searching the current property array.
Property foundProp = findProperty(substring, prop.propertyArray());
if (foundProp != null) {
return foundProp;
}
}
}
}
return null;
}
/**
* Validate properties with a given prefix.
*
* @param prefix The property prefix.
* @param properties The properties to validate.
* @param propertyArray The property array to validate against.
* @throws PropertyException if any unknown properties are found.
*/
static void validatePropertiesWithPrefix(
String prefix, Properties properties, PropertyArray propertyArray) {
// Do not check for unknown properties if Ice prefix, ie Ice, Glacier2, etc
for (PropertyArray props : PropertyNames.validProps) {
if (prefix.startsWith(props.name() + ".")) {
return;
}
}
var unknownProperties =
properties.getPropertiesForPrefix(prefix + ".").keySet().stream()
.filter(
key ->
findProperty(
key.substring(prefix.length() + 1),
propertyArray)
== null)
.collect(Collectors.toList());
if (unknownProperties.size() > 0) {
throw new PropertyException(
"found unknown properties for "
+ propertyArray.name()
+ ": '"
+ prefix
+ "'\n "
+ String.join("\n ", unknownProperties));
}
}
/**
* Find an Ice property array by key.
*
* @param key The property key.
* @return The property array if found, null otherwise.
*/
private static PropertyArray findIcePropertyArray(String key) {
int dotPos = key.indexOf('.');
// If the key doesn't contain a dot, it's not a valid Ice property.
if (dotPos == -1) {
return null;
}
String prefix = key.substring(0, dotPos);
return Arrays.stream(PropertyNames.validProps)
.filter(properties -> properties.name().equals(prefix))
.findFirst()
.orElse(null);
}
/**
* Gets the default value for a given Ice property.
*
* @param key The property key.
* @return The default value.
* @throws PropertyException if the property is unknown.
*/
private static String getDefaultProperty(String key) {
PropertyArray propertyArray = findIcePropertyArray(key);
if (propertyArray == null) {
throw new PropertyException("unknown Ice property: " + key);
}
Property prop =
findProperty(key.substring(propertyArray.name().length() + 1), propertyArray);
if (prop == null) {
throw new PropertyException("unknown Ice property: " + key);
}
return prop.defaultValue();
}
private static final int ParseStateKey = 0;
private static final int ParseStateValue = 1;
private final HashMap<String, PropertyValue> _properties = new HashMap<>();
private final List<String> _optInPrefixes = new ArrayList<>();
}