/
AnnotatedConfig.java
123 lines (101 loc) · 4.13 KB
/
AnnotatedConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC 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.openqa.selenium.grid.config;
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
/**
* A form of {@link Config} that is generated by looking at fields in the constructor arg that are
* annotated with {@link ConfigValue}. The class hierarchy is walked from closest to Object to the
* constructor argument's type, null values are ignored, and the order in which fields are read is
* not stable (meaning duplicate config values may give different values each time).
* <p>
* The main use of this class is to allow an object configured using (for example) jcommander to be
* used directly within the app, without requiring intermediate support classes to convert flags to
* config values.
*/
public class AnnotatedConfig implements Config {
private final Map<String, Map<String, String>> config;
public AnnotatedConfig(Object obj) {
Map<String, Map<String, String>> values = new HashMap<>();
Deque<Field> allConfigValues = findConfigFields(obj.getClass());
for (Field field : allConfigValues) {
if (Collection.class.isAssignableFrom(field.getType())) {
throw new ConfigException("Collection fields may not be used for configuration: " + field);
}
if (Map.class.isAssignableFrom(field.getType())) {
throw new ConfigException("Map fields may not be used for configuration: " + field);
}
field.setAccessible(true);
Object value;
try {
value = field.get(obj);
} catch (IllegalAccessException e) {
throw new ConfigException("Unable to read field: " + field);
}
if (value == null) {
continue;
}
ConfigValue annotation = field.getAnnotation(ConfigValue.class);
Map<String, String> section = values.getOrDefault(annotation.section(), new HashMap<>());
section.put(annotation.name(), String.valueOf(value));
values.put(annotation.section(), section);
}
config = ImmutableMap.copyOf(values);
}
private Deque<Field> findConfigFields(Class<?> clazz) {
Deque<Field> toSet = new ArrayDeque<>();
Set<Class<?>> toVisit = new HashSet<>();
toVisit.add(clazz);
Set<Class<?>> seen = new HashSet<>();
while (!toVisit.isEmpty()) {
clazz = toVisit.iterator().next();
toVisit.remove(clazz);
seen.add(clazz);
Arrays.stream(clazz.getDeclaredFields())
.filter(field -> field.getAnnotation(ConfigValue.class) != null)
.forEach(toSet::addFirst);
Class<?> toAdd = clazz.getSuperclass();
if (!Object.class.equals(toAdd) && !seen.contains(toAdd)) {
toVisit.add(toAdd);
}
Arrays.stream(clazz.getInterfaces())
.filter(face -> !seen.contains(face))
.forEach(toVisit::add);
}
return toSet;
}
@Override
public Optional<String> get(String section, String option) {
Objects.requireNonNull(section, "Section name not set");
Objects.requireNonNull(option, "Option name not set");
Map<String, String> sec = config.get(section);
if (sec == null) {
return Optional.empty();
}
return Optional.ofNullable(sec.get(option));
}
}