20 package org.matsim.core.config;
22 import static com.google.common.collect.ImmutableSet.toImmutableSet;
23 import static java.util.stream.Collectors.joining;
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;
46 import java.util.stream.Collectors;
47 import java.util.stream.Stream;
49 import javax.annotation.Nullable;
51 import org.apache.logging.log4j.LogManager;
52 import org.apache.logging.log4j.Logger;
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;
140 this.storeUnknownParameters = storeUnknownParametersAsStrings;
144 registeredParams = Sets.union(stringGetters.keySet(), paramFields.keySet());
148 checkModuleConsistency(setters.keySet().containsAll(stringGetters.keySet()),
"setters and getters inconsistent");
150 "Use either StringGetter/Setter or Parameter annotations");
154 final Map<String, Method> gs =
new HashMap<>();
157 for (Method m : allMethods) {
159 if (annotation != null) {
161 final Method old = gs.put(annotation.
value(), m);
175 final Map<String, Method> ss =
new HashMap<>();
178 for (Method m : allMethods) {
180 if (annotation != null) {
182 final Method old = ss.put(annotation.
value(), m);
193 var methods =
new ArrayList<Method>();
195 Collections.addAll(methods, c.getDeclaredMethods());
201 var
params = m.getGenericParameterTypes();
202 checkModuleConsistency(params.length == 1,
"setter %s has %s parameters instead of 1.", m, params.length);
204 var param = params[0];
209 final Map<String, Field> pf =
new HashMap<>();
212 for (Field f : allFields) {
214 if (annotation != null) {
216 var paramName = annotation.
value().isEmpty() ? f.getName() : annotation.
value();
217 Field old = pf.put(paramName, f);
228 var fields =
new ArrayList<Field>();
230 Collections.addAll(fields, c.getDeclaredFields());
236 var type = field.getGenericType();
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);
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.";
251 if (ALLOWED_PARAMETER_TYPES.contains(type)) {
253 }
else if (type instanceof Class<?> c && c.isEnum()) {
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());
265 if (rawType.equals(Class.class))
276 public final void addParam(
final String param_name,
final String value) {
277 var setter = setters.get(param_name);
278 if (setter != null) {
280 log.trace(
"value {} successfully set for field {} for group {}", value, param_name,
getName());
284 var field = paramFields.get(param_name);
287 log.trace(
"value {} successfully set for field {} for group {}", value, param_name,
getName());
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);
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);
319 var type = setter.getParameterTypes()[0];
320 setter.invoke(
this,
fromString(value, type, null));
321 }
catch (IllegalAccessException e) {
323 }
catch (InvocationTargetException e) {
326 setter.setAccessible(accessible);
333 var type = paramField.getType();
334 paramField.set(
this,
fromString(value, type, paramField));
335 }
catch (IllegalAccessException e) {
338 paramField.setAccessible(accessible);
347 boolean accessible =
object.isAccessible();
348 object.setAccessible(
true);
357 var cause = e.getCause();
358 if (cause instanceof Error error) {
364 private Object
fromString(String value, Class<?> type, @Nullable Field paramField) {
365 if (value.equals(
"null")) {
367 }
else if (type.equals(String.class)) {
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()) {
396 return Enum.valueOf(type.asSubclass(Enum.class), value);
397 }
catch (IllegalArgumentException e) {
401 String comment =
"Error trying to set value " 405 +
". Possible values are: " 406 + Arrays.stream(type.getEnumConstants()).map(Object::toString).collect(joining(
","));
407 throw new IllegalArgumentException(comment, e);
409 }
else if (type.equals(Set.class)) {
410 if (value.isBlank()) {
411 return ImmutableSet.of();
416 return stream.map(s ->
stringToEnumValue(s, enumConstants)).collect(toImmutableSet());
419 return stream.map(Double::parseDouble).collect(toImmutableSet());
421 return stream.map(Integer::parseInt).collect(toImmutableSet());
423 return stream.collect(toImmutableSet());
424 }
else if (type.equals(List.class)) {
425 if (value.isBlank()) {
434 return stream.map(Double::parseDouble).toList();
436 return stream.map(Integer::parseInt).toList();
438 return stream.toList();
439 }
else if (type.equals(Class.class)) {
441 return ClassLoader.getSystemClassLoader().loadClass(value);
442 }
catch (ClassNotFoundException e) {
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.",
459 public final String
getValue(
final String param_name) {
460 var getter = stringGetters.get(param_name);
461 if (getter != null) {
465 var field = paramFields.get(param_name);
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);
483 log.warn(
"no getter found for param {}: trying parent method", paramName);
484 return super.getValue(paramName);
490 return toString(getter.invoke(
this));
491 }
catch (IllegalAccessException e) {
493 }
catch (InvocationTargetException e) {
496 getter.setAccessible(accessible);
503 var result = paramField.get(
this);
507 result = ((Collection<Object>) result).stream()
508 .map(Object::toString)
509 .collect(Collectors.toList());
512 }
catch (IllegalAccessException e) {
515 paramField.setAccessible(accessible);
520 if (result == null) {
522 }
else if (result instanceof Set<?> || result instanceof List<?>) {
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);
535 final Map<String, String> map = super.getParams();
551 final Map<String, String> comments = super.getComments();
553 for (String paramName : registeredParams) {
554 if (comments.containsKey(paramName)) {
561 var setter = setters.get(paramName);
562 if (setter != null) {
563 type = setter.getParameterTypes()[0];
565 var field = paramFields.get(paramName);
568 if (annotation != null) {
569 comments.put(paramName, annotation.
value());
573 type = field.getType();
578 var comment =
"Possible values: " + Arrays.stream(type.getEnumConstants())
579 .map(Object::toString)
580 .collect(joining(
","));
581 comments.put(paramName, comment);
599 @Retention(RetentionPolicy.RUNTIME)
615 @Retention(RetentionPolicy.RUNTIME)
624 @Retention(RetentionPolicy.RUNTIME)
631 String value()
default "";
635 @Retention(RetentionPolicy.RUNTIME)
640 String value()
default "";
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()) {
671 return Collections.emptyList();
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()) {
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);
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);
717 T[] enumConstants = enumClass.getEnumConstants();
718 long uniqueStringValues = Arrays.stream(enumConstants)
719 .map(Object::toString)
722 if (uniqueStringValues != enumConstants.length) {
723 throw new IllegalArgumentException(
"Enum class " + enumClass +
" has values with identical string value");
729 for (Enum<?> e : enumConstants) {
730 if (e.toString().equals(s)) {
Map< String, Method > getStringGetters()
static void checkParamFieldValidity(Field field)
String toString(Object result)
Object fromString(String value, Class<?> type, @Nullable Field paramField)
static Enum<?> stringToEnumValue(String s, List<? extends Enum<?>> enumConstants)
final Map< String, Method > stringGetters
ReflectiveConfigGroup(final String name, final boolean storeUnknownParametersAsStrings)
List< Method > getDeclaredMethodsInSubclasses()
final Map< String, Field > paramFields
static final long serialVersionUID
final Set< String > registeredParams
String handleGetUnknownValue(final String paramName)
Map< String, String > getComments()
Stream< String > splitStringToStream(String value)
String invokeGetter(Method getter)
static void checkGetterValidity(final Method m)
Map< String, Field > getParamFields()
static boolean isCollectionOfEnumsWithUniqueStringValues(Field paramField)
static void checkModuleConsistency(boolean condition, String messageTemplate, Object... args)
final Map< String, String > getParams()
InconsistentModuleException(final String msg)
Map< String, Method > getSetters()
static boolean isCollectionOfDoubleType(Field paramField)
final String getValue(final String param_name)
static final Set< Class<?> > ALLOWED_PARAMETER_TYPES
String getParamField(Field paramField)
ReflectiveConfigGroup(final String name)
void addParameterToMap(final Map< String, String > map, final String paramName)
final boolean storeUnknownParameters
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)
final Map< String, Method > setters
void invokeSetter(final Method setter, final String value)
RuntimeException convertToUnchecked(InvocationTargetException e)
static void checkSetterValidity(final Method m)
List< Field > getDeclaredFieldsInSubclasses()
static boolean isCollectionOfIntegerType(Field paramField)
String value() default ""
void setParamField(Field paramField, String value)
static< T > boolean enumStringsAreUnique(Class< T > enumClass)
static boolean checkType(Type type)
static< E extends Enum<?> List< E > getEnumConstants(Field paramField)