From 49183fea795fac0626d0fd846859ddb1ef979e20 Mon Sep 17 00:00:00 2001 From: Mustapha Zorgati <15628173+mustaphazorgati@users.noreply.github.com> Date: Fri, 29 Oct 2021 16:24:33 +0200 Subject: [PATCH] TSK-1763: refactored TaskanaConfigurationInitalizer --- .../TaskanaConfigurationInitializer.java | 215 ++++++++++-------- .../util/ObjectAttributeChangeDetector.java | 13 +- .../common/internal/util/ReflectionUtil.java | 45 ++++ .../internal/util/ReflectionUtilTest.java | 48 ++++ .../taskana/TaskanaEngineConfiguration.java | 10 +- 5 files changed, 219 insertions(+), 112 deletions(-) create mode 100644 common/taskana-common/src/main/java/pro/taskana/common/internal/util/ReflectionUtil.java create mode 100644 common/taskana-common/src/test/java/pro/taskana/common/internal/util/ReflectionUtilTest.java diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java index 86cd27778..8f49a08a4 100644 --- a/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -28,6 +29,7 @@ import pro.taskana.common.api.exceptions.SystemException; import pro.taskana.common.api.exceptions.WrongCustomHolidayFormatException; import pro.taskana.common.internal.util.CheckedFunction; import pro.taskana.common.internal.util.Pair; +import pro.taskana.common.internal.util.ReflectionUtil; public class TaskanaConfigurationInitializer { @@ -36,26 +38,38 @@ public class TaskanaConfigurationInitializer { private static final String TASKANA_CUSTOM_HOLIDAY_DAY_MONTH_SEPARATOR = "."; private static final String TASKANA_CLASSIFICATION_CATEGORIES_PROPERTY = "taskana.classification.categories"; + private static final Map, PropertyParser> PROPERTY_INITIALIZER_BY_CLASS = + new HashMap<>(); + + static { + PROPERTY_INITIALIZER_BY_CLASS.put(Integer.class, new IntegerPropertyParser()); + PROPERTY_INITIALIZER_BY_CLASS.put(Boolean.class, new BooleanPropertyParser()); + PROPERTY_INITIALIZER_BY_CLASS.put(String.class, new StringPropertyParser()); + PROPERTY_INITIALIZER_BY_CLASS.put(Duration.class, new DurationPropertyParser()); + PROPERTY_INITIALIZER_BY_CLASS.put(Instant.class, new InstantPropertyParser()); + PROPERTY_INITIALIZER_BY_CLASS.put(List.class, new ListPropertyParser()); + } private TaskanaConfigurationInitializer() { throw new IllegalStateException("utility class"); } - public static Optional parseProperty( - Properties props, String key, CheckedFunction function) { - String property = props.getProperty(key, ""); - if (!property.isEmpty()) { - try { - return Optional.ofNullable(function.apply(property)); - } catch (Throwable t) { - LOGGER.warn( - "Could not parse property {} ({}). Using default. Exception: {}", - key, - property, - t.getMessage()); - } + public static void configureAnnotatedFields(Object instance, String separator, Properties props) { + final List fields = ReflectionUtil.retrieveAllFields(instance.getClass()); + for (Field field : fields) { + Optional.ofNullable(field.getAnnotation(TaskanaProperty.class)) + .ifPresent( + taskanaProperty -> { + Class type = ReflectionUtil.wrap(field.getType()); + PropertyParser propertyParser = + Optional.ofNullable(PROPERTY_INITIALIZER_BY_CLASS.get(type)) + .orElseThrow( + () -> new SystemException("Unknown configuration type " + type)); + propertyParser + .initialize(props, separator, field, taskanaProperty) + .ifPresent(value -> setFieldValue(instance, field, value)); + }); } - return Optional.empty(); } public static Map> configureClassificationCategoriesForType( @@ -89,85 +103,6 @@ public class TaskanaConfigurationInitializer { .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); } - public static List getAllFields(List fields, Class type) { - fields.addAll(Arrays.asList(type.getDeclaredFields())); - if (type.getSuperclass() != null) { - getAllFields(fields, type.getSuperclass()); - } - return fields; - } - - public static void configureAnnotatedFields(Object instance, String separator, Properties props) { - - final List fields = getAllFields(new ArrayList<>(), instance.getClass()); - for (Field field : fields) { - Optional.ofNullable(field.getAnnotation(TaskanaProperty.class)) - .ifPresent( - taskanaProperty -> { - final String fieldPropertyName = taskanaProperty.value(); - final Class type = field.getType(); - String name = type.getSimpleName(); - switch (name) { - case "int": - parseProperty(props, fieldPropertyName, Integer::parseInt) - .ifPresent(value -> setFieldValue(instance, field, value)); - break; - case "boolean": - parseProperty(props, fieldPropertyName, Boolean::parseBoolean) - .ifPresent(value -> setFieldValue(instance, field, value)); - break; - case "String": - parseProperty(props, fieldPropertyName, String::new) - .ifPresent(value -> setFieldValue(instance, field, value)); - break; - case "Duration": - parseProperty(props, fieldPropertyName, Duration::parse) - .ifPresent(value -> setFieldValue(instance, field, value)); - break; - case "Instant": - parseProperty(props, fieldPropertyName, Instant::parse) - .ifPresent(value -> setFieldValue(instance, field, value)); - break; - case "List": - final String typeName = - ((ParameterizedType) field.getGenericType()) - .getActualTypeArguments()[0].getTypeName(); - - if (typeName.equals("pro.taskana.common.api.CustomHoliday")) { - CheckedFunction, Exception> parseFunction2 = - s -> - splitStringAndTrimElements(s, separator).stream() - .map( - str -> { - try { - return createCustomHolidayFromPropsEntry(str); - } catch (WrongCustomHolidayFormatException e) { - LOGGER.warn(e.getMessage()); - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - parseProperty(props, fieldPropertyName, parseFunction2) - .ifPresent(value -> setFieldValue(instance, field, value)); - - } else { - CheckedFunction, Exception> parseListFunction = - p -> splitStringAndTrimElements(p, ",", String::toUpperCase); - parseProperty(props, fieldPropertyName, parseListFunction) - .ifPresent(value -> setFieldValue(instance, field, value)); - } - break; - case "Map": - // TODO - break; - default: - throw new SystemException("Unknown configuration type " + name); - } - }); - } - } - static List splitStringAndTrimElements(String str, String separator) { return splitStringAndTrimElements(str, separator, UnaryOperator.identity()); } @@ -211,4 +146,98 @@ public class TaskanaConfigurationInitializer { "Property value " + value + " is invalid for field " + field.getName(), e); } } + + private static Optional parseProperty( + Properties props, String key, CheckedFunction function) { + String property = props.getProperty(key, ""); + if (!property.isEmpty()) { + try { + return Optional.ofNullable(function.apply(property)); + } catch (Throwable t) { + LOGGER.warn( + "Could not parse property {} ({}). Using default. Exception: {}", + key, + property, + t.getMessage()); + } + } + return Optional.empty(); + } + + interface PropertyParser { + Optional initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty); + } + + static class ListPropertyParser implements PropertyParser> { + @Override + public Optional> initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) { + final String typeName = + ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0].getTypeName(); + + if (typeName.equals("pro.taskana.common.api.CustomHoliday")) { + CheckedFunction, Exception> parseFunction2 = + s -> + splitStringAndTrimElements(s, separator).stream() + .map( + str -> { + try { + return createCustomHolidayFromPropsEntry(str); + } catch (WrongCustomHolidayFormatException e) { + LOGGER.warn(e.getMessage()); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return parseProperty(properties, taskanaProperty.value(), parseFunction2); + + } else { + CheckedFunction, Exception> parseListFunction = + p -> splitStringAndTrimElements(p, ",", String::toUpperCase); + return parseProperty(properties, taskanaProperty.value(), parseListFunction); + } + } + } + + static class InstantPropertyParser implements PropertyParser { + @Override + public Optional initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) { + return parseProperty(properties, taskanaProperty.value(), Instant::parse); + } + } + + static class DurationPropertyParser implements PropertyParser { + @Override + public Optional initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) { + return parseProperty(properties, taskanaProperty.value(), Duration::parse); + } + } + + static class StringPropertyParser implements PropertyParser { + @Override + public Optional initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) { + return parseProperty(properties, taskanaProperty.value(), String::new); + } + } + + static class IntegerPropertyParser implements PropertyParser { + @Override + public Optional initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) { + return parseProperty(properties, taskanaProperty.value(), Integer::parseInt); + } + } + + static class BooleanPropertyParser implements PropertyParser { + @Override + public Optional initialize( + Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) { + return parseProperty(properties, taskanaProperty.value(), Boolean::parseBoolean); + } + } } diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ObjectAttributeChangeDetector.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ObjectAttributeChangeDetector.java index 7ad14abcc..682da6fbf 100644 --- a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ObjectAttributeChangeDetector.java +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ObjectAttributeChangeDetector.java @@ -3,8 +3,6 @@ package pro.taskana.common.internal.util; import static pro.taskana.common.internal.util.CheckedFunction.wrap; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -51,7 +49,7 @@ public class ObjectAttributeChangeDetector { } List changedAttributes = - retrieveAllFields(objectClass).stream() + ReflectionUtil.retrieveAllFields(objectClass).stream() .peek(field -> field.setAccessible(true)) .filter(field -> !"customAttributes".equals(field.getName())) .map(wrap(field -> Triplet.of(field, field.get(oldObject), field.get(newObject)))) @@ -75,15 +73,6 @@ public class ObjectAttributeChangeDetector { return changedAttribute; } - private static List retrieveAllFields(Class currentClass) { - List fields = new ArrayList<>(); - while (currentClass.getSuperclass() != null) { - fields.addAll(Arrays.asList(currentClass.getDeclaredFields())); - currentClass = currentClass.getSuperclass(); - } - return fields; - } - private static String compareLists(T oldObject, T newObject) { if (oldObject.equals(newObject)) { return ""; diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ReflectionUtil.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ReflectionUtil.java new file mode 100644 index 000000000..535e0dc94 --- /dev/null +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/ReflectionUtil.java @@ -0,0 +1,45 @@ +package pro.taskana.common.internal.util; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ReflectionUtil { + + private static final Map, Class> PRIMITIVES_TO_WRAPPERS = new HashMap<>(); + + static { + PRIMITIVES_TO_WRAPPERS.put(boolean.class, Boolean.class); + PRIMITIVES_TO_WRAPPERS.put(byte.class, Byte.class); + PRIMITIVES_TO_WRAPPERS.put(char.class, Character.class); + PRIMITIVES_TO_WRAPPERS.put(double.class, Double.class); + PRIMITIVES_TO_WRAPPERS.put(float.class, Float.class); + PRIMITIVES_TO_WRAPPERS.put(int.class, Integer.class); + PRIMITIVES_TO_WRAPPERS.put(long.class, Long.class); + PRIMITIVES_TO_WRAPPERS.put(short.class, Short.class); + PRIMITIVES_TO_WRAPPERS.put(void.class, Void.class); + } + + private ReflectionUtil() { + throw new IllegalStateException("utility class"); + } + + public static List retrieveAllFields(Class currentClass) { + List fields = new ArrayList<>(); + while (currentClass.getSuperclass() != null) { + fields.addAll(Arrays.asList(currentClass.getDeclaredFields())); + currentClass = currentClass.getSuperclass(); + } + return fields.stream().filter(f -> !f.isSynthetic()).collect(Collectors.toList()); + } + + // safe because both Long.class and long.class are of type Class + @SuppressWarnings("unchecked") + public static Class wrap(Class c) { + return c.isPrimitive() ? (Class) PRIMITIVES_TO_WRAPPERS.get(c) : c; + } +} diff --git a/common/taskana-common/src/test/java/pro/taskana/common/internal/util/ReflectionUtilTest.java b/common/taskana-common/src/test/java/pro/taskana/common/internal/util/ReflectionUtilTest.java new file mode 100644 index 000000000..a19357b27 --- /dev/null +++ b/common/taskana-common/src/test/java/pro/taskana/common/internal/util/ReflectionUtilTest.java @@ -0,0 +1,48 @@ +package pro.taskana.common.internal.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Field; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ReflectionUtilTest { + + @Test + void should_RetrieveAllFieldsFromClassAndSuperClass() { + List fields = ReflectionUtil.retrieveAllFields(SubSubTestClass.class); + + assertThat(fields) + .extracting(Field::getName) + .containsExactlyInAnyOrder("fieldA", "fieldB", "fieldC"); + } + + @Test + void should_WrapPrimitiveClassToItsWrapperClass() { + Class wrap = ReflectionUtil.wrap(int.class); + + assertThat(wrap).isEqualTo(Integer.class); + } + + @Test + void should_NotWrapNonPrimitiveClass() { + Class wrap = ReflectionUtil.wrap(TestClass.class); + + assertThat(wrap).isEqualTo(TestClass.class); + } + + static class TestClass { + @SuppressWarnings("unused") + String fieldA; + } + + static class SubTestClass extends TestClass { + @SuppressWarnings("unused") + String fieldB; + } + + static class SubSubTestClass extends SubTestClass { + @SuppressWarnings("unused") + String fieldC; + } +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java index 96d29a238..884872cc0 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskanaEngineConfiguration.java @@ -63,21 +63,17 @@ public class TaskanaEngineConfiguration { // authorizations protected boolean securityEnabled; protected boolean useManagedTransactions; - - @TaskanaProperty("taskana.custom.holidays") - private List customHolidays = new ArrayList<>(); - // List of configured domain names @TaskanaProperty("taskana.domains") protected List domains = new ArrayList<>(); - // List of configured classification types @TaskanaProperty("taskana.classification.types") protected List classificationTypes = new ArrayList<>(); - @TaskanaProperty("taskana.classification.categories") protected Map> classificationCategoriesByTypeMap = new HashMap<>(); + @TaskanaProperty("taskana.custom.holidays") + private List customHolidays = new ArrayList<>(); // Properties for the monitor @TaskanaProperty("taskana.history.deletion.on.task.deletion.enabled") private boolean deleteHistoryOnTaskDeletionEnabled; @@ -111,7 +107,7 @@ public class TaskanaEngineConfiguration { @TaskanaProperty("taskana.validation.allowTimestampServiceLevelMismatch") private boolean validationAllowTimestampServiceLevelMismatch = false; - //Property to enable/disable the addition of user full/long name through joins + // Property to enable/disable the addition of user full/long name through joins @TaskanaProperty("taskana.addAdditionalUserInfo") private boolean addAdditionalUserInfo = false;