1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.any23.configuration;
19
20 import java.lang.reflect.GenericArrayType;
21 import java.lang.reflect.ParameterizedType;
22 import java.lang.reflect.Type;
23 import java.lang.reflect.TypeVariable;
24 import java.util.HashMap;
25 import java.util.Objects;
26 import java.util.Optional;
27 import java.util.regex.Pattern;
28
29 /**
30 * Represents a setting key paired with a compatible value.
31 *
32 * @author Hans Brende (hansbrende@apache.org)
33 */
34 public abstract class Setting<V> implements Cloneable {
35
36 private final Key key;
37 private V value;
38
39 /**
40 * Constructs a new setting with the specified identifier and default value. This constructor must be called with
41 * concrete type arguments.
42 *
43 * @param identifier
44 * the identifier for this setting
45 * @param defaultValue
46 * the default value for this setting
47 *
48 * @throws IllegalArgumentException
49 * if the identifier or any of the type arguments were invalid
50 */
51 protected Setting(String identifier, V defaultValue) {
52 checkIdentifier(identifier);
53 this.key = new Key(identifier, lookupValueType(getClass(), identifier), defaultValue != null);
54 this.value = defaultValue;
55 }
56
57 /**
58 * Constructs a new setting with the specified identifier, value type, and default value.
59 *
60 * @param identifier
61 * the identifier for this setting
62 * @param valueType
63 * the value type for this setting
64 * @param defaultValue
65 * the default value for this setting
66 *
67 * @throws IllegalArgumentException
68 * if the identifier is invalid, or the value type is primitive, mutable, or has type parameters
69 */
70 protected Setting(String identifier, Class<V> valueType, V defaultValue) {
71 this(identifier, defaultValue, valueType);
72 if (valueType.isArray()) {
73 throw new IllegalArgumentException(identifier + " value class must be immutable");
74 } else if (valueType.getTypeParameters().length != 0) {
75 throw new IllegalArgumentException(
76 identifier + " setting key must fill in type parameters for " + valueType.toGenericString());
77 } else if (valueType.isPrimitive()) {
78 // ensure using primitive wrapper classes
79 // so that Class.isInstance(), etc. will work as expected
80 throw new IllegalArgumentException(identifier + " value class cannot be primitive");
81 }
82 }
83
84 private Setting(String identifier, V defaultValue, Class<V> valueType) {
85 checkIdentifier(identifier);
86 this.key = new Key(identifier, valueType, defaultValue != null);
87 this.value = defaultValue;
88 }
89
90 /**
91 * @return the identifier for this setting
92 */
93 public final String getIdentifier() {
94 return key.identifier;
95 }
96
97 /**
98 * Subclasses may override this method to check that new values for this setting are valid. The default
99 * implementation of this method throws a {@link NullPointerException} if the new value is null and the original
100 * default value for this setting was non-null.
101 *
102 * @param newValue
103 * the new value for this setting
104 *
105 * @throws Exception
106 * if the new value for this setting is invalid
107 */
108 protected void checkValue(V newValue) throws Exception {
109 if (newValue == null && key.nonnull) {
110 throw new NullPointerException();
111 }
112 }
113
114 /**
115 * @return the value for this setting
116 */
117 public final V getValue() {
118 return value;
119 }
120
121 /**
122 * @return the type of value supported for this setting
123 */
124 public final Type getValueType() {
125 return key.valueType;
126 }
127
128 /**
129 * @param setting
130 * a setting that may or may not have the same key as this setting
131 * @param <S>
132 * the type of the supplied setting
133 *
134 * @return this setting, if this setting has the same key as the supplied setting
135 */
136 @SuppressWarnings("unchecked")
137 public final <S extends Setting<?>> Optional<S> as(S setting) {
138 return key == ((Setting<?>) setting).key ? Optional.of((S) this) : Optional.empty();
139 }
140
141 /**
142 * @param newValue
143 * a value for a new setting
144 *
145 * @return a new {@link Setting} object with this setting's key and the supplied value.
146 *
147 * @throws IllegalArgumentException
148 * if the new value was invalid, as determined by:
149 *
150 * <pre>
151 * {@code this.checkValue(newValue)}
152 * </pre>
153 *
154 * @see Setting#checkValue(Object)
155 */
156 public final Setting<V> withValue(V newValue) {
157 return clone(this, newValue);
158 }
159
160 @Override
161 protected final Object clone() {
162 try {
163 // ensure no subclasses override this incorrectly
164 return super.clone();
165 } catch (CloneNotSupportedException e) {
166 throw new AssertionError(e);
167 }
168 }
169
170 /**
171 * @return true if the supplied object is an instance of {@link Setting} and has the same key and value as this
172 * setting.
173 */
174 @Override
175 public final boolean equals(Object o) {
176 if (this == o)
177 return true;
178 if (!(o instanceof Setting))
179 return false;
180
181 Setting<?> setting = (Setting<?>) o;
182 return key == setting.key && Objects.equals(value, setting.value);
183 }
184
185 @Override
186 public final int hashCode() {
187 return key.hashCode() ^ Objects.hashCode(value);
188 }
189
190 @Override
191 public String toString() {
192 return key.identifier + "=" + value;
193 }
194
195 /**
196 * Convenience method to create a new setting with the specified identifier and default value.
197 *
198 * @param identifier
199 * the identifier for this setting
200 * @param defaultValue
201 * the default value for this setting
202 *
203 * @return the new setting
204 *
205 * @throws IllegalArgumentException
206 * if the identifier is invalid
207 */
208 public static Setting<Boolean> create(String identifier, Boolean defaultValue) {
209 return new Impl<>(identifier, defaultValue, Boolean.class);
210 }
211
212 /**
213 * Convenience method to create a new setting with the specified identifier and default value.
214 *
215 * @param identifier
216 * the identifier for this setting
217 * @param defaultValue
218 * the default value for this setting
219 *
220 * @return the new setting
221 *
222 * @throws IllegalArgumentException
223 * if the identifier is invalid
224 */
225 public static Setting<String> create(String identifier, String defaultValue) {
226 return new Impl<>(identifier, defaultValue, String.class);
227 }
228
229 /**
230 * Convenience method to create a new setting with the specified identifier and default value.
231 *
232 * @param identifier
233 * the identifier for this setting
234 * @param defaultValue
235 * the default value for this setting
236 *
237 * @return the new setting
238 *
239 * @throws IllegalArgumentException
240 * if the identifier is invalid
241 */
242 public static Setting<Integer> create(String identifier, Integer defaultValue) {
243 return new Impl<>(identifier, defaultValue, Integer.class);
244 }
245
246 /**
247 * Convenience method to create a new setting with the specified identifier and default value.
248 *
249 * @param identifier
250 * the identifier for this setting
251 * @param defaultValue
252 * the default value for this setting
253 *
254 * @return the new setting
255 *
256 * @throws IllegalArgumentException
257 * if the identifier is invalid
258 */
259 public static Setting<Long> create(String identifier, Long defaultValue) {
260 return new Impl<>(identifier, defaultValue, Long.class);
261 }
262
263 /**
264 * Convenience method to create a new setting with the specified identifier and default value.
265 *
266 * @param identifier
267 * the identifier for this setting
268 * @param defaultValue
269 * the default value for this setting
270 *
271 * @return the new setting
272 *
273 * @throws IllegalArgumentException
274 * if the identifier is invalid
275 */
276 public static Setting<Float> create(String identifier, Float defaultValue) {
277 return new Impl<>(identifier, defaultValue, Float.class);
278 }
279
280 /**
281 * Convenience method to create a new setting with the specified identifier and default value.
282 *
283 * @param identifier
284 * the identifier for this setting
285 * @param defaultValue
286 * the default value for this setting
287 *
288 * @return the new setting
289 *
290 * @throws IllegalArgumentException
291 * if the identifier is invalid
292 */
293 public static Setting<Double> create(String identifier, Double defaultValue) {
294 return new Impl<>(identifier, defaultValue, Double.class);
295 }
296
297 /**
298 * Convenience method to create a new setting with the specified identifier, value type, and default value.
299 *
300 * @param <V>
301 * generic setting value type
302 * @param identifier
303 * the identifier for this setting
304 * @param valueType
305 * the value type for this setting
306 * @param defaultValue
307 * the default value for this setting
308 *
309 * @return the new setting
310 *
311 * @throws IllegalArgumentException
312 * if the identifier is invalid, or the value type is primitive, mutable, or has type parameters
313 */
314 public static <V> Setting<V> create(String identifier, Class<V> valueType, V defaultValue) {
315 return new Impl<>(identifier, valueType, defaultValue);
316 }
317
318 ///////////////////////////////////////
319 // Private static helpers
320 ///////////////////////////////////////
321
322 // Use Impl when possible to avoid creating an anonymous class (and class file) for
323 // every single existing setting, and to avoid the overhead of value type lookup.
324 private static class Impl<V> extends Setting<V> {
325 // this constructor does not check the value type
326 private Impl(String identifier, V defaultValue, Class<V> valueType) {
327 super(identifier, defaultValue, valueType);
328 }
329
330 // this constructor does check the value type
331 private Impl(String identifier, Class<V> valueType, V defaultValue) {
332 super(identifier, valueType, defaultValue);
333 }
334 }
335
336 private static final class Key {
337 final String identifier;
338 final Type valueType;
339 final boolean nonnull;
340
341 Key(String identifier, Type valueType, boolean nonnull) {
342 this.identifier = identifier;
343 this.valueType = valueType;
344 this.nonnull = nonnull;
345 }
346 }
347
348 @SuppressWarnings("unchecked")
349 private static <V, S extends Setting<V>> S clone(S setting, V newValue) {
350 try {
351 setting.checkValue(newValue);
352 } catch (Exception e) {
353 throw new IllegalArgumentException("invalid value for key '" + ((Setting<V>) setting).key.identifier + "': "
354 + ((Setting<V>) setting).value, e);
355 }
356
357 // important to clone so that we can retain checkValue(), toString() behavior on returned instance
358 S s = (S) setting.clone();
359
360 assert ((Setting<V>) s).key == ((Setting<V>) setting).key;
361 assert ((Setting<V>) s).getClass().equals(setting.getClass());
362
363 ((Setting<V>) s).value = newValue;
364 return s;
365 }
366
367 private static final Pattern identifierPattern = Pattern.compile("[a-z][0-9a-z]*(\\.[a-z][0-9a-z]*)*");
368
369 private static void checkIdentifier(String identifier) {
370 if (identifier == null) {
371 throw new IllegalArgumentException("identifier cannot be null");
372 }
373 if (!identifierPattern.matcher(identifier).matches()) {
374 throw new IllegalArgumentException("identifier does not match " + identifierPattern.pattern());
375 }
376 }
377
378 private static Type lookupValueType(Class<?> rawType, String identifier) {
379 HashMap<TypeVariable<?>, Type> mapping = new HashMap<>();
380 assert rawType != Setting.class;
381 for (;;) {
382 Type superclass = rawType.getGenericSuperclass();
383 if (superclass instanceof ParameterizedType) {
384 rawType = (Class<?>) ((ParameterizedType) superclass).getRawType();
385 Type[] args = ((ParameterizedType) superclass).getActualTypeArguments();
386 if (Setting.class.equals(rawType)) {
387 Type type = args[0];
388 type = mapping.getOrDefault(type, type);
389 if (type instanceof Class) {
390 if (((Class<?>) type).isArray()) {
391 throw new IllegalArgumentException(identifier + " value class must be immutable");
392 } else if (((Class<?>) type).getTypeParameters().length != 0) {
393 throw new IllegalArgumentException(identifier + " setting must fill in type parameters for "
394 + ((Class<?>) type).toGenericString());
395 }
396 } else if (type instanceof GenericArrayType) {
397 throw new IllegalArgumentException(identifier + " value class must be immutable");
398 } else if (type instanceof TypeVariable) {
399 throw new IllegalArgumentException(
400 "Invalid setting type 'Key<" + type.getTypeName() + ">' for identifier " + identifier);
401 } else if (!(type instanceof ParameterizedType)) {
402 throw new IllegalArgumentException(
403 identifier + " invalid type " + type + " (" + type.getClass().getName() + ")");
404 }
405 return type;
406 }
407 TypeVariable<?>[] vars = rawType.getTypeParameters();
408 for (int i = 0, len = vars.length; i < len; i++) {
409 Type t = args[i];
410 mapping.put(vars[i], t instanceof TypeVariable ? mapping.get(t) : t);
411 }
412 } else {
413 rawType = (Class<?>) superclass;
414 if (Setting.class.equals(rawType)) {
415 throw new IllegalArgumentException(rawType + " does not supply type arguments");
416 }
417 }
418 }
419 }
420
421 }