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 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 }