blob: 22c3d9946e1c69ef9711c0d74c1e6dbd7398d2ce [file] [log] [blame]
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.config;
import static java.util.Objects.requireNonNull;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.UsedAt.Project;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
public class ConfigUtil {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@SuppressWarnings("unchecked")
private static <T> T[] allValuesOf(T defaultValue) {
try {
return (T[]) defaultValue.getClass().getMethod("values").invoke(null);
} catch (IllegalArgumentException
| NoSuchMethodException
| InvocationTargetException
| IllegalAccessException
| SecurityException e) {
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
}
}
/**
* Parse a Java enumeration from the configuration.
*
* @param <T> type of the enumeration object.
* @param section section the key is in.
* @param subsection subsection the key is in, or null if not in a subsection.
* @param setting name of the setting to read.
* @param valueString string value from git Config
* @param all all possible values in the enumeration which should be recognized. This should be
* {@code EnumType.values()}.
* @return the selected enumeration value, or {@code defaultValue}.
*/
private static <T extends Enum<?>> T getEnum(
final String section,
final String subsection,
final String setting,
String valueString,
final T[] all) {
String n = valueString.replace(' ', '_').replace('-', '_');
for (T e : all) {
if (e.name().equalsIgnoreCase(n)) {
return e;
}
}
final StringBuilder r = new StringBuilder();
r.append("Value \"");
r.append(valueString);
r.append("\" not recognized in ");
r.append(section);
if (subsection != null) {
r.append(".");
r.append(subsection);
}
r.append(".");
r.append(setting);
r.append("; supported values are: ");
for (T e : all) {
r.append(e.name());
r.append(" ");
}
throw new IllegalArgumentException(r.toString().trim());
}
/**
* Parse a Java enumeration list from the configuration.
*
* @param <T> type of the enumeration object.
* @param config the configuration file to read.
* @param section section the key is in.
* @param subsection subsection the key is in, or null if not in a subsection.
* @param setting name of the setting to read.
* @param defaultValue default value to return if the setting was not set. Must not be null as the
* enumeration values are derived from this.
* @return the selected enumeration values list, or {@code defaultValue}.
*/
public static <T extends Enum<?>> List<T> getEnumList(
final Config config,
final String section,
final String subsection,
final String setting,
final T defaultValue) {
final T[] all = allValuesOf(defaultValue);
return getEnumList(config, section, subsection, setting, all, defaultValue);
}
/**
* Parse a Java enumeration list from the configuration.
*
* @param <T> type of the enumeration object.
* @param config the configuration file to read.
* @param section section the key is in.
* @param subsection subsection the key is in, or null if not in a subsection.
* @param setting name of the setting to read.
* @param all all possible values in the enumeration which should be recognized. This should be
* {@code EnumType.values()}.
* @param defaultValue default value to return if the setting was not set. This value may be null.
* @return the selected enumeration values list, or {@code defaultValue}.
*/
public static <T extends Enum<?>> List<T> getEnumList(
final Config config,
final String section,
final String subsection,
final String setting,
final T[] all,
final T defaultValue) {
final List<T> list = new ArrayList<>();
final String[] values = config.getStringList(section, subsection, setting);
if (values.length == 0) {
list.add(defaultValue);
} else {
for (String string : values) {
if (string != null) {
try {
list.add(getEnum(section, subsection, setting, string, all));
} catch (IllegalArgumentException ex) {
// It's better to ignore a wrongly configured enum, rather than fail to load Gerrit.
logger.atWarning().log("%s", ex.getMessage());
}
}
}
}
return list;
}
/**
* Parse a numerical time unit, such as "1 minute", from the configuration.
*
* @param config the configuration file to read.
* @param section section the key is in.
* @param subsection subsection the key is in, or null if not in a subsection.
* @param setting name of the setting to read.
* @param defaultValue default value to return if no value was set in the configuration file.
* @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
* assume if the value does not contain an indication of the units.
* @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
*/
public static long getTimeUnit(
final Config config,
final String section,
final String subsection,
final String setting,
final long defaultValue,
final TimeUnit wantUnit) {
final String valueString = config.getString(section, subsection, setting);
if (valueString == null) {
return defaultValue;
}
String s = valueString.trim();
if (s.length() == 0) {
return defaultValue;
}
if (s.startsWith("-") /* negative */) {
throw notTimeUnit(section, subsection, setting, valueString);
}
try {
return getTimeUnit(s, defaultValue, wantUnit);
} catch (IllegalArgumentException notTime) {
throw notTimeUnit(section, subsection, setting, valueString, notTime);
}
}
/**
* Parse a numerical time unit, such as "1 minute", from a string.
*
* @param valueString the string to parse.
* @param defaultValue default value to return if no value was set in the configuration file.
* @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
* assume if the value does not contain an indication of the units.
* @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
*/
public static long getTimeUnit(String valueString, long defaultValue, TimeUnit wantUnit) {
Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
if (!m.matches()) {
return defaultValue;
}
String digits = m.group(1);
String unitName = m.group(2).trim();
TimeUnit inputUnit;
int inputMul;
if ("".equals(unitName)) {
inputUnit = wantUnit;
inputMul = 1;
} else if (match(unitName, "ms", "milliseconds")) {
inputUnit = TimeUnit.MILLISECONDS;
inputMul = 1;
} else if (match(unitName, "s", "sec", "second", "seconds")) {
inputUnit = TimeUnit.SECONDS;
inputMul = 1;
} else if (match(unitName, "m", "min", "minute", "minutes")) {
inputUnit = TimeUnit.MINUTES;
inputMul = 1;
} else if (match(unitName, "h", "hr", "hour", "hours")) {
inputUnit = TimeUnit.HOURS;
inputMul = 1;
} else if (match(unitName, "d", "day", "days")) {
inputUnit = TimeUnit.DAYS;
inputMul = 1;
} else if (match(unitName, "w", "week", "weeks")) {
inputUnit = TimeUnit.DAYS;
inputMul = 7;
} else if (match(unitName, "mon", "month", "months")) {
inputUnit = TimeUnit.DAYS;
inputMul = 30;
} else if (match(unitName, "y", "year", "years")) {
inputUnit = TimeUnit.DAYS;
inputMul = 365;
} else {
throw notTimeUnit(valueString);
}
try {
return wantUnit.convert(Long.parseLong(digits) * inputMul, inputUnit);
} catch (NumberFormatException nfe) {
throw notTimeUnit(valueString, nfe);
}
}
public static String getRequired(Config cfg, String section, String name) {
final String v = cfg.getString(section, null, name);
if (v == null || "".equals(v)) {
throw new IllegalArgumentException("No " + section + "." + name + " configured");
}
return v;
}
/**
* Store section by inspecting Java class attributes.
*
* <p>Optimize the storage by unsetting a variable if it is being set to default value by the
* server.
*
* <p>Fields marked with final or transient modifiers are skipped.
*
* @param cfg config in which the values should be stored
* @param section section
* @param sub subsection
* @param s instance of class with config values
* @param defaults instance of class with default values
*/
public static <T> void storeSection(Config cfg, String section, String sub, T s, T defaults)
throws ConfigInvalidException {
try {
for (Field f : s.getClass().getDeclaredFields()) {
if (skipField(f)) {
continue;
}
Class<?> t = f.getType();
String n = f.getName();
f.setAccessible(true);
Object c = f.get(s);
Object d = f.get(defaults);
if (!isString(t) && !isCollectionOrMap(t)) {
requireNonNull(d, "Default cannot be null for: " + n);
}
if (c == null || c.equals(d)) {
cfg.unset(section, sub, n);
} else {
if (isString(t)) {
cfg.setString(section, sub, n, (String) c);
} else if (isInteger(t)) {
cfg.setInt(section, sub, n, (Integer) c);
} else if (isLong(t)) {
cfg.setLong(section, sub, n, (Long) c);
} else if (isBoolean(t)) {
cfg.setBoolean(section, sub, n, (Boolean) c);
} else if (t.isEnum()) {
cfg.setEnum(section, sub, n, (Enum<?>) c);
} else if (isCollectionOrMap(t)) {
// TODO(davido): accept closure passed in from caller
continue;
} else {
throw new ConfigInvalidException("type is unknown: " + t.getName());
}
}
}
} catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
throw new ConfigInvalidException("cannot save values", e);
}
}
/**
* Load section by inspecting Java class attributes.
*
* <p>Config values are stored optimized: no default values are stored. The loading is performed
* eagerly: all values are set.
*
* <p>Fields marked with final or transient modifiers are skipped.
*
* @param cfg config from which the values are loaded
* @param section section
* @param sub subsection
* @param s instance of class in which the values are set
* @param defaults instance of class with default values
* @param i instance to merge during the load. When present, the boolean fields are not nullified
* when their values are false
* @return loaded instance
*/
public static <T> T loadSection(Config cfg, String section, String sub, T s, T defaults, T i)
throws ConfigInvalidException {
try {
for (Field f : s.getClass().getDeclaredFields()) {
if (skipField(f)) {
continue;
}
Class<?> t = f.getType();
String n = f.getName();
f.setAccessible(true);
Object d = f.get(defaults);
if (!isString(t) && !isCollectionOrMap(t)) {
requireNonNull(d, "Default cannot be null for: " + n);
}
if (isString(t)) {
String v = cfg.getString(section, sub, n);
if (v == null) {
v = (String) d;
}
f.set(s, v);
} else if (isInteger(t)) {
f.set(s, cfg.getInt(section, sub, n, (Integer) d));
} else if (isLong(t)) {
f.set(s, cfg.getLong(section, sub, n, (Long) d));
} else if (isBoolean(t)) {
boolean b = cfg.getBoolean(section, sub, n, (Boolean) d);
if (b || i != null) {
f.set(s, b);
}
} else if (t.isEnum()) {
f.set(s, cfg.getEnum(section, sub, n, (Enum<?>) d));
} else if (isCollectionOrMap(t)) {
// TODO(davido): accept closure passed in from caller
continue;
} else {
throw new ConfigInvalidException("type is unknown: " + t.getName());
}
if (i != null) {
Object o = f.get(i);
if (o != null) {
f.set(s, o);
}
}
}
} catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
throw new ConfigInvalidException("cannot load values", e);
}
return s;
}
/**
* Update user config by applying the specified delta
*
* <p>As opposed to {@link com.google.gerrit.server.config.ConfigUtil#storeSection}, this method
* does not unset a variable that are set to default, because it is expected that the input {@code
* original} is the raw user config value (does not include the defaults)
*
* <p>To use this method with the proto config (see {@link
* CachedPreferences#asUserPreferencesProto()}), the caller can first convert the proto to a java
* class usign one of the {@link UserPreferencesConverter} classes.
*
* <p>Fields marked with final or transient modifiers are skipped.
*
* @param original the original current user config
* @param updateDelta instance of class with config values that need to be uplied to the original
* config
*/
@UsedAt(Project.GOOGLE)
public static <T> void updatePreferences(T original, T updateDelta) throws IOException {
try {
for (Field f : updateDelta.getClass().getDeclaredFields()) {
if (skipField(f)) {
continue;
}
f.setAccessible(true);
Object c = f.get(updateDelta);
if (c != null) {
f.set(original, c);
}
}
} catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
throw new IOException("cannot apply delta the original config", e);
}
}
public static boolean skipField(Field field) {
int modifiers = field.getModifiers();
return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
}
private static boolean isCollectionOrMap(Class<?> t) {
return Collection.class.isAssignableFrom(t) || Map.class.isAssignableFrom(t);
}
private static boolean isString(Class<?> t) {
return String.class == t;
}
private static boolean isBoolean(Class<?> t) {
return Boolean.class == t || boolean.class == t;
}
private static boolean isLong(Class<?> t) {
return Long.class == t || long.class == t;
}
private static boolean isInteger(Class<?> t) {
return Integer.class == t || int.class == t;
}
private static boolean match(String a, String... cases) {
for (String b : cases) {
if (b != null && b.equalsIgnoreCase(a)) {
return true;
}
}
return false;
}
private static IllegalArgumentException notTimeUnit(
String section, String subsection, String setting, String valueString, Throwable why) {
return notTimeUnit(
section
+ (subsection != null ? "." + subsection : "")
+ "."
+ setting
+ " = "
+ valueString,
why);
}
private static IllegalArgumentException notTimeUnit(
String section, String subsection, String setting, String valueString) {
return notTimeUnit(
section
+ (subsection != null ? "." + subsection : "")
+ "."
+ setting
+ " = "
+ valueString);
}
private static IllegalArgumentException notTimeUnit(String val) {
return new IllegalArgumentException("Invalid time unit value: " + val);
}
private static IllegalArgumentException notTimeUnit(String val, Throwable why) {
return new IllegalArgumentException("Invalid time unit value: " + val, why);
}
private ConfigUtil() {}
}