Skip to content

Commit fbf8780

Browse files
committed
✨ Added registry for Equality check: consider java.util.Date to equal
1 parent 445a46f commit fbf8780

7 files changed

Lines changed: 335 additions & 1 deletion

File tree

sormas-backend/src/main/java/de/symeda/sormas/backend/patch/DataPatcherImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import de.symeda.sormas.backend.common.ConfigFacadeEjb;
3131
import de.symeda.sormas.backend.feature.FeatureConfigurationFacadeEjb;
3232
import de.symeda.sormas.backend.json.ObjectMapperProvider;
33+
import de.symeda.sormas.backend.patch.mapping.EqualityCheckerRegistry;
3334
import de.symeda.sormas.backend.patch.mapping.FieldCustomMapperRegistry;
3435
import de.symeda.sormas.backend.patch.mapping.GroupedFieldMapperRegistry;
3536
import de.symeda.sormas.backend.patch.mapping.ValueMapperRegistry;
@@ -54,6 +55,9 @@ public class DataPatcherImpl implements DataPatcher {
5455
@Inject
5556
private GroupedFieldMapperRegistry groupedFieldMapperRegistry;
5657

58+
@Inject
59+
private EqualityCheckerRegistry equalityCheckerRegistry;
60+
5761
@Inject
5862
private BusinessDtoFacade businessDtoFacade;
5963

@@ -71,13 +75,15 @@ public DataPatcherImpl(
7175
ValueMapperRegistry valueMapperRegistry,
7276
FieldCustomMapperRegistry fieldCustomMapperRegistry,
7377
GroupedFieldMapperRegistry groupedFieldMapperRegistry,
78+
EqualityCheckerRegistry equalityCheckerRegistry,
7479
BusinessDtoFacade businessDtoFacade,
7580
FeatureConfigurationFacadeEjb.FeatureConfigurationFacadeEjbLocal featureConfigurationFacade,
7681
ConfigFacadeEjb.ConfigFacadeEjbLocal configFacade) {
7782
this.patchFieldHelper = patchFieldHelper;
7883
this.valueMapperRegistry = valueMapperRegistry;
7984
this.fieldCustomMapperRegistry = fieldCustomMapperRegistry;
8085
this.groupedFieldMapperRegistry = groupedFieldMapperRegistry;
86+
this.equalityCheckerRegistry = equalityCheckerRegistry;
8187
this.businessDtoFacade = businessDtoFacade;
8288
this.featureConfigurationFacade = featureConfigurationFacade;
8389
this.configFacade = configFacade;
@@ -262,7 +268,7 @@ private boolean anyFieldPatchedWithPrefix(Map<String, Object> validPatchDictiona
262268
if (nestedPropertyValue.isPresent()) {
263269
Object currentValue = nestedPropertyValue.orElseThrow();
264270

265-
if (!currentValue.equals(typedValue)) {
271+
if (!equalityCheckerRegistry.areEqual(currentValue, typedValue)) {
266272
return singlePatchResult.setFailure(
267273
new DataPatchFailure().setDataPatchFailureCause(DataPatchFailureCause.FORBIDDEN_VALUE_OVERRIDE)
268274
.setExistingFieldValue(currentValue)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package de.symeda.sormas.backend.patch.mapping;
2+
3+
import de.symeda.sormas.api.utils.OrderedRegisterable;
4+
5+
/**
6+
* Contract to specify how two values of a supported type should be compared for equality.
7+
*/
8+
public interface EqualityChecker extends OrderedRegisterable<EqualityChecker> {
9+
10+
/**
11+
* @param a
12+
* first value — guaranteed non-null by the registry
13+
* @param b
14+
* second value — guaranteed non-null by the registry
15+
* @return true if the two values are considered equal for patch purposes
16+
*/
17+
boolean areEqual(Object a, Object b);
18+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package de.symeda.sormas.backend.patch.mapping;
2+
3+
import java.util.List;
4+
import java.util.stream.Collectors;
5+
6+
import javax.annotation.PostConstruct;
7+
import javax.enterprise.context.ApplicationScoped;
8+
import javax.enterprise.inject.Instance;
9+
import javax.inject.Inject;
10+
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
@ApplicationScoped
15+
public class EqualityCheckerRegistry {
16+
17+
private final static Logger logger = LoggerFactory.getLogger(EqualityCheckerRegistry.class);
18+
19+
private List<EqualityChecker> orderedInstances;
20+
21+
@Inject
22+
private Instance<EqualityChecker> instances;
23+
24+
public EqualityCheckerRegistry() {
25+
}
26+
27+
public EqualityCheckerRegistry(Instance<EqualityChecker> instances) {
28+
this.instances = instances;
29+
}
30+
31+
@PostConstruct
32+
void init() {
33+
orderedInstances = instances.stream().sorted().collect(Collectors.toList());
34+
}
35+
36+
public boolean areEqual(Object a, Object b) {
37+
if (a == null && b == null) {
38+
return true;
39+
}
40+
if (a == null || b == null) {
41+
return false;
42+
}
43+
44+
Class<?> type = a.getClass();
45+
EqualityChecker checker = orderedInstances.stream()
46+
.filter(c -> c.supports(type))
47+
.findFirst()
48+
.orElseThrow(
49+
() -> new IllegalStateException(
50+
String.format(
51+
"No equality checker found for: [%s]. Must not occur, ObjectEqualityChecker handles Object. Registered checkers: [%s]",
52+
type,
53+
orderedInstances)));
54+
55+
logger.debug("Values [{}] and [{}] will be compared with checker: [{}]", a, b, checker);
56+
return checker.areEqual(a, b);
57+
}
58+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package de.symeda.sormas.backend.patch.mapping.impl.equalitychecker;
2+
3+
import java.time.ZoneId;
4+
import java.util.Date;
5+
import java.util.Objects;
6+
import java.util.Set;
7+
8+
import javax.enterprise.context.ApplicationScoped;
9+
10+
import de.symeda.sormas.backend.patch.mapping.EqualityChecker;
11+
12+
@ApplicationScoped
13+
public class DateEqualityChecker implements EqualityChecker {
14+
15+
public static final Set<Class<?>> SUPPORTED_TYPES = Set.of(Date.class);
16+
17+
@Override
18+
public boolean areEqual(Object a, Object b) {
19+
return toLocalDate((Date) a).equals(toLocalDate((Date) b));
20+
}
21+
22+
@Override
23+
public Set<Class<?>> getSupportedTypes() {
24+
return SUPPORTED_TYPES;
25+
}
26+
27+
@Override
28+
public int getOrder() {
29+
return HIGH_PRECEDENCE;
30+
}
31+
32+
private java.time.LocalDate toLocalDate(Date date) {
33+
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
34+
}
35+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package de.symeda.sormas.backend.patch.mapping.impl.equalitychecker;
2+
3+
import java.util.Objects;
4+
import java.util.Set;
5+
6+
import javax.enterprise.context.ApplicationScoped;
7+
8+
import de.symeda.sormas.backend.patch.mapping.EqualityChecker;
9+
10+
@ApplicationScoped
11+
public class ObjectEqualityChecker implements EqualityChecker {
12+
13+
public static final Set<Class<?>> SUPPORTED_TYPES = Set.of(Object.class);
14+
15+
@Override
16+
public boolean areEqual(Object a, Object b) {
17+
return Objects.equals(a, b);
18+
}
19+
20+
@Override
21+
public Set<Class<?>> getSupportedTypes() {
22+
return SUPPORTED_TYPES;
23+
}
24+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package de.symeda.sormas.backend.patch.mapping.impl.equalitychecker;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.time.LocalDate;
8+
import java.time.LocalDateTime;
9+
import java.time.ZoneId;
10+
import java.util.Date;
11+
import java.util.Set;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.mockito.InjectMocks;
15+
16+
import de.symeda.sormas.api.utils.OrderedRegisterable;
17+
import de.symeda.sormas.backend.AbstractUnitTest;
18+
19+
class DateEqualityCheckerTest extends AbstractUnitTest {
20+
21+
@InjectMocks
22+
private DateEqualityChecker victim;
23+
24+
@Test
25+
void getSupportedTypes_containsDateClass() {
26+
// PREPARE
27+
Set<Class<?>> expected = Set.of(Date.class);
28+
29+
// EXECUTE
30+
Set<Class<?>> actual = victim.getSupportedTypes();
31+
32+
// CHECK
33+
assertEquals(expected, actual);
34+
}
35+
36+
@Test
37+
void getOrder_isHighPrecedence() {
38+
assertEquals(OrderedRegisterable.HIGH_PRECEDENCE, victim.getOrder());
39+
}
40+
41+
@Test
42+
void areEqual_sameDateInstance_returnsTrue() {
43+
// PREPARE
44+
Date date = toDate(LocalDate.of(2024, 6, 15));
45+
46+
// EXECUTE & CHECK
47+
assertTrue(victim.areEqual(date, date));
48+
}
49+
50+
@Test
51+
void areEqual_sameTimestamp_returnsTrue() {
52+
// PREPARE
53+
long millis = toDate(LocalDate.of(2024, 6, 15)).getTime();
54+
Date a = new Date(millis);
55+
Date b = new Date(millis);
56+
57+
// EXECUTE & CHECK
58+
assertTrue(victim.areEqual(a, b));
59+
}
60+
61+
@Test
62+
void areEqual_sameDayDifferentTime_returnsTrue() {
63+
// PREPARE
64+
Date morning = toDate(LocalDateTime.of(2024, 6, 15, 8, 0, 0));
65+
Date evening = toDate(LocalDateTime.of(2024, 6, 15, 23, 59, 59));
66+
67+
// EXECUTE & CHECK
68+
assertTrue(victim.areEqual(morning, evening));
69+
}
70+
71+
@Test
72+
void areEqual_differentDays_returnsFalse() {
73+
// PREPARE
74+
Date day1 = toDate(LocalDate.of(2024, 6, 15));
75+
Date day2 = toDate(LocalDate.of(2024, 6, 16));
76+
77+
// EXECUTE & CHECK
78+
assertFalse(victim.areEqual(day1, day2));
79+
}
80+
81+
@Test
82+
void areEqual_differentMonths_returnsFalse() {
83+
// PREPARE
84+
Date june = toDate(LocalDate.of(2024, 6, 15));
85+
Date july = toDate(LocalDate.of(2024, 7, 15));
86+
87+
// EXECUTE & CHECK
88+
assertFalse(victim.areEqual(june, july));
89+
}
90+
91+
@Test
92+
void areEqual_differentYears_returnsFalse() {
93+
// PREPARE
94+
Date year2023 = toDate(LocalDate.of(2023, 6, 15));
95+
Date year2024 = toDate(LocalDate.of(2024, 6, 15));
96+
97+
// EXECUTE & CHECK
98+
assertFalse(victim.areEqual(year2023, year2024));
99+
}
100+
101+
@Test
102+
void areEqual_endOfDayAndStartOfNextDay_returnsFalse() {
103+
// PREPARE
104+
Date endOfDay = toDate(LocalDateTime.of(2024, 6, 15, 23, 59, 59));
105+
Date startOfNextDay = toDate(LocalDateTime.of(2024, 6, 16, 0, 0, 0));
106+
107+
// EXECUTE & CHECK
108+
assertFalse(victim.areEqual(endOfDay, startOfNextDay));
109+
}
110+
111+
private static Date toDate(LocalDate localDate) {
112+
return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
113+
}
114+
115+
private static Date toDate(LocalDateTime localDateTime) {
116+
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
117+
}
118+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package de.symeda.sormas.backend.patch.mapping.impl.equalitychecker;
2+
3+
import static de.symeda.sormas.api.utils.OrderedRegisterable.LOW_PRECEDENCE;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.util.Set;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.mockito.InjectMocks;
12+
13+
import de.symeda.sormas.backend.AbstractUnitTest;
14+
15+
class ObjectEqualityCheckerTest extends AbstractUnitTest {
16+
17+
@InjectMocks
18+
private ObjectEqualityChecker victim;
19+
20+
@Test
21+
void getSupportedTypes_containsObjectClass() {
22+
// PREPARE
23+
Set<Class<?>> expected = Set.of(Object.class);
24+
25+
// EXECUTE
26+
Set<Class<?>> actual = victim.getSupportedTypes();
27+
28+
// CHECK
29+
assertEquals(expected, actual);
30+
}
31+
32+
@Test
33+
void getOrder_isLowPrecedence() {
34+
assertEquals(LOW_PRECEDENCE, victim.getOrder());
35+
}
36+
37+
@Test
38+
void areEqual_sameStringInstance_returnsTrue() {
39+
// PREPARE
40+
String value = "hello";
41+
42+
// EXECUTE & CHECK
43+
assertTrue(victim.areEqual(value, value));
44+
}
45+
46+
@Test
47+
void areEqual_equalStrings_returnsTrue() {
48+
// EXECUTE & CHECK
49+
assertTrue(victim.areEqual("hello", "hello"));
50+
}
51+
52+
@Test
53+
void areEqual_differentStrings_returnsFalse() {
54+
// EXECUTE & CHECK
55+
assertFalse(victim.areEqual("hello", "world"));
56+
}
57+
58+
@Test
59+
void areEqual_equalIntegers_returnsTrue() {
60+
// EXECUTE & CHECK
61+
assertTrue(victim.areEqual(42, 42));
62+
}
63+
64+
@Test
65+
void areEqual_differentIntegers_returnsFalse() {
66+
// EXECUTE & CHECK
67+
assertFalse(victim.areEqual(1, 2));
68+
}
69+
70+
@Test
71+
void areEqual_differentTypes_returnsFalse() {
72+
// EXECUTE & CHECK
73+
assertFalse(victim.areEqual("42", 42));
74+
}
75+
}

0 commit comments

Comments
 (0)