MATSIM
ReflectiveConfigGroup.java
Go to the documentation of this file.
1 /* *********************************************************************** *
2  * project: org.matsim.*
3  * ReflectiveModule.java
4  * *
5  * *********************************************************************** *
6  * *
7  * copyright : (C) 2012 by the members listed in the COPYING, *
8  * LICENSE and WARRANTY file. *
9  * email : info at matsim dot org *
10  * *
11  * *********************************************************************** *
12  * *
13  * This program is free software; you can redistribute it and/or modify *
14  * it under the terms of the GNU General Public License as published by *
15  * the Free Software Foundation; either version 2 of the License, or *
16  * (at your option) any later version. *
17  * See also COPYING, LICENSE and WARRANTY file *
18  * *
19  * *********************************************************************** */
20 package org.matsim.core.config;
21 
22 import static com.google.common.collect.ImmutableSet.toImmutableSet;
23 import static java.util.stream.Collectors.joining;
24 
25 import java.io.Serial;
26 import java.lang.annotation.Documented;
27 import java.lang.annotation.Retention;
28 import java.lang.annotation.RetentionPolicy;
29 import java.lang.reflect.AccessibleObject;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.InvocationTargetException;
32 import java.lang.reflect.Method;
33 import java.lang.reflect.ParameterizedType;
34 import java.lang.reflect.Type;
35 import java.time.LocalDate;
36 import java.time.LocalDateTime;
37 import java.time.LocalTime;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collection;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.stream.Collectors;
47 import java.util.stream.Stream;
48 
49 import javax.annotation.Nullable;
50 
51 import org.apache.logging.log4j.LogManager;
52 import org.apache.logging.log4j.Logger;
53 import org.matsim.api.core.v01.Id;
55 
56 import com.google.common.base.Preconditions;
57 import com.google.common.base.Strings;
58 import com.google.common.collect.ImmutableSet;
59 import com.google.common.collect.Sets;
60 
105 public abstract class ReflectiveConfigGroup extends ConfigGroup implements MatsimExtensionPoint {
106  private static final Logger log = LogManager.getLogger(ReflectiveConfigGroup.class);
107 
108  private final boolean storeUnknownParameters;
109 
110  private final Map<String, Method> setters;
111  private final Map<String, Method> stringGetters;
112  private final Map<String, Field> paramFields;
113  private final Set<String> registeredParams;// accessible via getters/setters or as fields
114 
115  // /////////////////////////////////////////////////////////////////////////
116  // Construction
117  // /////////////////////////////////////////////////////////////////////////
118 
125  public ReflectiveConfigGroup(final String name) {
126  this(name, false);
127  }
128 
138  public ReflectiveConfigGroup(final String name, final boolean storeUnknownParametersAsStrings) {
139  super(name);
140  this.storeUnknownParameters = storeUnknownParametersAsStrings;
141  setters = getSetters();
142  stringGetters = getStringGetters();
143  paramFields = getParamFields();
144  registeredParams = Sets.union(stringGetters.keySet(), paramFields.keySet());
145 
146  // Each parameter which has a setter must have a getter. But there can be parameters which have a getter but no setter in order
147  // to provide backwards compatibility.
148  checkModuleConsistency(setters.keySet().containsAll(stringGetters.keySet()), "setters and getters inconsistent");
149  checkModuleConsistency(paramFields.keySet().stream().noneMatch(setters::containsKey),
150  "Use either StringGetter/Setter or Parameter annotations");
151  }
152 
153  private Map<String, Method> getStringGetters() {
154  final Map<String, Method> gs = new HashMap<>();
155 
156  final var allMethods = getDeclaredMethodsInSubclasses();
157  for (Method m : allMethods) {
158  final StringGetter annotation = m.getAnnotation(StringGetter.class);
159  if (annotation != null) {
161  final Method old = gs.put(annotation.value(), m);
162  checkModuleConsistency(old == null, "several string getters for: %s", annotation.value());
163  }
164  }
165 
166  return gs;
167  }
168 
169  private static void checkGetterValidity(final Method m) {
170  checkModuleConsistency(m.getParameterTypes().length == 0, "getter %s has parameters", m);
171  checkModuleConsistency(!m.getReturnType().equals(Void.TYPE), "getter %s has void return type", m);
172  }
173 
174  private Map<String, Method> getSetters() {
175  final Map<String, Method> ss = new HashMap<>();
176 
177  final var allMethods = getDeclaredMethodsInSubclasses();
178  for (Method m : allMethods) {
179  final StringSetter annotation = m.getAnnotation(StringSetter.class);
180  if (annotation != null) {
182  final Method old = ss.put(annotation.value(), m);
183  checkModuleConsistency(old == null, "several string setters for: %s", annotation.value());
184  }
185  }
186 
187  return ss;
188  }
189 
190  private List<Method> getDeclaredMethodsInSubclasses() {
191  // in order to support multi-level inheritance of ReflectiveConfigGroup, we need to collect all methods from
192  // all levels of inheritance below ReflectiveConfigGroup
193  var methods = new ArrayList<Method>();
194  for (Class<?> c = getClass(); c != ReflectiveConfigGroup.class; c = c.getSuperclass()) {
195  Collections.addAll(methods, c.getDeclaredMethods());
196  }
197  return methods;
198  }
199 
200  private static void checkSetterValidity(final Method m) {
201  var params = m.getGenericParameterTypes();
202  checkModuleConsistency(params.length == 1, "setter %s has %s parameters instead of 1.", m, params.length);
203 
204  var param = params[0];
205  checkModuleConsistency(checkType(param), "Setter %s takes a %s argument." + HINT, m, param);
206  }
207 
208  private Map<String, Field> getParamFields() {
209  final Map<String, Field> pf = new HashMap<>();
210 
211  final var allFields = getDeclaredFieldsInSubclasses();
212  for (Field f : allFields) {
213  Parameter annotation = f.getAnnotation(Parameter.class);
214  if (annotation != null) {
216  var paramName = annotation.value().isEmpty() ? f.getName() : annotation.value();
217  Field old = pf.put(paramName, f);
218  checkModuleConsistency(old == null, "several parameter fields for: %s", paramName);
219  }
220  }
221 
222  return pf;
223  }
224 
225  private List<Field> getDeclaredFieldsInSubclasses() {
226  // in order to support multi-level inheritance of ReflectiveConfigGroup, we need to collect all fields from
227  // all levels of inheritance below ReflectiveConfigGroup
228  var fields = new ArrayList<Field>();
229  for (Class<?> c = getClass(); c != ReflectiveConfigGroup.class; c = c.getSuperclass()) {
230  Collections.addAll(fields, c.getDeclaredFields());
231  }
232  return fields;
233  }
234 
235  private static void checkParamFieldValidity(Field field) {
236  var type = field.getGenericType();
237  checkModuleConsistency(checkType(type), "Field %s is of type %s." + HINT, field, type);
238  }
239 
240  private static final Set<Class<?>> ALLOWED_PARAMETER_TYPES = Set.of(String.class, Float.class, Double.class,
241  Integer.class, Long.class, Boolean.class, Character.class, Byte.class, Short.class, Float.TYPE, Double.TYPE,
242  Integer.TYPE, Long.TYPE, Boolean.TYPE, Character.TYPE, Byte.TYPE, Short.TYPE, LocalTime.class,
243  LocalDate.class, LocalDateTime.class);
244 
245  private static final String HINT = " Valid types are String, primitive types and their wrapper classes,"
246  + " enumerations, List<String> and Set<String>, LocalTime, LocalDate, LocalDateTime"
247  + " Other types are fine as parameters, but you will need to implement conversion strategies"
248  + " in corresponding StringGetters andStringSetters.";
249 
250  private static boolean checkType(Type type) {
251  if (ALLOWED_PARAMETER_TYPES.contains(type)) {
252  return true;
253  } else if (type instanceof Class<?> c && c.isEnum()) {
254  return true;
255  } else if (type instanceof ParameterizedType pType) {
256  var rawType = pType.getRawType();
257  if (rawType.equals(List.class) || rawType.equals(Set.class)) {
258  var typeArgument = pType.getActualTypeArguments()[0];
259  return typeArgument.equals(String.class) ||
260  typeArgument.equals(Double.class) ||
261  typeArgument.equals(Integer.class) ||
262  (typeArgument instanceof Class && ((Class<?>) typeArgument).isEnum());
263  }
264 
265  if (rawType.equals(Class.class))
266  return true;
267  }
268 
269  return false;
270  }
271 
272  // /////////////////////////////////////////////////////////////////////////
273  // module methods
274  // /////////////////////////////////////////////////////////////////////////
275  @Override
276  public final void addParam(final String param_name, final String value) {
277  var setter = setters.get(param_name);
278  if (setter != null) {
279  invokeSetter(setter, value);
280  log.trace("value {} successfully set for field {} for group {}", value, param_name, getName());
281  return;
282  }
283 
284  var field = paramFields.get(param_name);
285  if (field != null) {
286  setParamField(field, value);
287  log.trace("value {} successfully set for field {} for group {}", value, param_name, getName());
288  return;
289  }
290 
291  this.handleAddUnknownParam(param_name, value);
292  }
293 
305  public void handleAddUnknownParam(final String paramName, final String value) {
306  Preconditions.checkArgument(this.storeUnknownParameters, "Module %s of type %s doesn't accept unknown parameters."
307  + " Parameter %s is not part of the valid parameters: %s", getName(), getClass().getName(), paramName,
308  this.registeredParams);
309 
310  log.warn(
311  "Unknown parameter {} for group {}. Here are the valid parameter names: {}. Only the string value will be remembered.",
312  paramName, getName(), this.registeredParams);
313  super.addParam(paramName, value);
314  }
315 
316  private void invokeSetter(final Method setter, final String value) {
317  boolean accessible = enforceAccessible(setter);
318  try {
319  var type = setter.getParameterTypes()[0];
320  setter.invoke(this, fromString(value, type, null));
321  } catch (IllegalAccessException e) {
322  throw new RuntimeException(e);
323  } catch (InvocationTargetException e) {
324  throw convertToUnchecked(e);
325  } finally {
326  setter.setAccessible(accessible);
327  }
328  }
329 
330  private void setParamField(Field paramField, String value) {
331  boolean accessible = enforceAccessible(paramField);
332  try {
333  var type = paramField.getType();
334  paramField.set(this, fromString(value, type, paramField));
335  } catch (IllegalAccessException e) {
336  throw new RuntimeException(e);
337  } finally {
338  paramField.setAccessible(accessible);
339  }
340  }
341 
342  private boolean enforceAccessible(AccessibleObject object) {
343  // do not care about access modifier:
344  // if a method is tagged with the StringSetter
345  // annotation, we are supposed to access it.
346  // This *is* safe.
347  boolean accessible = object.isAccessible();
348  object.setAccessible(true);
349  return accessible;
350  }
351 
352  private RuntimeException convertToUnchecked(InvocationTargetException e) {
353  // this exception wraps Throwables intercepted in the invocation of the setter.
354  // Avoid multiple wrappings (exception wrapped in InvocationTargetException
355  // itself wrapped in a RuntimeException), as it makes error messages
356  // messy.
357  var cause = e.getCause();
358  if (cause instanceof Error error) {
359  throw error;
360  }
361  return (cause instanceof RuntimeException runtimeException) ? runtimeException : new RuntimeException(cause);
362  }
363 
364  private Object fromString(String value, Class<?> type, @Nullable Field paramField) {
365  if (value.equals("null")) {
366  return null;
367  } else if (type.equals(String.class)) {
368  return value;
369  } else if (type.equals(LocalTime.class)) {
370  return LocalTime.parse(value);
371  } else if (type.equals(LocalDate.class)) {
372  return LocalDate.parse(value);
373  } else if (type.equals(LocalDateTime.class)) {
374  return LocalDateTime.parse(value);
375  } else if (type.equals(Float.class) || type.equals(Float.TYPE)) {
376  return Float.parseFloat(value);
377  } else if (type.equals(Double.class) || type.equals(Double.TYPE)) {
378  return Double.parseDouble(value);
379  } else if (type.equals(Integer.class) || type.equals(Integer.TYPE)) {
380  return Integer.parseInt(value);
381  } else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
382  return Long.parseLong(value);
383  } else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE)) {
384  Preconditions.checkArgument(value.equals("true") || value.equals("false"),
385  "Incorrect value of the boolean parameter: %s", value);
386  return Boolean.parseBoolean(value);
387  } else if (type.equals(Character.class) || type.equals(Character.TYPE)) {
388  Preconditions.checkArgument(value.length() == 1, "%s is not a single char!", value);
389  return value.toCharArray()[0];
390  } else if (type.equals(Byte.class) || type.equals(Byte.TYPE)) {
391  return Byte.parseByte(value);
392  } else if (type.equals(Short.class) || type.equals(Short.TYPE)) {
393  return Short.parseShort(value);
394  } else if (type.isEnum()) {
395  try {
396  return Enum.valueOf(type.asSubclass(Enum.class), value);
397  } catch (IllegalArgumentException e) {
398  // happens when the string does not correspond to any enum values.
399  // Surprisingly, the default error message does not print the possible
400  // values: do it here, so that the user gets an idea of what went wrong
401  String comment = "Error trying to set value "
402  + value
403  + " for type "
404  + type.getName()
405  + ". Possible values are: "
406  + Arrays.stream(type.getEnumConstants()).map(Object::toString).collect(joining(","));
407  throw new IllegalArgumentException(comment, e);
408  }
409  } else if (type.equals(Set.class)) {
410  if (value.isBlank()) {
411  return ImmutableSet.of();
412  }
413  Stream<String> stream = splitStringToStream(value);
414  if (paramField != null && isCollectionOfEnumsWithUniqueStringValues(paramField)) {
415  List<? extends Enum<?>> enumConstants = getEnumConstants(paramField);
416  return stream.map(s -> stringToEnumValue(s, enumConstants)).collect(toImmutableSet());
417  }
418  if (paramField != null && isCollectionOfDoubleType(paramField))
419  return stream.map(Double::parseDouble).collect(toImmutableSet());
420  if (paramField != null && isCollectionOfIntegerType(paramField))
421  return stream.map(Integer::parseInt).collect(toImmutableSet());
422 
423  return stream.collect(toImmutableSet());
424  } else if (type.equals(List.class)) {
425  if (value.isBlank()) {
426  return List.of();
427  }
428  Stream<String> stream = splitStringToStream(value);
429  if (paramField != null && isCollectionOfEnumsWithUniqueStringValues(paramField)) {
430  List<? extends Enum<?>> enumConstants = getEnumConstants(paramField);
431  return stream.map(s -> stringToEnumValue(s, enumConstants)).toList();
432  }
433  if (paramField != null && isCollectionOfDoubleType(paramField))
434  return stream.map(Double::parseDouble).toList();
435  if (paramField != null && isCollectionOfIntegerType(paramField))
436  return stream.map(Integer::parseInt).toList();
437 
438  return stream.toList();
439  } else if (type.equals(Class.class)) {
440  try {
441  return ClassLoader.getSystemClassLoader().loadClass(value);
442  } catch (ClassNotFoundException e) {
443  throw new RuntimeException("Could not load specified class; " + value, e);
444  }
445  } else {
446  throw new RuntimeException("Unsupported type: " + type);
447  }
448  }
449 
450  private Stream<String> splitStringToStream(String value) {
451  return Arrays.stream(value.split(","))
452  .peek(e -> Preconditions.checkArgument(!e.isBlank(),
453  "String '%s' contains comma-separated blank elements. Only non-blank elements are supported.",
454  value))
455  .map(String::trim);
456  }
457 
458  @Override
459  public final String getValue(final String param_name) {
460  var getter = stringGetters.get(param_name);
461  if (getter != null) {
462  return invokeGetter(getter);
463  }
464 
465  var field = paramFields.get(param_name);
466  if (field != null) {
467  return getParamField(field);
468  }
469  return this.handleGetUnknownValue(param_name);
470  }
471 
478  public String handleGetUnknownValue(final String paramName) {
479  Preconditions.checkArgument(this.storeUnknownParameters, "Module %s of type %s doesn't store unknown parameters."
480  + " Parameter %s is not part of the valid parameters: %s", getName(), getClass().getName(), paramName,
481  this.registeredParams);
482 
483  log.warn("no getter found for param {}: trying parent method", paramName);
484  return super.getValue(paramName);
485  }
486 
487  private String invokeGetter(Method getter) {
488  boolean accessible = enforceAccessible(getter);
489  try {
490  return toString(getter.invoke(this));
491  } catch (IllegalAccessException e) {
492  throw new RuntimeException(e);
493  } catch (InvocationTargetException e) {
494  throw convertToUnchecked(e);
495  } finally {
496  getter.setAccessible(accessible);
497  }
498  }
499 
500  private String getParamField(Field paramField) {
501  boolean accessible = enforceAccessible(paramField);
502  try {
503  var result = paramField.get(this);
504  if (result != null && (isCollectionOfEnumsWithUniqueStringValues(paramField) ||
505  isCollectionOfDoubleType(paramField) ||
506  isCollectionOfIntegerType(paramField))) {
507  result = ((Collection<Object>) result).stream()
508  .map(Object::toString) // map enum values to string
509  .collect(Collectors.toList());
510  }
511  return toString(result);
512  } catch (IllegalAccessException e) {
513  throw new RuntimeException(e);
514  } finally {
515  paramField.setAccessible(accessible);
516  }
517  }
518 
519  private String toString(Object result) {
520  if (result == null) {
521  return null;
522  } else if (result instanceof Set<?> || result instanceof List<?>) {
523  //we only support Set<String> and List<String>, therefore we can safely cast to Collection<String>
524  var collection = ((Collection<String>)result);
525  Preconditions.checkArgument(collection.stream().noneMatch(String::isBlank),
526  "Collection %s contains blank elements. Only non-blank elements are supported.", collection);
527  return String.join(", ", collection);
528  } else {
529  return result + "";
530  }
531  }
532 
533  @Override
534  public final Map<String, String> getParams() {
535  final Map<String, String> map = super.getParams();
536  registeredParams.forEach(f -> addParameterToMap(map, f));
537  return map;
538  }
539 
548  @Override
549  public Map<String, String> getComments() {
550  // generate some default comments.
551  final Map<String, String> comments = super.getComments();
552 
553  for (String paramName : registeredParams) {
554  if (comments.containsKey(paramName)) {
555  // at the time of implementation, this is not possible,
556  // but who knows? Do not override something already there.
557  continue;
558  }
559 
560  final Class<?> type;
561  var setter = setters.get(paramName);
562  if (setter != null) {
563  type = setter.getParameterTypes()[0];
564  } else {
565  var field = paramFields.get(paramName);
566 
567  Comment annotation = field.getAnnotation(Comment.class);
568  if (annotation != null) {
569  comments.put(paramName, annotation.value());
570  continue;
571  }
572 
573  type = field.getType();
574  }
575 
576  if (type.isEnum()) {
577  // generate an automatic comment containing the possible values for enum types
578  var comment = "Possible values: " + Arrays.stream(type.getEnumConstants())
579  .map(Object::toString)
580  .collect(joining(","));
581  comments.put(paramName, comment);
582  }
583  }
584 
585  return comments;
586  }
587 
588  // /////////////////////////////////////////////////////////////////////////
589  // annotations
590  // /////////////////////////////////////////////////////////////////////////
591 
598  @Documented
599  @Retention(RetentionPolicy.RUNTIME)
600  public @interface StringSetter {
604  String value();
605  }
606 
614  @Documented
615  @Retention(RetentionPolicy.RUNTIME)
616  public @interface StringGetter {
620  String value();
621  }
622 
623  @Documented
624  @Retention(RetentionPolicy.RUNTIME)
625  public @interface Parameter {
631  String value() default "";
632  }
633 
634  @Documented
635  @Retention(RetentionPolicy.RUNTIME)
636  public @interface Comment {
640  String value() default "";
641  }
642 
643  private static void checkModuleConsistency(boolean condition, String messageTemplate, Object... args) {
644  if (!condition) {
645  throw new InconsistentModuleException(Strings.lenientFormat(messageTemplate, args));
646  }
647  }
648 
649  public static class InconsistentModuleException extends RuntimeException {
650  @Serial
651  private static final long serialVersionUID = 1L;
652 
653  private InconsistentModuleException(final String msg) {
654  super(msg);
655  }
656  }
657 
658  // Helpers to support List<Enum> and Set<Enum> as fields
659 
660  private static <E extends Enum<?>> List<E> getEnumConstants(Field paramField) {
661  var type = paramField.getGenericType();
662  if (type instanceof ParameterizedType pType) {
663  var rawType = pType.getRawType();
664  if (rawType.equals(List.class) || rawType.equals(Set.class)) {
665  var typeArgument = pType.getActualTypeArguments()[0];
666  if (typeArgument instanceof Class && ((Class<?>) typeArgument).isEnum()) {
667  return Arrays.asList(((Class<E>) typeArgument).getEnumConstants());
668  }
669  }
670  }
671  return Collections.emptyList(); // no enum -> empty list
672  }
673 
674  private static boolean isCollectionOfEnumsWithUniqueStringValues(Field paramField) {
675  // This checks
676  // (1) whether the paramField is a list/set of Enum values, and
677  // (2) whether the enum constants of that Enum class have a differentiable string representation
678  var type = paramField.getGenericType();
679  if (type instanceof ParameterizedType pType) {
680  var rawType = pType.getRawType();
681  if (rawType.equals(List.class) || rawType.equals(Set.class)) {
682  var typeArgument = pType.getActualTypeArguments()[0];
683  if (typeArgument instanceof Class && ((Class<?>) typeArgument).isEnum()) {
684  // here, paramField *is* collection of Enums
685  return enumStringsAreUnique(((Class<?>) typeArgument));
686  }
687  }
688  }
689  return false;
690  }
691 
692  private static boolean isCollectionOfIntegerType(Field paramField) {
693  var type = paramField.getGenericType();
694  if (type instanceof ParameterizedType pType) {
695  var rawType = pType.getRawType();
696  if (rawType.equals(List.class) || rawType.equals(Set.class)) {
697  var typeArgument = pType.getActualTypeArguments()[0];
698  return typeArgument.equals(Integer.class) || typeArgument.equals(Integer.TYPE);
699  }
700  }
701  return false;
702  }
703 
704  private static boolean isCollectionOfDoubleType(Field paramField) {
705  var type = paramField.getGenericType();
706  if (type instanceof ParameterizedType pType) {
707  var rawType = pType.getRawType();
708  if (rawType.equals(List.class) || rawType.equals(Set.class)) {
709  var typeArgument = pType.getActualTypeArguments()[0];
710  return typeArgument.equals(Double.class) || typeArgument.equals(Double.TYPE);
711  }
712  }
713  return false;
714  }
715 
716  private static <T> boolean enumStringsAreUnique(Class<T> enumClass) {
717  T[] enumConstants = enumClass.getEnumConstants();
718  long uniqueStringValues = Arrays.stream(enumConstants)
719  .map(Object::toString)
720  .distinct()
721  .count();
722  if (uniqueStringValues != enumConstants.length) {
723  throw new IllegalArgumentException("Enum class " + enumClass + " has values with identical string value");
724  }
725  return true; // if true, then we can reconstruct a List<Enum> from List<String>
726  }
727 
728  private static Enum<?> stringToEnumValue(String s, List<? extends Enum<?>> enumConstants) {
729  for (Enum<?> e : enumConstants) {
730  if (e.toString().equals(s)) {
731  return e;
732  }
733  }
734  return null;
735  }
736 }
737 
Object fromString(String value, Class<?> type, @Nullable Field paramField)
static Enum<?> stringToEnumValue(String s, List<? extends Enum<?>> enumConstants)
ReflectiveConfigGroup(final String name, final boolean storeUnknownParametersAsStrings)
String handleGetUnknownValue(final String paramName)
static boolean isCollectionOfEnumsWithUniqueStringValues(Field paramField)
static void checkModuleConsistency(boolean condition, String messageTemplate, Object... args)
static boolean isCollectionOfDoubleType(Field paramField)
final String getValue(final String param_name)
void addParameterToMap(final Map< String, String > map, final String paramName)
boolean enforceAccessible(AccessibleObject object)
final void addParam(final String param_name, final String value)
final TreeMap< String, String > params
void handleAddUnknownParam(final String paramName, final String value)
void invokeSetter(final Method setter, final String value)
RuntimeException convertToUnchecked(InvocationTargetException e)
static boolean isCollectionOfIntegerType(Field paramField)
void setParamField(Field paramField, String value)
static< T > boolean enumStringsAreUnique(Class< T > enumClass)
static< E extends Enum<?> List< E > getEnumConstants(Field paramField)