1
2
3
4
5
6
7
8
9
10
11
12
13
14 package com.addthis.codec.plugins;
15
16 import javax.annotation.Nonnull;
17 import javax.annotation.Nullable;
18
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Set;
24
25 import com.google.common.annotations.Beta;
26 import com.google.common.base.Objects;
27 import com.google.common.collect.BiMap;
28 import com.google.common.collect.HashBiMap;
29 import com.google.common.collect.ImmutableBiMap;
30 import com.google.common.collect.Maps;
31
32 import com.typesafe.config.Config;
33 import com.typesafe.config.ConfigException;
34 import com.typesafe.config.ConfigFactory;
35 import com.typesafe.config.ConfigObject;
36 import com.typesafe.config.ConfigValue;
37 import com.typesafe.config.ConfigValueType;
38
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 import static com.google.common.base.Preconditions.checkNotNull;
43
44 @Beta
45 public class PluginMap {
46
47 private static final Logger log = LoggerFactory.getLogger(PluginMap.class);
48
49 public static final PluginMap EMPTY = new PluginMap();
50
51 @Nonnull private final Config config;
52 @Nonnull private final BiMap<String, Class<?>> map;
53 @Nonnull private final Map<String, String> aliases;
54 @Nonnull private final Set<String> inlinedAliases;
55
56 @Nonnull private final String category;
57 @Nonnull private final String classField;
58
59 @Nullable private final Class<?> baseClass;
60
61 public PluginMap(@Nonnull String category, @Nonnull Config config) {
62 this.config = config;
63 this.category = checkNotNull(category);
64 classField = config.getString("_field");
65 boolean errorMissing = config.getBoolean("_strict");
66 if (config.hasPath("_class")) {
67 String baseClassName = config.getString("_class");
68 try {
69 baseClass = Class.forName(baseClassName);
70 } catch (ClassNotFoundException e) {
71 log.error("could not find specified base class {} for category {}",
72 baseClassName, category);
73 throw new RuntimeException(e);
74 }
75 } else {
76 baseClass = null;
77 }
78 Set<String> labels = config.root().keySet();
79 BiMap<String, Class<?>> mutableMap = HashBiMap.create(labels.size());
80 Map<String, String> mutableAliasMap = new HashMap<>();
81 Set<String> mutableInlinedAliasSet = new HashSet<>();
82 for (String label : labels) {
83 if (!((label.charAt(0) != '_') || "_array".equals(label) || "_default".equals(label))) {
84 continue;
85 }
86 ConfigValue configValue = config.root().get(label);
87 String className;
88 if (configValue.valueType() == ConfigValueType.STRING) {
89 className = (String) configValue.unwrapped();
90 } else if (configValue.valueType() == ConfigValueType.OBJECT) {
91 ConfigObject configObject = (ConfigObject) configValue;
92 className = configObject.toConfig().getString("_class");
93 if (configObject.toConfig().hasPath("_inline") &&
94 configObject.toConfig().getBoolean("_inline")) {
95 mutableInlinedAliasSet.add(label);
96 }
97 } else {
98 throw new ConfigException.WrongType(configValue.origin(), label,
99 "STRING OR OBJECT", configValue.valueType().toString());
100 }
101 if (labels.contains(className)) {
102
103 mutableAliasMap.put(label, className);
104 } else {
105 try {
106 Class<?> foundClass = findAndValidateClass(className);
107 mutableMap.put(label, foundClass);
108 } catch (ClassNotFoundException maybeSwallowed) {
109 if (errorMissing) {
110 throw new RuntimeException(maybeSwallowed);
111 } else {
112 log.warn("plugin category {} with alias {} is pointing to missing class {}",
113 category, label, className);
114 }
115 }
116 }
117 }
118 map = Maps.unmodifiableBiMap(mutableMap);
119 aliases = Collections.unmodifiableMap(mutableAliasMap);
120 checkAliasesForCycles();
121 inlinedAliases = Collections.unmodifiableSet(mutableInlinedAliasSet);
122 }
123
124 private PluginMap() {
125 config = ConfigFactory.empty();
126 map = ImmutableBiMap.of();
127 aliases = Collections.emptyMap();
128 inlinedAliases = Collections.emptySet();
129 classField = "class";
130 category = "unknown";
131 baseClass = null;
132 }
133
134 @Nonnull public BiMap<String, Class<?>> asBiMap() {
135 return map;
136 }
137
138 @Nonnull public Config config() {
139 return config;
140 }
141
142 @Nonnull public ConfigObject aliasDefaults(String alias) {
143 ConfigValue configValue = config.root().get(alias);
144 ConfigObject defaults;
145 if ((configValue != null) && (configValue.valueType() == ConfigValueType.OBJECT)) {
146 defaults = (ConfigObject) configValue;
147 } else {
148 defaults = ConfigFactory.empty().root();
149 }
150 String aliasTarget = aliases.get(alias);
151 if (aliasTarget != null) {
152 defaults = defaults.withFallback(aliasDefaults(aliasTarget));
153 }
154 return defaults;
155 }
156
157 @Nonnull public String classField() {
158 return classField;
159 }
160
161 @Nonnull public String category() {
162 return category;
163 }
164
165 @Nonnull public Set<String> inlinedAliases() {
166 return inlinedAliases;
167 }
168
169 @Nullable public Class<?> arraySugar() {
170 return getClassIfConfigured("_array");
171 }
172
173 @Nullable public Class<?> defaultSugar() {
174 return getClassIfConfigured("_default");
175 }
176
177
178 @Nullable public Class<?> baseClass() {
179 return baseClass;
180 }
181
182
183 @Nonnull public String getClassName(Class<?> type) {
184 String alt = map.inverse().get(type);
185 if (alt != null) {
186 return alt;
187 } else {
188 return type.getName();
189 }
190 }
191
192
193 @Nullable public String getLastAlias(String alias) {
194 String aliasTarget = aliases.get(alias);
195 if (aliasTarget != null) {
196 return getLastAlias(aliasTarget);
197 } else {
198 if (asBiMap().containsKey(alias)) {
199 return alias;
200 } else {
201 return null;
202 }
203 }
204 }
205
206
207
208
209
210
211
212 @Nonnull public Class<?> getClass(String type) throws ClassNotFoundException {
213 Class<?> alt = map.get(type);
214 if (alt != null) {
215 return alt;
216 } else {
217 String aliasTarget = aliases.get(type);
218 if (aliasTarget != null) {
219 return getClass(aliasTarget);
220 }
221 }
222 return findAndValidateClass(type);
223 }
224
225
226 @Nullable public Class<?> getClassIfConfigured(String type) {
227 Class<?> alt = map.get(type);
228 if (alt != null) {
229 return alt;
230 } else {
231 String aliasTarget = aliases.get(type);
232 if (aliasTarget != null) {
233 return getClassIfConfigured(aliasTarget);
234 }
235 }
236 return null;
237 }
238
239 @Override public String toString() {
240 return Objects.toStringHelper(this)
241 .add("category", category)
242 .add("baseClass", baseClass)
243 .add("classField", classField)
244 .add("map", map)
245 .add("aliases", aliases)
246 .add("inlined-aliases", inlinedAliases)
247 .toString();
248 }
249
250 @Nonnull private Class<?> findAndValidateClass(String className) throws ClassNotFoundException {
251 Class<?> classValue = null;
252
253 if (baseClass != null) {
254 @Nullable String packageName = baseClass.getPackage().getName();
255 while ((packageName != null) && (classValue == null)) {
256 String packageSugaredName = packageName + '.' + className;
257 try {
258 classValue = Class.forName(packageSugaredName);
259 } catch (ClassNotFoundException ignored) {
260 int lastDotIndex = packageName.lastIndexOf('.');
261 if (lastDotIndex >= 0) {
262 packageName = packageName.substring(0, lastDotIndex);
263 } else {
264 packageName = null;
265 }
266 }
267 }
268 }
269 if (classValue == null) {
270 classValue = Class.forName(className);
271 }
272
273 if ((baseClass != null) && !baseClass.isAssignableFrom(classValue)) {
274 throw new ClassCastException(String.format(
275 "plugin %s specified a base class %s and '%s: %s', is not a valid subtype",
276 category, baseClass.getName(), classField, classValue.getName()));
277 }
278 return classValue;
279 }
280
281 private void checkAliasesForCycles() {
282 for (String key : aliases.keySet()) {
283 Set<String> visited = new HashSet<>(aliases.size());
284 checkAliasesForCyclesHelper(key, visited);
285 }
286 }
287
288 private void checkAliasesForCyclesHelper(String key, Set<String> visited) {
289 visited.add(key);
290 String nextKey = aliases.get(key);
291 if (nextKey == null) {
292
293 return;
294 }
295 if (visited.contains(nextKey)) {
296 throw new ConfigException.BadValue(config.root().get(key).origin(), key, "cyclical aliases detected");
297 } else {
298 checkAliasesForCyclesHelper(nextKey, visited);
299 }
300 }
301 }