|  | // 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 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(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; | 
|  | } | 
|  |  | 
|  | 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() {} | 
|  | } |