implement valid family check
This commit is contained in:
@@ -9,6 +9,7 @@ import lombok.*;
|
|||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -35,16 +36,26 @@ public class Person {
|
|||||||
private Integer partnerId;
|
private Integer partnerId;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Singular
|
@Builder.Default
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
private Set<Integer> childIds = new HashSet<>();
|
private Set<Integer> childIds = new HashSet<>();
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Singular
|
@Builder.Default
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
private Set<Integer> parentIds = new HashSet<>();
|
private Set<Integer> parentIds = new HashSet<>();
|
||||||
|
|
||||||
public Person(int id) {
|
public Person(int id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Integer> relatedIds() {
|
||||||
|
val ids = new HashSet<Integer>();
|
||||||
|
if (partnerId() != null) {
|
||||||
|
ids.add(partnerId);
|
||||||
|
}
|
||||||
|
ids.addAll(parentIds);
|
||||||
|
ids.addAll(childIds);
|
||||||
|
return Collections.unmodifiableSet(ids);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,6 @@ public class PersonController {
|
|||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ResponseEntity<?> getPerson(
|
|
||||||
@PathVariable int id
|
|
||||||
) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping
|
@DeleteMapping
|
||||||
public ResponseEntity<?> deletePeople(
|
public ResponseEntity<?> deletePeople(
|
||||||
@RequestBody List<Integer> ids
|
@RequestBody List<Integer> ids
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.spijkerman.ivo.threekidfamily.domain.person;
|
package com.spijkerman.ivo.threekidfamily.domain.person;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import lombok.Locked;
|
import lombok.Locked;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -9,14 +9,33 @@ import lombok.val;
|
|||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PersonService {
|
public class PersonService {
|
||||||
|
|
||||||
|
public static final int MAX_RELATED_ASSUMPTION = 1000;
|
||||||
private final PersonRepository personRepository;
|
private final PersonRepository personRepository;
|
||||||
|
|
||||||
|
// Cache all the valid parents, so we do not have to recalculate all on each upsert.
|
||||||
|
private final SortedSet<Person> parentsInValidFamiliesCache = new TreeSet<>(Comparator.comparingInt(Person::id));
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void populateCache() {
|
||||||
|
// Possible overflow risk, consider streaming/paging solution here
|
||||||
|
val persons = personRepository.findAll();
|
||||||
|
val related = persons.stream().collect(Collectors.toMap(Person::id, Function.identity()));
|
||||||
|
for (val person : persons) {
|
||||||
|
if (isValid(person, related)) {
|
||||||
|
parentsInValidFamiliesCache.add(person);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upserts a person to storage, overwriting any existing or conflicting data.
|
* Upserts a person to storage, overwriting any existing or conflicting data.
|
||||||
*
|
*
|
||||||
@@ -45,21 +64,40 @@ public class PersonService {
|
|||||||
}
|
}
|
||||||
if (!modifiedPersons.isEmpty()) {
|
if (!modifiedPersons.isEmpty()) {
|
||||||
personRepository.saveAll(modifiedPersons);
|
personRepository.saveAll(modifiedPersons);
|
||||||
|
|
||||||
|
// Update valid parent cache
|
||||||
|
val validModifieds = filterValid(modifiedPersons);
|
||||||
|
parentsInValidFamiliesCache.addAll(validModifieds);
|
||||||
|
|
||||||
|
val invalidModifieds = Sets.difference(modifiedPersons, validModifieds);
|
||||||
|
parentsInValidFamiliesCache.removeAll(invalidModifieds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<Person, Map<Integer, Person>> retrieveRelatedOf(Person newPerson) {
|
private Pair<Person, Map<Integer, Person>> retrieveRelatedOf(Person newPerson) {
|
||||||
val personsToConsider = Lists.newArrayList(newPerson);
|
val oldPerson = personRepository.findById(newPerson.id()).orElse(null);
|
||||||
val oldPerson = personRepository.findById(newPerson.id());
|
return oldPerson == null || newPerson.equals(oldPerson) ?
|
||||||
oldPerson.ifPresent(personsToConsider::add);
|
Pair.of(
|
||||||
|
oldPerson,
|
||||||
|
retrieveRelatedOf(Set.of(newPerson))
|
||||||
|
) :
|
||||||
|
Pair.of(
|
||||||
|
oldPerson,
|
||||||
|
retrieveRelatedOf(Set.of(newPerson, oldPerson))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, Person> retrieveRelatedOf(Set<Person> persons) {
|
||||||
val ids = new HashSet<Integer>();
|
val ids = new HashSet<Integer>();
|
||||||
for (val person : personsToConsider) {
|
for (val person : persons) {
|
||||||
if (person.partnerId() != null) {
|
ids.addAll(person.relatedIds());
|
||||||
ids.add(person.partnerId());
|
if (ids.size() > MAX_RELATED_ASSUMPTION) {
|
||||||
|
throw new AssertionError("Assumed that the amount of related IDs will never exceed %d. Need to rethink this program.".formatted(MAX_RELATED_ASSUMPTION));
|
||||||
}
|
}
|
||||||
ids.addAll(person.parentIds());
|
}
|
||||||
ids.addAll(person.childIds());
|
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
val related = new HashMap<Integer, Person>();
|
val related = new HashMap<Integer, Person>();
|
||||||
@@ -72,11 +110,7 @@ public class PersonService {
|
|||||||
for (val missingId : missingIds) {
|
for (val missingId : missingIds) {
|
||||||
related.put(missingId, new Person(missingId));
|
related.put(missingId, new Person(missingId));
|
||||||
}
|
}
|
||||||
|
return Collections.unmodifiableMap(related);
|
||||||
return Pair.of(
|
|
||||||
oldPerson.orElse(null),
|
|
||||||
Collections.unmodifiableMap(related)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Person> modifiedPersonSetOf(
|
private Set<Person> modifiedPersonSetOf(
|
||||||
@@ -156,18 +190,39 @@ public class PersonService {
|
|||||||
|
|
||||||
@Locked.Read
|
@Locked.Read
|
||||||
public SortedSet<Person> getParentsInValidFamilies() {
|
public SortedSet<Person> getParentsInValidFamilies() {
|
||||||
throw new UnsupportedOperationException();
|
return Collections.unmodifiableSortedSet(parentsInValidFamiliesCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private Set<Person> filterValid(Set<Person> candidates) {
|
||||||
* Returns all known data related to that specific user.
|
val related = retrieveRelatedOf(candidates);
|
||||||
*
|
return candidates.stream()
|
||||||
* @param id The ID of the person to be retrieved.
|
.filter(candidate -> isValid(candidate, related))
|
||||||
* @return A Person object, may contain all information, or just the ID.
|
.collect(Collectors.toSet());
|
||||||
*/
|
}
|
||||||
@Locked.Read
|
|
||||||
public Person getPersonById(int id) {
|
private boolean isValid(Person person, Map<Integer, Person> related) {
|
||||||
throw new UnsupportedOperationException();
|
// 1. Has a partner
|
||||||
|
if (person.partnerId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 2a. Has exactly 3 children
|
||||||
|
if (person.childIds().size() != 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 2b. All 3 children have that same partner listed as mother or father
|
||||||
|
val expectedParentIds = Set.of(person.id(), person.partnerId());
|
||||||
|
val under18Date = LocalDate.now().minusYears(18);
|
||||||
|
var atLeastOneUnder18 = false;
|
||||||
|
for (val childId : person.childIds()) {
|
||||||
|
val child = related.get(childId);
|
||||||
|
if (!child.parentIds().containsAll(expectedParentIds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
atLeastOneUnder18 |= child.birthDate() != null
|
||||||
|
&& (child.birthDate().isAfter(under18Date) || child.birthDate().isEqual(under18Date));
|
||||||
|
}
|
||||||
|
// 3. At least one of those children is under 18
|
||||||
|
return atLeastOneUnder18;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import jakarta.annotation.Nullable;
|
|||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record PersonUpsertRequest(
|
public record PersonUpsertRequest(
|
||||||
@@ -22,14 +24,25 @@ public record PersonUpsertRequest(
|
|||||||
var builder = Person.builder()
|
var builder = Person.builder()
|
||||||
.id(id)
|
.id(id)
|
||||||
.name(name)
|
.name(name)
|
||||||
.parentId(idOf(parent1))
|
|
||||||
.parentId(idOf(parent2))
|
|
||||||
.partnerId(idOf(partner));
|
.partnerId(idOf(partner));
|
||||||
|
|
||||||
|
val childIds = new HashSet<Integer>();
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
for (val child : children) {
|
for (val child : children) {
|
||||||
builder = builder.childId(idOf(child));
|
childIds.add(idOf(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
builder.childIds(childIds);
|
||||||
|
|
||||||
|
val parentIds = new HashSet<Integer>();
|
||||||
|
if (parent1 != null) {
|
||||||
|
parentIds.add(idOf(parent1));
|
||||||
|
}
|
||||||
|
if (parent2 != null) {
|
||||||
|
parentIds.add(idOf(parent2));
|
||||||
|
}
|
||||||
|
builder.parentIds(parentIds);
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.mockito.Mockito;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
class PersonServiceTest {
|
class PersonServiceTest {
|
||||||
@@ -23,8 +24,8 @@ class PersonServiceTest {
|
|||||||
.id(1)
|
.id(1)
|
||||||
.name("Alice")
|
.name("Alice")
|
||||||
.birthDate(LocalDate.now())
|
.birthDate(LocalDate.now())
|
||||||
.parentId(2)
|
.parentIds(Set.of(2))
|
||||||
.childId(10)
|
.childIds(Set.of(10))
|
||||||
.partnerId(20)
|
.partnerId(20)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -34,10 +35,11 @@ class PersonServiceTest {
|
|||||||
verify(repository).findAllById(Set.of(2, 10, 20));
|
verify(repository).findAllById(Set.of(2, 10, 20));
|
||||||
verify(repository).saveAll(Set.of(
|
verify(repository).saveAll(Set.of(
|
||||||
given, // Alice
|
given, // Alice
|
||||||
Person.builder().id(2).childId(1).build(), // Parent
|
Person.builder().id(2).childIds(Set.of(1)).build(), // Parent
|
||||||
Person.builder().id(10).parentId(1).build(), // Child
|
Person.builder().id(10).parentIds(Set.of(1)).build(), // Child
|
||||||
Person.builder().id(20).partnerId(1).build() // Partner
|
Person.builder().id(20).partnerId(1).build() // Partner
|
||||||
));
|
));
|
||||||
|
verify(repository).findAllById(Set.of(1, 2, 10, 20)); // For cache update
|
||||||
verifyNoMoreInteractions(repository);
|
verifyNoMoreInteractions(repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +52,8 @@ class PersonServiceTest {
|
|||||||
.id(1)
|
.id(1)
|
||||||
.name("Alice")
|
.name("Alice")
|
||||||
.birthDate(LocalDate.now())
|
.birthDate(LocalDate.now())
|
||||||
.parentId(2)
|
.parentIds(Set.of(2))
|
||||||
.childId(10)
|
.childIds(Set.of(10))
|
||||||
.partnerId(20)
|
.partnerId(20)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -96,6 +98,7 @@ class PersonServiceTest {
|
|||||||
verify(repository).findAllById(Set.of(1));
|
verify(repository).findAllById(Set.of(1));
|
||||||
verify(repository).findAllById(Set.of(2));
|
verify(repository).findAllById(Set.of(2));
|
||||||
verify(repository).saveAll(Set.of(alice, bob));
|
verify(repository).saveAll(Set.of(alice, bob));
|
||||||
|
verify(repository).findAllById(Set.of(1, 2)); // For cache update
|
||||||
verifyNoMoreInteractions(repository);
|
verifyNoMoreInteractions(repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,4 +132,46 @@ class PersonServiceTest {
|
|||||||
|
|
||||||
return repository;
|
return repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getParentsInValidFamiliesCache() {
|
||||||
|
val repository = repository();
|
||||||
|
val sut = new PersonService(repository);
|
||||||
|
|
||||||
|
// Two parents
|
||||||
|
val mom = Person.builder()
|
||||||
|
.id(1)
|
||||||
|
.build();
|
||||||
|
val dad = Person.builder()
|
||||||
|
.id(2)
|
||||||
|
.partnerId(mom.id())
|
||||||
|
.build();
|
||||||
|
val parentIds = Set.of(mom.id(), dad.id());
|
||||||
|
// Three kids
|
||||||
|
val alice = Person.builder()
|
||||||
|
.id(11)
|
||||||
|
.birthDate(LocalDate.now().minusYears(14)) // one under 18
|
||||||
|
.parentIds(parentIds)
|
||||||
|
.build();
|
||||||
|
val bob = Person.builder()
|
||||||
|
.id(12)
|
||||||
|
.parentIds(parentIds)
|
||||||
|
.build();
|
||||||
|
val charlie = Person.builder()
|
||||||
|
.id(13)
|
||||||
|
.parentIds(parentIds)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sut.upsertPerson(mom);
|
||||||
|
sut.upsertPerson(dad);
|
||||||
|
sut.upsertPerson(alice);
|
||||||
|
sut.upsertPerson(bob);
|
||||||
|
|
||||||
|
// No valid persons yet
|
||||||
|
assertThat(sut.getParentsInValidFamilies()).isEmpty();
|
||||||
|
sut.upsertPerson(charlie);
|
||||||
|
|
||||||
|
// Both parents are valid now
|
||||||
|
assertThat(sut.getParentsInValidFamilies()).map(Person::id).containsExactly(1, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user