TSK-1763: refactored TaskanaConfigurationInitalizer
This commit is contained in:
parent
9d615c437a
commit
49183fea79
|
@ -8,6 +8,7 @@ import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.api.exceptions.WrongCustomHolidayFormatException;
|
||||||
import pro.taskana.common.internal.util.CheckedFunction;
|
import pro.taskana.common.internal.util.CheckedFunction;
|
||||||
import pro.taskana.common.internal.util.Pair;
|
import pro.taskana.common.internal.util.Pair;
|
||||||
|
import pro.taskana.common.internal.util.ReflectionUtil;
|
||||||
|
|
||||||
public class TaskanaConfigurationInitializer {
|
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_CUSTOM_HOLIDAY_DAY_MONTH_SEPARATOR = ".";
|
||||||
private static final String TASKANA_CLASSIFICATION_CATEGORIES_PROPERTY =
|
private static final String TASKANA_CLASSIFICATION_CATEGORIES_PROPERTY =
|
||||||
"taskana.classification.categories";
|
"taskana.classification.categories";
|
||||||
|
private static final Map<Class<?>, 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() {
|
private TaskanaConfigurationInitializer() {
|
||||||
throw new IllegalStateException("utility class");
|
throw new IllegalStateException("utility class");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> Optional<T> parseProperty(
|
public static void configureAnnotatedFields(Object instance, String separator, Properties props) {
|
||||||
Properties props, String key, CheckedFunction<String, T, Exception> function) {
|
final List<Field> fields = ReflectionUtil.retrieveAllFields(instance.getClass());
|
||||||
String property = props.getProperty(key, "");
|
for (Field field : fields) {
|
||||||
if (!property.isEmpty()) {
|
Optional.ofNullable(field.getAnnotation(TaskanaProperty.class))
|
||||||
try {
|
.ifPresent(
|
||||||
return Optional.ofNullable(function.apply(property));
|
taskanaProperty -> {
|
||||||
} catch (Throwable t) {
|
Class<?> type = ReflectionUtil.wrap(field.getType());
|
||||||
LOGGER.warn(
|
PropertyParser<?> propertyParser =
|
||||||
"Could not parse property {} ({}). Using default. Exception: {}",
|
Optional.ofNullable(PROPERTY_INITIALIZER_BY_CLASS.get(type))
|
||||||
key,
|
.orElseThrow(
|
||||||
property,
|
() -> new SystemException("Unknown configuration type " + type));
|
||||||
t.getMessage());
|
propertyParser
|
||||||
}
|
.initialize(props, separator, field, taskanaProperty)
|
||||||
|
.ifPresent(value -> setFieldValue(instance, field, value));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, List<String>> configureClassificationCategoriesForType(
|
public static Map<String, List<String>> configureClassificationCategoriesForType(
|
||||||
|
@ -89,85 +103,6 @@ public class TaskanaConfigurationInitializer {
|
||||||
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
|
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Field> getAllFields(List<Field> 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<Field> 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<String, List<CustomHoliday>, 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<String, List<String>, 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<String> splitStringAndTrimElements(String str, String separator) {
|
static List<String> splitStringAndTrimElements(String str, String separator) {
|
||||||
return splitStringAndTrimElements(str, separator, UnaryOperator.identity());
|
return splitStringAndTrimElements(str, separator, UnaryOperator.identity());
|
||||||
}
|
}
|
||||||
|
@ -211,4 +146,98 @@ public class TaskanaConfigurationInitializer {
|
||||||
"Property value " + value + " is invalid for field " + field.getName(), e);
|
"Property value " + value + " is invalid for field " + field.getName(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static <T> Optional<T> parseProperty(
|
||||||
|
Properties props, String key, CheckedFunction<String, T, Exception> 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<T> {
|
||||||
|
Optional<T> initialize(
|
||||||
|
Properties properties, String separator, Field field, TaskanaProperty taskanaProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ListPropertyParser implements PropertyParser<List<?>> {
|
||||||
|
@Override
|
||||||
|
public Optional<List<?>> 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<String, List<?>, 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<String, List<?>, Exception> parseListFunction =
|
||||||
|
p -> splitStringAndTrimElements(p, ",", String::toUpperCase);
|
||||||
|
return parseProperty(properties, taskanaProperty.value(), parseListFunction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class InstantPropertyParser implements PropertyParser<Instant> {
|
||||||
|
@Override
|
||||||
|
public Optional<Instant> initialize(
|
||||||
|
Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) {
|
||||||
|
return parseProperty(properties, taskanaProperty.value(), Instant::parse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DurationPropertyParser implements PropertyParser<Duration> {
|
||||||
|
@Override
|
||||||
|
public Optional<Duration> initialize(
|
||||||
|
Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) {
|
||||||
|
return parseProperty(properties, taskanaProperty.value(), Duration::parse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StringPropertyParser implements PropertyParser<String> {
|
||||||
|
@Override
|
||||||
|
public Optional<String> initialize(
|
||||||
|
Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) {
|
||||||
|
return parseProperty(properties, taskanaProperty.value(), String::new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class IntegerPropertyParser implements PropertyParser<Integer> {
|
||||||
|
@Override
|
||||||
|
public Optional<Integer> initialize(
|
||||||
|
Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) {
|
||||||
|
return parseProperty(properties, taskanaProperty.value(), Integer::parseInt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class BooleanPropertyParser implements PropertyParser<Boolean> {
|
||||||
|
@Override
|
||||||
|
public Optional<Boolean> initialize(
|
||||||
|
Properties properties, String separator, Field field, TaskanaProperty taskanaProperty) {
|
||||||
|
return parseProperty(properties, taskanaProperty.value(), Boolean::parseBoolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ package pro.taskana.common.internal.util;
|
||||||
import static pro.taskana.common.internal.util.CheckedFunction.wrap;
|
import static pro.taskana.common.internal.util.CheckedFunction.wrap;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -51,7 +49,7 @@ public class ObjectAttributeChangeDetector {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<JSONObject> changedAttributes =
|
List<JSONObject> changedAttributes =
|
||||||
retrieveAllFields(objectClass).stream()
|
ReflectionUtil.retrieveAllFields(objectClass).stream()
|
||||||
.peek(field -> field.setAccessible(true))
|
.peek(field -> field.setAccessible(true))
|
||||||
.filter(field -> !"customAttributes".equals(field.getName()))
|
.filter(field -> !"customAttributes".equals(field.getName()))
|
||||||
.map(wrap(field -> Triplet.of(field, field.get(oldObject), field.get(newObject))))
|
.map(wrap(field -> Triplet.of(field, field.get(oldObject), field.get(newObject))))
|
||||||
|
@ -75,15 +73,6 @@ public class ObjectAttributeChangeDetector {
|
||||||
return changedAttribute;
|
return changedAttribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Field> retrieveAllFields(Class<?> currentClass) {
|
|
||||||
List<Field> fields = new ArrayList<>();
|
|
||||||
while (currentClass.getSuperclass() != null) {
|
|
||||||
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
|
|
||||||
currentClass = currentClass.getSuperclass();
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> String compareLists(T oldObject, T newObject) {
|
private static <T> String compareLists(T oldObject, T newObject) {
|
||||||
if (oldObject.equals(newObject)) {
|
if (oldObject.equals(newObject)) {
|
||||||
return "";
|
return "";
|
||||||
|
|
|
@ -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<?>, 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<Field> retrieveAllFields(Class<?> currentClass) {
|
||||||
|
List<Field> 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<Long>
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> Class<T> wrap(Class<T> c) {
|
||||||
|
return c.isPrimitive() ? (Class<T>) PRIMITIVES_TO_WRAPPERS.get(c) : c;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Field> fields = ReflectionUtil.retrieveAllFields(SubSubTestClass.class);
|
||||||
|
|
||||||
|
assertThat(fields)
|
||||||
|
.extracting(Field::getName)
|
||||||
|
.containsExactlyInAnyOrder("fieldA", "fieldB", "fieldC");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_WrapPrimitiveClassToItsWrapperClass() {
|
||||||
|
Class<Integer> wrap = ReflectionUtil.wrap(int.class);
|
||||||
|
|
||||||
|
assertThat(wrap).isEqualTo(Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void should_NotWrapNonPrimitiveClass() {
|
||||||
|
Class<TestClass> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,21 +63,17 @@ public class TaskanaEngineConfiguration {
|
||||||
// authorizations
|
// authorizations
|
||||||
protected boolean securityEnabled;
|
protected boolean securityEnabled;
|
||||||
protected boolean useManagedTransactions;
|
protected boolean useManagedTransactions;
|
||||||
|
|
||||||
@TaskanaProperty("taskana.custom.holidays")
|
|
||||||
private List<CustomHoliday> customHolidays = new ArrayList<>();
|
|
||||||
|
|
||||||
// List of configured domain names
|
// List of configured domain names
|
||||||
@TaskanaProperty("taskana.domains")
|
@TaskanaProperty("taskana.domains")
|
||||||
protected List<String> domains = new ArrayList<>();
|
protected List<String> domains = new ArrayList<>();
|
||||||
|
|
||||||
// List of configured classification types
|
// List of configured classification types
|
||||||
@TaskanaProperty("taskana.classification.types")
|
@TaskanaProperty("taskana.classification.types")
|
||||||
protected List<String> classificationTypes = new ArrayList<>();
|
protected List<String> classificationTypes = new ArrayList<>();
|
||||||
|
|
||||||
@TaskanaProperty("taskana.classification.categories")
|
|
||||||
protected Map<String, List<String>> classificationCategoriesByTypeMap = new HashMap<>();
|
protected Map<String, List<String>> classificationCategoriesByTypeMap = new HashMap<>();
|
||||||
|
|
||||||
|
@TaskanaProperty("taskana.custom.holidays")
|
||||||
|
private List<CustomHoliday> customHolidays = new ArrayList<>();
|
||||||
// Properties for the monitor
|
// Properties for the monitor
|
||||||
@TaskanaProperty("taskana.history.deletion.on.task.deletion.enabled")
|
@TaskanaProperty("taskana.history.deletion.on.task.deletion.enabled")
|
||||||
private boolean deleteHistoryOnTaskDeletionEnabled;
|
private boolean deleteHistoryOnTaskDeletionEnabled;
|
||||||
|
@ -111,7 +107,7 @@ public class TaskanaEngineConfiguration {
|
||||||
|
|
||||||
@TaskanaProperty("taskana.validation.allowTimestampServiceLevelMismatch")
|
@TaskanaProperty("taskana.validation.allowTimestampServiceLevelMismatch")
|
||||||
private boolean validationAllowTimestampServiceLevelMismatch = false;
|
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")
|
@TaskanaProperty("taskana.addAdditionalUserInfo")
|
||||||
private boolean addAdditionalUserInfo = false;
|
private boolean addAdditionalUserInfo = false;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue