View Javadoc
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
41       * with concrete type arguments.
42       * @param identifier the identifier for this setting
43       * @param defaultValue the default value for this setting
44       * @throws IllegalArgumentException if the identifier or any of the type arguments were invalid
45       */
46      protected Setting(String identifier, V defaultValue) {
47          checkIdentifier(identifier);
48          this.key = new Key(identifier, lookupValueType(getClass(), identifier), defaultValue != null);
49          this.value = defaultValue;
50      }
51  
52      /**
53       * Constructs a new setting with the specified identifier, value type, and default value.
54       * @param identifier the identifier for this setting
55       * @param valueType the value type for this setting
56       * @param defaultValue the default value for this setting
57       * @throws IllegalArgumentException if the identifier is invalid, or
58       * the value type is primitive, mutable, or has type parameters
59       */
60      protected Setting(String identifier, Class<V> valueType, V defaultValue) {
61          this(identifier, defaultValue, valueType);
62          if (valueType.isArray()) {
63              throw new IllegalArgumentException(identifier + " value class must be immutable");
64          } else if (valueType.getTypeParameters().length != 0) {
65              throw new IllegalArgumentException(identifier + " setting key must fill in type parameters for "
66                      + valueType.toGenericString());
67          } else if (valueType.isPrimitive()) {
68              //ensure using primitive wrapper classes
69              //so that Class.isInstance(), etc. will work as expected
70              throw new IllegalArgumentException(identifier + " value class cannot be primitive");
71          }
72      }
73  
74      private Setting(String identifier, V defaultValue, Class<V> valueType) {
75          checkIdentifier(identifier);
76          this.key = new Key(identifier, valueType, defaultValue != null);
77          this.value = defaultValue;
78      }
79  
80      /**
81       * @return the identifier for this setting
82       */
83      public final String getIdentifier() {
84          return key.identifier;
85      }
86  
87      /**
88       * Subclasses may override this method to check that new values for this setting are valid.
89       * The default implementation of this method throws a {@link NullPointerException} if the
90       * new value is null and the original default value for this setting was non-null.
91       *
92       * @param newValue the new value for this setting
93       * @throws Exception if the new value for this setting is invalid
94       */
95      protected void checkValue(V newValue) throws Exception {
96          if (newValue == null && key.nonnull) {
97              throw new NullPointerException();
98          }
99      }
100 
101     /**
102      * @return the value for this setting
103      */
104     public final V getValue() {
105         return value;
106     }
107 
108     /**
109      * @return the type of value supported for this setting
110      */
111     public final Type getValueType() {
112         return key.valueType;
113     }
114 
115     /**
116      * @return this setting, if this setting has the same key as the supplied setting
117      */
118     @SuppressWarnings("unchecked")
119     public final <S extends Setting<?>> Optional<S> as(S setting) {
120         return key == ((Setting<?>)setting).key ? Optional.of((S)this) : Optional.empty();
121     }
122 
123     /**
124      * @return a new {@link Setting} object with this setting's key and the supplied value.
125      *
126      * @throws IllegalArgumentException if the new value was invalid, as determined by:
127      * <pre>
128      *     {@code this.checkValue(newValue)}
129      * </pre>
130      *
131      * @see Setting#checkValue(Object)
132      */
133     public final Setting<V> withValue(V newValue) {
134         return clone(this, newValue);
135     }
136 
137     @Override
138     protected final Object clone() {
139         try {
140             //ensure no subclasses override this incorrectly
141             return super.clone();
142         } catch (CloneNotSupportedException e) {
143             throw new AssertionError(e);
144         }
145     }
146 
147     /**
148      * @return true if the supplied object is an instance of {@link Setting}
149      * and has the same key and value as this setting.
150      */
151     @Override
152     public final boolean equals(Object o) {
153         if (this == o) return true;
154         if (!(o instanceof Setting)) return false;
155 
156         Setting<?> setting = (Setting<?>) o;
157         return key == setting.key && Objects.equals(value, setting.value);
158     }
159 
160     @Override
161     public final int hashCode() {
162         return key.hashCode() ^ Objects.hashCode(value);
163     }
164 
165     @Override
166     public String toString() {
167         return key.identifier + "=" + value;
168     }
169 
170 
171     /**
172      * Convenience method to create a new setting with the specified identifier and default value.
173      * @param identifier the identifier for this setting
174      * @param defaultValue the default value for this setting
175      * @return the new setting
176      * @throws IllegalArgumentException if the identifier is invalid
177      */
178     public static Setting<Boolean> create(String identifier, Boolean defaultValue) {
179         return new Impl<>(identifier, defaultValue, Boolean.class);
180     }
181 
182     /**
183      * Convenience method to create a new setting with the specified identifier and default value.
184      * @param identifier the identifier for this setting
185      * @param defaultValue the default value for this setting
186      * @return the new setting
187      * @throws IllegalArgumentException if the identifier is invalid
188      */
189     public static Setting<String> create(String identifier, String defaultValue) {
190         return new Impl<>(identifier, defaultValue, String.class);
191     }
192 
193     /**
194      * Convenience method to create a new setting with the specified identifier and default value.
195      * @param identifier the identifier for this setting
196      * @param defaultValue the default value for this setting
197      * @return the new setting
198      * @throws IllegalArgumentException if the identifier is invalid
199      */
200     public static Setting<Integer> create(String identifier, Integer defaultValue) {
201         return new Impl<>(identifier, defaultValue, Integer.class);
202     }
203 
204     /**
205      * Convenience method to create a new setting with the specified identifier and default value.
206      * @param identifier the identifier for this setting
207      * @param defaultValue the default value for this setting
208      * @return the new setting
209      * @throws IllegalArgumentException if the identifier is invalid
210      */
211     public static Setting<Long> create(String identifier, Long defaultValue) {
212         return new Impl<>(identifier, defaultValue, Long.class);
213     }
214 
215     /**
216      * Convenience method to create a new setting with the specified identifier and default value.
217      * @param identifier the identifier for this setting
218      * @param defaultValue the default value for this setting
219      * @return the new setting
220      * @throws IllegalArgumentException if the identifier is invalid
221      */
222     public static Setting<Float> create(String identifier, Float defaultValue) {
223         return new Impl<>(identifier, defaultValue, Float.class);
224     }
225 
226     /**
227      * Convenience method to create a new setting with the specified identifier and default value.
228      * @param identifier the identifier for this setting
229      * @param defaultValue the default value for this setting
230      * @return the new setting
231      * @throws IllegalArgumentException if the identifier is invalid
232      */
233     public static Setting<Double> create(String identifier, Double defaultValue) {
234         return new Impl<>(identifier, defaultValue, Double.class);
235     }
236 
237     /**
238      * Convenience method to create a new setting with the specified identifier,
239      * value type, and default value.
240      * @param identifier the identifier for this setting
241      * @param valueType the value type for this setting
242      * @param defaultValue the default value for this setting
243      * @return the new setting
244      * @throws IllegalArgumentException if the identifier is invalid, or
245      * the value type is primitive, mutable, or has type parameters
246      */
247     public static <V> Setting<V> create(String identifier, Class<V> valueType, V defaultValue) {
248         return new Impl<>(identifier, valueType, defaultValue);
249     }
250 
251 
252 
253     ///////////////////////////////////////
254     // Private static helpers
255     ///////////////////////////////////////
256 
257     // Use Impl when possible to avoid creating an anonymous class (and class file) for
258     // every single existing setting, and to avoid the overhead of value type lookup.
259     private static class Impl<V> extends Setting<V> {
260         //this constructor does not check the value type
261         private Impl(String identifier, V defaultValue, Class<V> valueType) {
262             super(identifier, defaultValue, valueType);
263         }
264         //this constructor does check the value type
265         private Impl(String identifier, Class<V> valueType, V defaultValue) {
266             super(identifier, valueType, defaultValue);
267         }
268     }
269 
270     private static final class Key {
271         final String identifier;
272         final Type valueType;
273         final boolean nonnull;
274 
275         Key(String identifier, Type valueType, boolean nonnull) {
276             this.identifier = identifier;
277             this.valueType = valueType;
278             this.nonnull = nonnull;
279         }
280     }
281 
282     @SuppressWarnings("unchecked")
283     private static <V, S extends Setting<V>> S clone(S setting, V newValue) {
284         try {
285             setting.checkValue(newValue);
286         } catch (Exception e) {
287             throw new IllegalArgumentException("invalid value for key '"
288                     + ((Setting<V>)setting).key.identifier + "': " + ((Setting<V>)setting).value, e);
289         }
290 
291         //important to clone so that we can retain checkValue(), toString() behavior on returned instance
292         S s = (S)setting.clone();
293 
294         assert ((Setting<V>)s).key == ((Setting<V>)setting).key;
295         assert ((Setting<V>)s).getClass().equals(setting.getClass());
296 
297         ((Setting<V>)s).value = newValue;
298         return s;
299     }
300 
301     private static final Pattern identifierPattern = Pattern.compile("[a-z][0-9a-z]*(\\.[a-z][0-9a-z]*)*");
302     private static void checkIdentifier(String identifier) {
303         if (identifier == null) {
304             throw new IllegalArgumentException("identifier cannot be null");
305         }
306         if (!identifierPattern.matcher(identifier).matches()) {
307             throw new IllegalArgumentException("identifier does not match " + identifierPattern.pattern());
308         }
309     }
310 
311     private static Type lookupValueType(Class<?> rawType, String identifier) {
312         HashMap<TypeVariable<?>, Type> mapping = new HashMap<>();
313         assert rawType != Setting.class;
314         for (;;) {
315             Type superclass = rawType.getGenericSuperclass();
316             if (superclass instanceof ParameterizedType) {
317                 rawType = (Class)((ParameterizedType) superclass).getRawType();
318                 Type[] args = ((ParameterizedType) superclass).getActualTypeArguments();
319                 if (Setting.class.equals(rawType)) {
320                     Type type = args[0];
321                     type = mapping.getOrDefault(type, type);
322                     if (type instanceof Class) {
323                         if (((Class) type).isArray()) {
324                             throw new IllegalArgumentException(identifier + " value class must be immutable");
325                         } else if (((Class) type).getTypeParameters().length != 0) {
326                             throw new IllegalArgumentException(identifier + " setting must fill in type parameters for " + ((Class) type).toGenericString());
327                         }
328                     } else if (type instanceof GenericArrayType) {
329                         throw new IllegalArgumentException(identifier + " value class must be immutable");
330                     } else if (type instanceof TypeVariable) {
331                         throw new IllegalArgumentException("Invalid setting type 'Key<" + type.getTypeName() + ">' for identifier " + identifier);
332                     } else if (!(type instanceof ParameterizedType)) {
333                         throw new IllegalArgumentException(identifier + " invalid type " + type + " (" + type.getClass().getName() + ")");
334                     }
335                     return type;
336                 }
337                 TypeVariable<?>[] vars = rawType.getTypeParameters();
338                 for (int i = 0, len = vars.length; i < len; i++) {
339                     Type t = args[i];
340                     mapping.put(vars[i], t instanceof TypeVariable ? mapping.get(t) : t);
341                 }
342             } else {
343                 rawType = (Class<?>)superclass;
344                 if (Setting.class.equals(rawType)) {
345                     throw new IllegalArgumentException(rawType + " does not supply type arguments");
346                 }
347             }
348         }
349     }
350 
351 }