Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ public class ValuePatchRequest<T> {
@NotNull
private Class<T> targetType;

/**
* Only present when {@link #targetType} is of type: {@link java.util.Collection}.
* Gives the actual type within the collection.
*/
@Nullable
private Class<T> collectionSubType;

/**
* To be able to support I18n inputs the input languages can be passed, system locale by default.
*/
Expand Down Expand Up @@ -49,6 +56,16 @@ public ValuePatchRequest<T> setTargetType(Class<T> targetType) {
return this;
}

@Nullable
public Class<T> getCollectionSubType() {
return collectionSubType;
}

public ValuePatchRequest<T> setCollectionSubType(@Nullable Class<T> collectionSubType) {
this.collectionSubType = collectionSubType;
return this;
}

public List<Language> getInputLanguages() {
return inputLanguages;
}
Expand All @@ -75,17 +92,18 @@ public boolean equals(Object o) {
return allowFallbackValues == that.allowFallbackValues
&& Objects.equals(value, that.value)
&& Objects.equals(targetType, that.targetType)
&& Objects.equals(collectionSubType, that.collectionSubType)
&& Objects.equals(inputLanguages, that.inputLanguages);
}

@Override
public int hashCode() {
return Objects.hash(value, targetType, inputLanguages, allowFallbackValues);
return Objects.hash(value, targetType, collectionSubType, inputLanguages, allowFallbackValues);
}

@Override
public String toString() {
return "ValuePatchRequest{" + "value=" + value + ", targetType=" + targetType + ", inputLanguages=" + inputLanguages + ", allowDefaultValues="
+ allowFallbackValues + '}';
return "ValuePatchRequest{" + "value=" + value + ", targetType=" + targetType + ", collectionSubType=" + collectionSubType
+ ", inputLanguages=" + inputLanguages + ", allowFallbackValues=" + allowFallbackValues + '}';
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.symeda.sormas.backend.patch;

import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -300,9 +301,16 @@ private void saveDTOsIfAppropriate(Map<Tuple<String, Integer>, AttachedEntityWra
}
Class<?> targetType = nestedPropertyTypeTuple.getFirst();

Class<?> collectionSubType = null;
if (Collection.class.isAssignableFrom(targetType)) {
logger.info("Subtype will be computed as the targetType is a collectionType: [{}]", targetType);
collectionSubType = PropertyAccessor.getCollectionGenericType(target, relativeFieldName);
}

ValueMappingResult<?> result = valueMapperRegistry.map(
new ValuePatchRequest().setValue(untypedTargetValue)
.setTargetType(targetType)
.setCollectionSubType(collectionSubType)
.setInputLanguages(request.getInputLanguages())
.setAllowFallbackValues(request.isAllowFallbackValues()));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package de.symeda.sormas.backend.patch;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;

import javax.validation.constraints.NotNull;
Expand Down Expand Up @@ -125,6 +128,50 @@ public static Optional<Object> getNestedProperty(final Object bean, final String
}
}

public static Class<?> getCollectionGenericType(final Object bean, final String fieldName) {
if (bean == null || fieldName == null || fieldName.isEmpty()) {
return null;
}

boolean notNestedPath = fieldName.indexOf(PATH_SEPARATOR) == -1;

if (notNestedPath) {
return getCollectionGenericType(bean.getClass(), fieldName);
}

String leafPath = fieldName.substring(fieldName.lastIndexOf(PATH_SEPARATOR) + 1);

return getNestedProperty(bean, fieldName.substring(0, fieldName.lastIndexOf(PATH_SEPARATOR)))
.map(leafParent -> getCollectionGenericType(leafParent.getClass(), leafPath))
.orElse(null);
}

private static Class<?> getCollectionGenericType(final Class<?> beanClass, final String fieldName) {
Class<?> currentClass = beanClass;

while (currentClass != null) {
try {
Field field = currentClass.getDeclaredField(fieldName);
Type genericType = field.getGenericType();

if (genericType instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) genericType).getActualTypeArguments();

if (typeArguments.length > 0 && typeArguments[0] instanceof Class) {
return (Class<?>) typeArguments[0];
}
}

return null;
} catch (NoSuchFieldException e) {
currentClass = currentClass.getSuperclass();
}
}

logger.info("Could not get collection generic type for field [{}] on [{}]", fieldName, beanClass);
return null;
}

public static Optional<Exception> setNestedProperty(final Object bean, final String name, final Object value) {
try {
PropertyUtils.setNestedProperty(bean, name, value);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.symeda.sormas.backend.patch.mapping;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -54,6 +55,21 @@ public <T> ValueMappingResult<T> map(ValuePatchRequest<T> request) {
}

if (targetType.isInstance(value)) {
if (Collection.class.isAssignableFrom(targetType)) {

Class<T> collectionSubType = request.getCollectionSubType();
if (collectionSubType == null) {
logger.error("Subtype must be present, was not for: [{}]", request);
return ValueMappingResult.withCause(DataPatchFailureCause.TECHNICAL);
}

// Making sure every element is of the appropriate type; empty collections trivially pass.
boolean allElementsMatch = ((Collection<?>) value).stream().allMatch(collectionSubType::isInstance);
if (!allElementsMatch) {
return ValueMappingResult.withCause(DataPatchFailureCause.INVALID_VALUE_TYPE);
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
return ValueMappingResult.withData((T) targetType.cast(value));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package de.symeda.sormas.backend.patch.mapping.impl.valuemapper;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import javax.enterprise.context.ApplicationScoped;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.symeda.sormas.api.patch.DataPatchFailureCause;
import de.symeda.sormas.api.patch.mapping.ValueMappingResult;
import de.symeda.sormas.api.patch.mapping.ValuePatchMapper;
import de.symeda.sormas.api.patch.mapping.ValuePatchRequest;
import de.symeda.sormas.backend.patch.mapping.ValueMapperRegistry;
import de.symeda.sormas.backend.util.InstanceProvider;

/**
* Edge-case handling: field which is a collection of a singular values: Set/List.
* Calls the registry (where it's also contained!) to allow to map to every supported type.
*/
@ApplicationScoped
public class CollectionPatchMapper implements ValuePatchMapper {

private static final Logger logger = LoggerFactory.getLogger(CollectionPatchMapper.class);

public static final String SEPARATOR = ",";

public static final Set<Class<?>> SUPPORTED_TYPES = Set.of(Set.class, List.class);

public static final Map<Class<?>, Collector<?, ?, ?>> COLLECTOR_DICTIONARY =
Map.of(Set.class, Collectors.toSet(), List.class, Collectors.toList());

/**
* Not injected through CDI, but lazily to avoid cycle: Registry <-> CollectionPatchMapper
*/
private ValueMapperRegistry valueMapperRegistry;

@Override
public <T> ValueMappingResult<T> map(ValuePatchRequest<T> request) {
Class<T> collectionSubType = request.getCollectionSubType();
if (collectionSubType == null) {
logger.warn("CollectionSubType is null for request: [{}]", request);
return ValueMappingResult.withCause(DataPatchFailureCause.TECHNICAL);
}

Object groupedValue = request.getValue();

if (groupedValue.getClass() != String.class) {
logger.warn(
"CollectionSubType: [{}] only supports matching type or string, split with ',' (commas). Request: [{}]",
collectionSubType,
request);
return ValueMappingResult.withCause(DataPatchFailureCause.INVALID_VALUE_TYPE);
}

List<ValueMappingResult<?>> mappingResults = Arrays.stream(((String) groupedValue).split(SEPARATOR))
.filter(StringUtils::isNotBlank)
.map(String::trim)
.map(singleValue -> getValueMapperRegistry().map(buildRequestFrom(request, singleValue)))
.collect(Collectors.toList());

if (mappingResults.isEmpty()) {
logger.warn("The input string was empty, therefore no value can be patched within the collection, this seems off: [{}]", request);
return ValueMappingResult.withData(null);
}

Optional<ValueMappingResult<?>> failureOpt = mappingResults.stream().filter(result -> result.getDataPatchFailureCause() != null).findAny();
Collector appropriateCollector = COLLECTOR_DICTIONARY.get(request.getTargetType());

return failureOpt.<ValueMappingResult<T>> map(result -> ValueMappingResult.withCause(result.getDataPatchFailureCause()))
.orElseGet(() -> ValueMappingResult.withData((T) mappingResults.stream().map(ValueMappingResult::getData).collect(appropriateCollector)));

}

private static ValuePatchRequest<?> buildRequestFrom(ValuePatchRequest<?> request, String singleValue) {
return new ValuePatchRequest().setInputLanguages(request.getInputLanguages())
.setValue(singleValue)
.setAllowFallbackValues(request.isAllowFallbackValues())
.setTargetType(request.getCollectionSubType());
}

private ValueMapperRegistry getValueMapperRegistry() {
if (valueMapperRegistry == null) {
valueMapperRegistry = InstanceProvider.getInstanceFor(ValueMapperRegistry.class);
}

return valueMapperRegistry;
}

public void setValueMapperRegistry(ValueMapperRegistry valueMapperRegistry) {
this.valueMapperRegistry = valueMapperRegistry;
}

@Override
public Set<Class<?>> getSupportedTypes() {
return SUPPORTED_TYPES;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
import de.symeda.sormas.api.patch.DataPatcher;
import de.symeda.sormas.api.patch.partial_retrieval.PartialRetriever;
import de.symeda.sormas.api.person.notifier.NotifierFacade;
import de.symeda.sormas.api.referencedata.ReferenceDataValueInstanceProvider;
import de.symeda.sormas.api.report.AggregateReportFacade;
import de.symeda.sormas.api.report.WeeklyReportFacade;
import de.symeda.sormas.api.sample.AdditionalTestFacade;
Expand Down Expand Up @@ -1165,10 +1164,6 @@ public PartialRetriever getPartialRetriever() {
return getBean(PartialRetrieverImpl.class);
}

public ReferenceDataValueInstanceProvider getReferenceDataValueInstanceProvider() {
return getBean(ReferenceDataValueInstanceProviderImpl.class);
}

/**
* The context of the {@link AbstractBeanTest} does not possess a proper (Initial)Context, therefore if you want to ma
*
Expand Down
Loading
Loading