| // 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.errorprone.annotations.CanIgnoreReturnValue; |
| 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 |
| */ |
| @CanIgnoreReturnValue |
| 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; |
| } |
| |
| /** |
| * Merges config by inspecting Java class attributes, similar to {@link #loadSection}. |
| * |
| * <p>Config values are stored optimized: no default values are stored. The loading is performed |
| * eagerly: all values are set, except default boolean values. |
| * |
| * <p>Fields marked with final or transient modifiers are skipped. |
| * |
| * @param cfg config from which the values are loaded |
| * @param s instance of class in which the values are set |
| * @param defaults instance of class with default values |
| * @return loaded instance |
| */ |
| @CanIgnoreReturnValue |
| public static <T> T mergeWithDefaults(T cfg, 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 val = f.get(cfg); |
| if (val == null) { |
| val = f.get(defaults); |
| if (!isString(t) && !isCollectionOrMap(t)) { |
| requireNonNull(val, "Default cannot be null for: " + n); |
| } |
| } |
| if (!isBoolean(t) || (boolean) val) { |
| // To reproduce the same behavior as in the loadSection method above, values are |
| // explicitly set for all types, except the boolean type. For the boolean type, the value |
| // is set only if it is 'true' (so, the false value is omitted in the result object). |
| f.set(s, val); |
| } |
| } |
| } 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() {} |
| } |