implement valid family check
This commit is contained in:
@@ -9,6 +9,7 @@ import lombok.*;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -35,16 +36,26 @@ public class Person {
|
||||
private Integer partnerId;
|
||||
|
||||
@NonNull
|
||||
@Singular
|
||||
@Builder.Default
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
private Set<Integer> childIds = new HashSet<>();
|
||||
|
||||
@NonNull
|
||||
@Singular
|
||||
@Builder.Default
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
private Set<Integer> parentIds = new HashSet<>();
|
||||
|
||||
public Person(int 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);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> getPerson(
|
||||
@PathVariable int id
|
||||
) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<?> deletePeople(
|
||||
@RequestBody List<Integer> ids
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.spijkerman.ivo.threekidfamily.domain.person;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.Locked;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -9,14 +9,33 @@ import lombok.val;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PersonService {
|
||||
|
||||
public static final int MAX_RELATED_ASSUMPTION = 1000;
|
||||
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.
|
||||
*
|
||||
@@ -45,21 +64,40 @@ public class PersonService {
|
||||
}
|
||||
if (!modifiedPersons.isEmpty()) {
|
||||
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) {
|
||||
val personsToConsider = Lists.newArrayList(newPerson);
|
||||
val oldPerson = personRepository.findById(newPerson.id());
|
||||
oldPerson.ifPresent(personsToConsider::add);
|
||||
val oldPerson = personRepository.findById(newPerson.id()).orElse(null);
|
||||
return oldPerson == null || newPerson.equals(oldPerson) ?
|
||||
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>();
|
||||
for (val person : personsToConsider) {
|
||||
if (person.partnerId() != null) {
|
||||
ids.add(person.partnerId());
|
||||
for (val person : persons) {
|
||||
ids.addAll(person.relatedIds());
|
||||
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>();
|
||||
@@ -72,11 +110,7 @@ public class PersonService {
|
||||
for (val missingId : missingIds) {
|
||||
related.put(missingId, new Person(missingId));
|
||||
}
|
||||
|
||||
return Pair.of(
|
||||
oldPerson.orElse(null),
|
||||
Collections.unmodifiableMap(related)
|
||||
);
|
||||
return Collections.unmodifiableMap(related);
|
||||
}
|
||||
|
||||
private Set<Person> modifiedPersonSetOf(
|
||||
@@ -156,18 +190,39 @@ public class PersonService {
|
||||
|
||||
@Locked.Read
|
||||
public SortedSet<Person> getParentsInValidFamilies() {
|
||||
throw new UnsupportedOperationException();
|
||||
return Collections.unmodifiableSortedSet(parentsInValidFamiliesCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all known data related to that specific user.
|
||||
*
|
||||
* @param id The ID of the person to be retrieved.
|
||||
* @return A Person object, may contain all information, or just the ID.
|
||||
*/
|
||||
@Locked.Read
|
||||
public Person getPersonById(int id) {
|
||||
throw new UnsupportedOperationException();
|
||||
private Set<Person> filterValid(Set<Person> candidates) {
|
||||
val related = retrieveRelatedOf(candidates);
|
||||
return candidates.stream()
|
||||
.filter(candidate -> isValid(candidate, related))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean isValid(Person person, Map<Integer, Person> related) {
|
||||
// 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 java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public record PersonUpsertRequest(
|
||||
@@ -22,14 +24,25 @@ public record PersonUpsertRequest(
|
||||
var builder = Person.builder()
|
||||
.id(id)
|
||||
.name(name)
|
||||
.parentId(idOf(parent1))
|
||||
.parentId(idOf(parent2))
|
||||
.partnerId(idOf(partner));
|
||||
|
||||
val childIds = new HashSet<Integer>();
|
||||
if (children != null) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.mockito.Mockito;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class PersonServiceTest {
|
||||
@@ -23,8 +24,8 @@ class PersonServiceTest {
|
||||
.id(1)
|
||||
.name("Alice")
|
||||
.birthDate(LocalDate.now())
|
||||
.parentId(2)
|
||||
.childId(10)
|
||||
.parentIds(Set.of(2))
|
||||
.childIds(Set.of(10))
|
||||
.partnerId(20)
|
||||
.build();
|
||||
|
||||
@@ -34,10 +35,11 @@ class PersonServiceTest {
|
||||
verify(repository).findAllById(Set.of(2, 10, 20));
|
||||
verify(repository).saveAll(Set.of(
|
||||
given, // Alice
|
||||
Person.builder().id(2).childId(1).build(), // Parent
|
||||
Person.builder().id(10).parentId(1).build(), // Child
|
||||
Person.builder().id(2).childIds(Set.of(1)).build(), // Parent
|
||||
Person.builder().id(10).parentIds(Set.of(1)).build(), // Child
|
||||
Person.builder().id(20).partnerId(1).build() // Partner
|
||||
));
|
||||
verify(repository).findAllById(Set.of(1, 2, 10, 20)); // For cache update
|
||||
verifyNoMoreInteractions(repository);
|
||||
}
|
||||
|
||||
@@ -50,8 +52,8 @@ class PersonServiceTest {
|
||||
.id(1)
|
||||
.name("Alice")
|
||||
.birthDate(LocalDate.now())
|
||||
.parentId(2)
|
||||
.childId(10)
|
||||
.parentIds(Set.of(2))
|
||||
.childIds(Set.of(10))
|
||||
.partnerId(20)
|
||||
.build();
|
||||
|
||||
@@ -96,6 +98,7 @@ class PersonServiceTest {
|
||||
verify(repository).findAllById(Set.of(1));
|
||||
verify(repository).findAllById(Set.of(2));
|
||||
verify(repository).saveAll(Set.of(alice, bob));
|
||||
verify(repository).findAllById(Set.of(1, 2)); // For cache update
|
||||
verifyNoMoreInteractions(repository);
|
||||
}
|
||||
|
||||
@@ -129,4 +132,46 @@ class PersonServiceTest {
|
||||
|
||||
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