org.apache.ratis.conf.RaftProperties Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ratis.conf;
import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.ReflectionUtils;
import org.apache.ratis.util.SizeInBytes;
import org.apache.ratis.util.StringUtils;
import org.apache.ratis.util.TimeDuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
/**
* Provides access to configuration parameters. The current implementation is a
* simplified version of hadoop's Configuration.
*/
public class RaftProperties {
private static final Logger LOG = LoggerFactory.getLogger(RaftProperties.class);
private final ConcurrentMap properties = new ConcurrentHashMap<>();
/** A new configuration. */
public RaftProperties() {
}
/**
* A new RaftProperties with the same settings cloned from another.
*
* @param other the RaftProperties from which to clone settings.
*/
public RaftProperties(RaftProperties other) {
this.properties.putAll(other.properties);
}
private static final int MAX_SUBST = 20;
private static final int SUB_START_IDX = 0;
private static final int SUB_END_IDX = SUB_START_IDX + 1;
/**
* This is a manual implementation of the following regex
* "\\$\\{[^\\}\\$\u0020]+\\}".
*
* @param eval a string that may contain variables requiring expansion.
* @return a 2-element int array res such that
* eval.substring(res[0], res[1]) is "var" for the left-most occurrence of
* ${var} in eval. If no variable is found -1, -1 is returned.
*/
private static int[] findSubVariable(String eval) {
int[] result = {-1, -1};
int matchStart;
int leftBrace;
// scanning for a brace first because it's less frequent than $
// that can occur in nested class names
//
match_loop:
for (matchStart = 1, leftBrace = eval.indexOf('{', matchStart);
// minimum left brace position (follows '$')
leftBrace > 0
// right brace of a smallest valid expression "${c}"
&& leftBrace + "{c".length() < eval.length();
leftBrace = eval.indexOf('{', matchStart)) {
int matchedLen = 0;
if (eval.charAt(leftBrace - 1) == '$') {
int subStart = leftBrace + 1; // after '{'
for (int i = subStart; i < eval.length(); i++) {
switch (eval.charAt(i)) {
case '}':
if (matchedLen > 0) { // match
result[SUB_START_IDX] = subStart;
result[SUB_END_IDX] = subStart + matchedLen;
break match_loop;
}
// fall through to skip 1 char
case ' ':
case '$':
matchStart = i + 1;
continue match_loop;
default:
matchedLen++;
}
}
// scanned from "${" to the end of eval, and no reset via ' ', '$':
// no match!
break;
} else {
// not a start of a variable
//
matchStart = leftBrace + 1;
}
}
return result;
}
/**
* Attempts to repeatedly expand the value {@code expr} by replacing the
* left-most substring of the form "${var}" in the following precedence order
*
* - by the value of the environment variable "var" if defined
* - by the value of the Java system property "var" if defined
* - by the value of the configuration key "var" if defined
*
*
* If var is unbounded the current state of expansion "prefix${var}suffix" is
* returned.
*
* If a cycle is detected: replacing var1 requires replacing var2 ... requires
* replacing var1, i.e., the cycle is shorter than
* {@link RaftProperties#MAX_SUBST} then the original expr is returned.
*
* @param expr the literal value of a config key
* @return null if expr is null, otherwise the value resulting from expanding
* expr using the algorithm above.
* @throws IllegalArgumentException when more than
* {@link RaftProperties#MAX_SUBST} replacements are required
*/
private String substituteVars(String expr) {
if (expr == null) {
return null;
}
String eval = expr;
Set evalSet = null;
for(int s = 0; s < MAX_SUBST; s++) {
final int[] varBounds = findSubVariable(eval);
if (varBounds[SUB_START_IDX] == -1) {
return eval;
}
final String var = eval.substring(varBounds[SUB_START_IDX],
varBounds[SUB_END_IDX]);
String val = null;
try {
if (var.startsWith("env.") && 4 < var.length()) {
String v = var.substring(4);
int i = 0;
for (; i < v.length(); i++) {
char c = v.charAt(i);
if (c == ':' && i < v.length() - 1 && v.charAt(i + 1) == '-') {
val = getenv(v.substring(0, i));
if (val == null || val.length() == 0) {
val = v.substring(i + 2);
}
break;
} else if (c == '-') {
val = getenv(v.substring(0, i));
if (val == null) {
val = v.substring(i + 1);
}
break;
}
}
if (i == v.length()) {
val = getenv(v);
}
} else {
val = getProperty(var);
}
} catch(SecurityException se) {
LOG.warn("Unexpected SecurityException in Configuration", se);
}
if (val == null) {
val = getRaw(var);
}
if (val == null) {
return eval; // return literal ${var}: var is unbound
}
// prevent recursive resolution
//
final int dollar = varBounds[SUB_START_IDX] - "${".length();
final int afterRightBrace = varBounds[SUB_END_IDX] + "}".length();
final String refVar = eval.substring(dollar, afterRightBrace);
if (evalSet == null) {
evalSet = new HashSet<>();
}
if (!evalSet.add(refVar)) {
return expr; // return original expression if there is a loop
}
// substitute
eval = eval.substring(0, dollar)
+ val
+ eval.substring(afterRightBrace);
}
throw new IllegalStateException("Variable substitution depth too large: "
+ MAX_SUBST + " " + expr);
}
String getenv(String name) {
return System.getenv(name);
}
String getProperty(String key) {
return System.getProperty(key);
}
/**
* Get the value of the name
property, null
if
* no such property exists. If the key is deprecated, it returns the value of
* the first key which replaces the deprecated key and is not null.
*
* Values are processed for variable expansion
* before being returned.
*
* @param name the property name, will be trimmed before get value.
* @return the value of the name
or its replacing property,
* or null if no such property exists.
*/
public String get(String name) {
return substituteVars(getRaw(name));
}
/**
* Get the value of the name
property as a trimmed String
,
* null
if no such property exists.
* If the key is deprecated, it returns the value of
* the first key which replaces the deprecated key and is not null
*
* Values are processed for variable expansion
* before being returned.
*
* Underscores can be optionally removed.
*
* @param name the property name.
* @param removeUnderscores should the underscores be removed?
*
* @return the value of the name
or its replacing property,
* or null if no such property exists.
*/
public String getTrimmed(String name, boolean removeUnderscores) {
String value = get(name);
if (null == value) {
return null;
}
value = value.trim();
if (removeUnderscores) {
value = value.replace("_", "");
}
return value;
}
/** The same as getTrimmed(name, false). */
public String getTrimmed(String name) {
return getTrimmed(name, false);
}
/**
* Get the value of the name
property, without doing
* variable expansion.If the key is
* deprecated, it returns the value of the first key which replaces
* the deprecated key and is not null.
*
* @param name the property name.
* @return the value of the name
property or
* its replacing property and null if no such property exists.
*/
public String getRaw(String name) {
return properties.get(Objects.requireNonNull(name, "name == null").trim());
}
/**
* Set the value
of the name
property. If
* name
is deprecated, it also sets the value
to
* the keys that replace the deprecated key. Name will be trimmed before put
* into configuration.
*
* @param name property name.
* @param value property value.
* @throws IllegalArgumentException when the value or name is null.
*/
public void set(String name, String value) {
final String trimmed = Objects.requireNonNull(name, "name == null").trim();
Objects.requireNonNull(value, () -> "value == null for " + trimmed);
properties.put(trimmed, value);
}
/**
* Unset a previously set property.
*/
public void unset(String name) {
properties.remove(name);
}
/**
* Sets a property if it is currently unset.
* @param name the property name
* @param value the new value
*/
public synchronized void setIfUnset(String name, String value) {
if (get(name) == null) {
set(name, value);
}
}
/**
* Get the value of the name
. If the key is deprecated,
* it returns the value of the first key which replaces the deprecated key
* and is not null.
* If no such property exists,
* then defaultValue
is returned.
*
* @param name property name, will be trimmed before get value.
* @param defaultValue default value.
* @return property value, or defaultValue
if the property
* doesn't exist.
*/
public String get(String name, String defaultValue) {
return substituteVars(properties.getOrDefault(name, defaultValue));
}
/**
* Get the value of the name
property as an int
.
*
* If no such property exists, the provided default value is returned,
* or if the specified value is not a valid int
,
* then an error is thrown.
*
* @param name property name.
* @param defaultValue default value.
* @throws NumberFormatException when the value is invalid
* @return property value as an int
,
* or defaultValue
.
*/
public Integer getInt(String name, Integer defaultValue) {
final String valueString = getTrimmed(name, true);
if (valueString == null) {
return defaultValue;
}
String hexString = getHexDigits(valueString);
if (hexString != null) {
return Integer.parseInt(hexString, 16);
}
return Integer.parseInt(valueString);
}
/**
* Set the value of the name
property to an int
.
*
* @param name property name.
* @param value int
value of the property.
*/
public void setInt(String name, int value) {
set(name, Integer.toString(value));
}
/**
* Get the value of the name
property as a long
.
* If no such property exists, the provided default value is returned,
* or if the specified value is not a valid long
,
* then an error is thrown.
*
* @param name property name.
* @param defaultValue default value.
* @throws NumberFormatException when the value is invalid
* @return property value as a long
,
* or defaultValue
.
*/
public Long getLong(String name, Long defaultValue) {
final String valueString = getTrimmed(name, true);
if (valueString == null) {
return defaultValue;
}
String hexString = getHexDigits(valueString);
if (hexString != null) {
return Long.parseLong(hexString, 16);
}
return Long.parseLong(valueString);
}
/** @return property value; if it is not set, return the default value. */
public File getFile(String name, File defaultValue) {
final String valueString = getTrimmed(name);
return valueString == null? defaultValue: new File(valueString);
}
/**
* Get the value of the name
property as a list
* of File
.
* The value of the property specifies a list of comma separated path names.
* If no such property is specified, then defaultValue
is
* returned.
*
* @param name the property name.
* @param defaultValue default value.
* @return property value as a List of File, or defaultValue
.
*/
public List getFiles(String name, List defaultValue) {
String valueString = getRaw(name);
if (null == valueString) {
return defaultValue;
}
final String[] paths = StringUtils.getTrimmedStrings(get(name));
return Arrays.stream(paths).map(File::new).collect(Collectors.toList());
}
public void setFile(String name, File value) {
try {
set(name, value.getCanonicalPath());
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to get canonical path from file " + value + " for " + name, e);
}
}
public void setFiles(String name, List value) {
String paths = value.stream().map(File::getAbsolutePath)
.collect(Collectors.joining(","));
set(name, paths);
}
/** @return property value; if it is not set, return the default value. */
public SizeInBytes getSizeInBytes(String name, SizeInBytes defaultValue) {
final String valueString = getTrimmed(name);
return valueString == null? defaultValue: SizeInBytes.valueOf(valueString);
}
private String getHexDigits(String value) {
boolean negative = false;
String str = value;
String hexString;
if (value.startsWith("-")) {
negative = true;
str = value.substring(1);
}
if (str.startsWith("0x") || str.startsWith("0X")) {
hexString = str.substring(2);
if (negative) {
hexString = "-" + hexString;
}
return hexString;
}
return null;
}
/**
* Set the value of the name
property to a long
.
*
* @param name property name.
* @param value long
value of the property.
*/
public void setLong(String name, long value) {
set(name, Long.toString(value));
}
/**
* Get the value of the name
property as a double
.
* If no such property exists, the provided default value is returned,
* or if the specified value is not a valid double
,
* then an error is thrown.
*
* @param name property name.
* @param defaultValue default value.
* @throws NumberFormatException when the value is invalid
* @return property value as a double
,
* or defaultValue
.
*/
public double getDouble(String name, double defaultValue) {
final String valueString = getTrimmed(name, true);
if (valueString == null) {
return defaultValue;
}
return Double.parseDouble(valueString);
}
/**
* Set the value of the name
property to a double
.
*
* @param name property name.
* @param value property value.
*/
public void setDouble(String name, double value) {
set(name,Double.toString(value));
}
/**
* Get the value of the name
property as a boolean
.
* If no such property is specified, or if the specified value is not a valid
* boolean
, then defaultValue
is returned.
*
* @param name property name.
* @param defaultValue default value.
* @return property value as a boolean
,
* or defaultValue
.
*/
public Boolean getBoolean(String name, Boolean defaultValue) {
String valueString = getTrimmed(name);
return StringUtils.string2boolean(valueString, defaultValue);
}
/**
* Set the value of the name
property to a boolean
.
*
* @param name property name.
* @param value boolean
value of the property.
*/
public void setBoolean(String name, boolean value) {
set(name, Boolean.toString(value));
}
/**
* Set the value of the name
property to the given type. This
* is equivalent to set(<name>, value.toString())
.
* @param name property name
* @param value new value
*/
public > void setEnum(String name, T value) {
set(name, value.toString());
}
/**
* Get the enum value mapped the given property name.
*
* @param name Property name
* @param enumClass the {@link Class} of the enum
* @param defaultValue Value returned if no mapping exists
*/
public > T getEnum(String name, Class enumClass, T defaultValue) {
final String val = getTrimmed(name);
return null == val? defaultValue : Enum.valueOf(enumClass, val);
}
/** The same as getEnum(name, defaultValue.getDeclaringClass(), defaultValue). */
public > T getEnum(String name, T defaultValue) {
Objects.requireNonNull(defaultValue, "defaultValue == null");
return getEnum(name, defaultValue.getDeclaringClass(), defaultValue);
}
/**
* Set the value of name
to the given time duration. This
* is equivalent to set(<name>, value + <time suffix>)
.
* @param name Property name
* @param value Time duration
*/
public void setTimeDuration(String name, TimeDuration value) {
set(name, value.toString());
}
/**
* Return time duration in the given time unit. Valid units are encoded in
* properties as suffixes: nanoseconds (ns), microseconds (us), milliseconds
* (ms), seconds (s), minutes (m), hours (h), and days (d).
* @param name Property name
* @param defaultValue Value returned if no mapping exists.
* @throws NumberFormatException If the property stripped of its unit is not
* a number
*/
public TimeDuration getTimeDuration(
String name, TimeDuration defaultValue, TimeUnit defaultUnit) {
final String value = getTrimmed(name);
if (null == value) {
return defaultValue;
}
try {
return TimeDuration.valueOf(value, defaultUnit);
} catch(NumberFormatException e) {
throw new IllegalArgumentException("Failed to parse "
+ name + " = " + value, e);
}
}
public BiFunction getTimeDuration(TimeUnit defaultUnit) {
return (key, defaultValue) -> getTimeDuration(key, defaultValue, defaultUnit);
}
/**
* Get the value of the name
property as a Class
.
* If no such property is specified, then defaultValue
is
* returned.
*
* @param name the class name.
* @param defaultValue default value.
* @return property value as a Class
,
* or defaultValue
.
*/
public Class> getClass(String name, Class> defaultValue) {
String valueString = getTrimmed(name);
if (valueString == null) {
return defaultValue;
}
try {
return ReflectionUtils.getClassByName(valueString);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* Get the value of the name
property as a Class
* implementing the interface specified by xface
.
*
* If no such property is specified, then defaultValue
is
* returned.
*
* An exception is thrown if the returned class does not implement the named
* interface.
*
* @param name the class name.
* @param defaultValue default value.
* @param xface the interface implemented by the named class.
* @return property value as a Class
,
* or defaultValue
.
*/
public Class extends BASE> getClass(
String name, Class extends BASE> defaultValue, Class xface) {
try {
Class> theClass = getClass(name, defaultValue);
if (theClass != null && !xface.isAssignableFrom(theClass)) {
throw new RuntimeException(theClass+" not "+xface.getName());
} else if (theClass != null) {
return theClass.asSubclass(xface);
} else {
return null;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Set the value of the name
property to the name of a
* theClass
implementing the given interface xface
.
*
* An exception is thrown if theClass
does not implement the
* interface xface
.
*
* @param name property name.
* @param theClass property value.
* @param xface the interface implemented by the named class.
*/
public void setClass(String name, Class> theClass, Class> xface) {
if (!xface.isAssignableFrom(theClass)) {
throw new RuntimeException(theClass+" not "+xface.getName());
}
set(name, theClass.getName());
}
/** @return number of keys in the properties. */
public int size() {
return properties.size();
}
/**
* Clears all keys from the configuration.
*/
public void clear() {
properties.clear();
}
/** @return the property entry set. */
Set> entrySet() {
return properties.entrySet();
}
@Override
public String toString() {
return JavaUtils.getClassSimpleName(getClass()) + ":" + size();
}
}