Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.config;
16 :
17 : import static java.util.Objects.requireNonNull;
18 :
19 : import com.google.common.flogger.FluentLogger;
20 : import java.lang.reflect.Field;
21 : import java.lang.reflect.InvocationTargetException;
22 : import java.lang.reflect.Modifier;
23 : import java.util.ArrayList;
24 : import java.util.Collection;
25 : import java.util.List;
26 : import java.util.Map;
27 : import java.util.concurrent.TimeUnit;
28 : import java.util.regex.Matcher;
29 : import java.util.regex.Pattern;
30 : import org.eclipse.jgit.errors.ConfigInvalidException;
31 : import org.eclipse.jgit.lib.Config;
32 :
33 : public class ConfigUtil {
34 153 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
35 :
36 : @SuppressWarnings("unchecked")
37 : private static <T> T[] allValuesOf(T defaultValue) {
38 : try {
39 5 : return (T[]) defaultValue.getClass().getMethod("values").invoke(null);
40 0 : } catch (IllegalArgumentException
41 : | NoSuchMethodException
42 : | InvocationTargetException
43 : | IllegalAccessException
44 : | SecurityException e) {
45 0 : throw new IllegalArgumentException("Cannot obtain enumeration values", e);
46 : }
47 : }
48 :
49 : /**
50 : * Parse a Java enumeration from the configuration.
51 : *
52 : * @param <T> type of the enumeration object.
53 : * @param section section the key is in.
54 : * @param subsection subsection the key is in, or null if not in a subsection.
55 : * @param setting name of the setting to read.
56 : * @param valueString string value from git Config
57 : * @param all all possible values in the enumeration which should be recognized. This should be
58 : * {@code EnumType.values()}.
59 : * @return the selected enumeration value, or {@code defaultValue}.
60 : */
61 : private static <T extends Enum<?>> T getEnum(
62 : final String section,
63 : final String subsection,
64 : final String setting,
65 : String valueString,
66 : final T[] all) {
67 :
68 3 : String n = valueString.replace(' ', '_').replace('-', '_');
69 3 : for (T e : all) {
70 3 : if (e.name().equalsIgnoreCase(n)) {
71 3 : return e;
72 : }
73 : }
74 :
75 1 : final StringBuilder r = new StringBuilder();
76 1 : r.append("Value \"");
77 1 : r.append(valueString);
78 1 : r.append("\" not recognized in ");
79 1 : r.append(section);
80 1 : if (subsection != null) {
81 0 : r.append(".");
82 0 : r.append(subsection);
83 : }
84 1 : r.append(".");
85 1 : r.append(setting);
86 1 : r.append("; supported values are: ");
87 1 : for (T e : all) {
88 1 : r.append(e.name());
89 1 : r.append(" ");
90 : }
91 :
92 1 : throw new IllegalArgumentException(r.toString().trim());
93 : }
94 :
95 : /**
96 : * Parse a Java enumeration list from the configuration.
97 : *
98 : * @param <T> type of the enumeration object.
99 : * @param config the configuration file to read.
100 : * @param section section the key is in.
101 : * @param subsection subsection the key is in, or null if not in a subsection.
102 : * @param setting name of the setting to read.
103 : * @param defaultValue default value to return if the setting was not set. Must not be null as the
104 : * enumeration values are derived from this.
105 : * @return the selected enumeration values list, or {@code defaultValue}.
106 : */
107 : public static <T extends Enum<?>> List<T> getEnumList(
108 : final Config config,
109 : final String section,
110 : final String subsection,
111 : final String setting,
112 : final T defaultValue) {
113 5 : final T[] all = allValuesOf(defaultValue);
114 5 : return getEnumList(config, section, subsection, setting, all, defaultValue);
115 : }
116 :
117 : /**
118 : * Parse a Java enumeration list from the configuration.
119 : *
120 : * @param <T> type of the enumeration object.
121 : * @param config the configuration file to read.
122 : * @param section section the key is in.
123 : * @param subsection subsection the key is in, or null if not in a subsection.
124 : * @param setting name of the setting to read.
125 : * @param all all possible values in the enumeration which should be recognized. This should be
126 : * {@code EnumType.values()}.
127 : * @param defaultValue default value to return if the setting was not set. This value may be null.
128 : * @return the selected enumeration values list, or {@code defaultValue}.
129 : */
130 : public static <T extends Enum<?>> List<T> getEnumList(
131 : final Config config,
132 : final String section,
133 : final String subsection,
134 : final String setting,
135 : final T[] all,
136 : final T defaultValue) {
137 149 : final List<T> list = new ArrayList<>();
138 149 : final String[] values = config.getStringList(section, subsection, setting);
139 149 : if (values.length == 0) {
140 149 : list.add(defaultValue);
141 : } else {
142 3 : for (String string : values) {
143 3 : if (string != null) {
144 : try {
145 3 : list.add(getEnum(section, subsection, setting, string, all));
146 1 : } catch (IllegalArgumentException ex) {
147 : // It's better to ignore a wrongly configured enum, rather than fail to load Gerrit.
148 1 : logger.atWarning().log("%s", ex.getMessage());
149 3 : }
150 : }
151 : }
152 : }
153 149 : return list;
154 : }
155 :
156 : /**
157 : * Parse a numerical time unit, such as "1 minute", from the configuration.
158 : *
159 : * @param config the configuration file to read.
160 : * @param section section the key is in.
161 : * @param subsection subsection the key is in, or null if not in a subsection.
162 : * @param setting name of the setting to read.
163 : * @param defaultValue default value to return if no value was set in the configuration file.
164 : * @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
165 : * assume if the value does not contain an indication of the units.
166 : * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
167 : */
168 : public static long getTimeUnit(
169 : final Config config,
170 : final String section,
171 : final String subsection,
172 : final String setting,
173 : final long defaultValue,
174 : final TimeUnit wantUnit) {
175 153 : final String valueString = config.getString(section, subsection, setting);
176 153 : if (valueString == null) {
177 152 : return defaultValue;
178 : }
179 :
180 140 : String s = valueString.trim();
181 140 : if (s.length() == 0) {
182 0 : return defaultValue;
183 : }
184 :
185 140 : if (s.startsWith("-") /* negative */) {
186 2 : throw notTimeUnit(section, subsection, setting, valueString);
187 : }
188 :
189 : try {
190 140 : return getTimeUnit(s, defaultValue, wantUnit);
191 1 : } catch (IllegalArgumentException notTime) {
192 1 : throw notTimeUnit(section, subsection, setting, valueString, notTime);
193 : }
194 : }
195 :
196 : /**
197 : * Parse a numerical time unit, such as "1 minute", from a string.
198 : *
199 : * @param valueString the string to parse.
200 : * @param defaultValue default value to return if no value was set in the configuration file.
201 : * @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
202 : * assume if the value does not contain an indication of the units.
203 : * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
204 : */
205 : public static long getTimeUnit(String valueString, long defaultValue, TimeUnit wantUnit) {
206 144 : Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
207 144 : if (!m.matches()) {
208 3 : return defaultValue;
209 : }
210 :
211 144 : String digits = m.group(1);
212 144 : String unitName = m.group(2).trim();
213 :
214 : TimeUnit inputUnit;
215 : int inputMul;
216 :
217 144 : if ("".equals(unitName)) {
218 139 : inputUnit = wantUnit;
219 139 : inputMul = 1;
220 :
221 11 : } else if (match(unitName, "ms", "milliseconds")) {
222 3 : inputUnit = TimeUnit.MILLISECONDS;
223 3 : inputMul = 1;
224 :
225 11 : } else if (match(unitName, "s", "sec", "second", "seconds")) {
226 3 : inputUnit = TimeUnit.SECONDS;
227 3 : inputMul = 1;
228 :
229 10 : } else if (match(unitName, "m", "min", "minute", "minutes")) {
230 10 : inputUnit = TimeUnit.MINUTES;
231 10 : inputMul = 1;
232 :
233 8 : } else if (match(unitName, "h", "hr", "hour", "hours")) {
234 1 : inputUnit = TimeUnit.HOURS;
235 1 : inputMul = 1;
236 :
237 8 : } else if (match(unitName, "d", "day", "days")) {
238 5 : inputUnit = TimeUnit.DAYS;
239 5 : inputMul = 1;
240 :
241 8 : } else if (match(unitName, "w", "week", "weeks")) {
242 6 : inputUnit = TimeUnit.DAYS;
243 6 : inputMul = 7;
244 :
245 3 : } else if (match(unitName, "mon", "month", "months")) {
246 1 : inputUnit = TimeUnit.DAYS;
247 1 : inputMul = 30;
248 :
249 3 : } else if (match(unitName, "y", "year", "years")) {
250 1 : inputUnit = TimeUnit.DAYS;
251 1 : inputMul = 365;
252 :
253 : } else {
254 3 : throw notTimeUnit(valueString);
255 : }
256 :
257 : try {
258 144 : return wantUnit.convert(Long.parseLong(digits) * inputMul, inputUnit);
259 0 : } catch (NumberFormatException nfe) {
260 0 : throw notTimeUnit(valueString, nfe);
261 : }
262 : }
263 :
264 : public static String getRequired(Config cfg, String section, String name) {
265 0 : final String v = cfg.getString(section, null, name);
266 0 : if (v == null || "".equals(v)) {
267 0 : throw new IllegalArgumentException("No " + section + "." + name + " configured");
268 : }
269 0 : return v;
270 : }
271 :
272 : /**
273 : * Store section by inspecting Java class attributes.
274 : *
275 : * <p>Optimize the storage by unsetting a variable if it is being set to default value by the
276 : * server.
277 : *
278 : * <p>Fields marked with final or transient modifiers are skipped.
279 : *
280 : * @param cfg config in which the values should be stored
281 : * @param section section
282 : * @param sub subsection
283 : * @param s instance of class with config values
284 : * @param defaults instance of class with default values
285 : */
286 : public static <T> void storeSection(Config cfg, String section, String sub, T s, T defaults)
287 : throws ConfigInvalidException {
288 : try {
289 10 : for (Field f : s.getClass().getDeclaredFields()) {
290 10 : if (skipField(f)) {
291 10 : continue;
292 : }
293 10 : Class<?> t = f.getType();
294 10 : String n = f.getName();
295 10 : f.setAccessible(true);
296 10 : Object c = f.get(s);
297 10 : Object d = f.get(defaults);
298 10 : if (!isString(t) && !isCollectionOrMap(t)) {
299 10 : requireNonNull(d, "Default cannot be null for: " + n);
300 : }
301 10 : if (c == null || c.equals(d)) {
302 10 : cfg.unset(section, sub, n);
303 : } else {
304 10 : if (isString(t)) {
305 2 : cfg.setString(section, sub, n, (String) c);
306 10 : } else if (isInteger(t)) {
307 3 : cfg.setInt(section, sub, n, (Integer) c);
308 10 : } else if (isLong(t)) {
309 1 : cfg.setLong(section, sub, n, (Long) c);
310 10 : } else if (isBoolean(t)) {
311 8 : cfg.setBoolean(section, sub, n, (Boolean) c);
312 8 : } else if (t.isEnum()) {
313 5 : cfg.setEnum(section, sub, n, (Enum<?>) c);
314 6 : } else if (isCollectionOrMap(t)) {
315 : // TODO(davido): accept closure passed in from caller
316 6 : continue;
317 : } else {
318 0 : throw new ConfigInvalidException("type is unknown: " + t.getName());
319 : }
320 : }
321 : }
322 0 : } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
323 0 : throw new ConfigInvalidException("cannot save values", e);
324 10 : }
325 10 : }
326 :
327 : /**
328 : * Load section by inspecting Java class attributes.
329 : *
330 : * <p>Config values are stored optimized: no default values are stored. The loading is performed
331 : * eagerly: all values are set.
332 : *
333 : * <p>Fields marked with final or transient modifiers are skipped.
334 : *
335 : * @param cfg config from which the values are loaded
336 : * @param section section
337 : * @param sub subsection
338 : * @param s instance of class in which the values are set
339 : * @param defaults instance of class with default values
340 : * @param i instance to merge during the load. When present, the boolean fields are not nullified
341 : * when their values are false
342 : * @return loaded instance
343 : */
344 : public static <T> T loadSection(Config cfg, String section, String sub, T s, T defaults, T i)
345 : throws ConfigInvalidException {
346 : try {
347 151 : for (Field f : s.getClass().getDeclaredFields()) {
348 151 : if (skipField(f)) {
349 151 : continue;
350 : }
351 151 : Class<?> t = f.getType();
352 151 : String n = f.getName();
353 151 : f.setAccessible(true);
354 151 : Object d = f.get(defaults);
355 151 : if (!isString(t) && !isCollectionOrMap(t)) {
356 151 : requireNonNull(d, "Default cannot be null for: " + n);
357 : }
358 151 : if (isString(t)) {
359 151 : String v = cfg.getString(section, sub, n);
360 151 : if (v == null) {
361 151 : v = (String) d;
362 : }
363 151 : f.set(s, v);
364 151 : } else if (isInteger(t)) {
365 151 : f.set(s, cfg.getInt(section, sub, n, (Integer) d));
366 151 : } else if (isLong(t)) {
367 1 : f.set(s, cfg.getLong(section, sub, n, (Long) d));
368 151 : } else if (isBoolean(t)) {
369 151 : boolean b = cfg.getBoolean(section, sub, n, (Boolean) d);
370 151 : if (b || i != null) {
371 151 : f.set(s, b);
372 : }
373 151 : } else if (t.isEnum()) {
374 151 : f.set(s, cfg.getEnum(section, sub, n, (Enum<?>) d));
375 151 : } else if (isCollectionOrMap(t)) {
376 : // TODO(davido): accept closure passed in from caller
377 151 : continue;
378 : } else {
379 0 : throw new ConfigInvalidException("type is unknown: " + t.getName());
380 : }
381 151 : if (i != null) {
382 9 : Object o = f.get(i);
383 9 : if (o != null) {
384 9 : f.set(s, o);
385 : }
386 : }
387 : }
388 0 : } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
389 0 : throw new ConfigInvalidException("cannot load values", e);
390 151 : }
391 151 : return s;
392 : }
393 :
394 : public static boolean skipField(Field field) {
395 151 : int modifiers = field.getModifiers();
396 151 : return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
397 : }
398 :
399 : private static boolean isCollectionOrMap(Class<?> t) {
400 151 : return Collection.class.isAssignableFrom(t) || Map.class.isAssignableFrom(t);
401 : }
402 :
403 : private static boolean isString(Class<?> t) {
404 151 : return String.class == t;
405 : }
406 :
407 : private static boolean isBoolean(Class<?> t) {
408 151 : return Boolean.class == t || boolean.class == t;
409 : }
410 :
411 : private static boolean isLong(Class<?> t) {
412 151 : return Long.class == t || long.class == t;
413 : }
414 :
415 : private static boolean isInteger(Class<?> t) {
416 151 : return Integer.class == t || int.class == t;
417 : }
418 :
419 : private static boolean match(String a, String... cases) {
420 11 : for (String b : cases) {
421 11 : if (b != null && b.equalsIgnoreCase(a)) {
422 11 : return true;
423 : }
424 : }
425 11 : return false;
426 : }
427 :
428 : private static IllegalArgumentException notTimeUnit(
429 : String section, String subsection, String setting, String valueString, Throwable why) {
430 1 : return notTimeUnit(
431 : section
432 1 : + (subsection != null ? "." + subsection : "")
433 : + "."
434 : + setting
435 : + " = "
436 : + valueString,
437 : why);
438 : }
439 :
440 : private static IllegalArgumentException notTimeUnit(
441 : String section, String subsection, String setting, String valueString) {
442 2 : return notTimeUnit(
443 : section
444 2 : + (subsection != null ? "." + subsection : "")
445 : + "."
446 : + setting
447 : + " = "
448 : + valueString);
449 : }
450 :
451 : private static IllegalArgumentException notTimeUnit(String val) {
452 4 : return new IllegalArgumentException("Invalid time unit value: " + val);
453 : }
454 :
455 : private static IllegalArgumentException notTimeUnit(String val, Throwable why) {
456 1 : return new IllegalArgumentException("Invalid time unit value: " + val, why);
457 : }
458 :
459 : private ConfigUtil() {}
460 : }
|