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 }