refactor(documents): bundle density filters into a record (#385)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-08 11:42:38 +02:00
parent 00f35ab675
commit 86de118d63
6 changed files with 63 additions and 38 deletions

View File

@@ -0,0 +1,23 @@
package org.raddatz.familienarchiv.document;
import org.raddatz.familienarchiv.tag.TagOperator;
import java.util.List;
import java.util.UUID;
/**
* The non-date filters honoured by {@link DocumentService#getDensity(DensityFilters)}.
* Date bounds (from/to) are deliberately excluded — see the service Javadoc for why.
*
* Kept as a record so the seven values are passed as one named bundle instead of a
* positional argument list where two UUIDs (sender vs. receiver) can be swapped by
* accident at the call site.
*/
public record DensityFilters(
String text,
UUID sender,
UUID receiver,
List<String> tags,
String tagQ,
DocumentStatus status,
TagOperator tagOperator) {}

View File

@@ -401,7 +401,7 @@ public class DocumentController {
@Parameter(description = "Tag operator: AND (default) or OR") @RequestParam(required = false) String tagOp) {
TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND;
DocumentDensityResult result = documentService.getDensity(
q, senderId, receiverId, tags, tagQ, status, operator);
new DensityFilters(q, senderId, receiverId, tags, tagQ, status, operator));
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePrivate())
.body(result);

View File

@@ -146,10 +146,8 @@ public class DocumentService {
* 'YYYY-MM')) and accept that the criteria/specification surface needs a
* parallel native-query path.
*/
public DocumentDensityResult getDensity(
String text, UUID sender, UUID receiver,
List<String> tags, String tagQ,
DocumentStatus status, TagOperator tagOperator) {
public DocumentDensityResult getDensity(DensityFilters filters) {
String text = filters.text();
boolean hasText = StringUtils.hasText(text);
List<UUID> rankedIds = null;
if (hasText) {
@@ -160,7 +158,9 @@ public class DocumentService {
}
Specification<Document> spec = buildSearchSpec(
hasText, rankedIds, null, null,
sender, receiver, tags, tagQ, status, tagOperator);
filters.sender(), filters.receiver(),
filters.tags(), filters.tagQ(),
filters.status(), filters.tagOperator());
List<LocalDate> dates = documentRepository.findAll(spec).stream()
.map(Document::getDocumentDate)

View File

@@ -1253,7 +1253,7 @@ class DocumentControllerTest {
@Test
@WithMockUser
void density_returns200_withResultBody_whenAuthenticated() throws Exception {
when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn(
when(documentService.getDensity(any())).thenReturn(
new DocumentDensityResult(
List.of(new MonthBucket("1915-08", 2), new MonthBucket("1915-09", 1)),
java.time.LocalDate.of(1915, 8, 3),
@@ -1278,7 +1278,7 @@ class DocumentControllerTest {
@Test
@WithMockUser
void density_declaresApplicationJsonContentType() throws Exception {
when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn(
when(documentService.getDensity(any())).thenReturn(
new DocumentDensityResult(List.of(), null, null));
mockMvc.perform(get("/api/documents/density")
@@ -1289,7 +1289,7 @@ class DocumentControllerTest {
@Test
@WithMockUser
void density_emitsPrivateCacheControlHeader() throws Exception {
when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn(
when(documentService.getDensity(any())).thenReturn(
new DocumentDensityResult(List.of(), null, null));
mockMvc.perform(get("/api/documents/density"))
@@ -1303,7 +1303,7 @@ class DocumentControllerTest {
@Test
@WithMockUser
void density_forwardsSenderAndTagFilters() throws Exception {
when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn(
when(documentService.getDensity(any())).thenReturn(
new DocumentDensityResult(List.of(), null, null));
UUID senderId = UUID.randomUUID();
@@ -1314,20 +1314,17 @@ class DocumentControllerTest {
.param("tagOp", "OR"))
.andExpect(status().isOk());
verify(documentService).getDensity(
org.mockito.ArgumentMatchers.isNull(),
eq(senderId),
org.mockito.ArgumentMatchers.isNull(),
eq(List.of("Familie", "Urlaub")),
org.mockito.ArgumentMatchers.isNull(),
org.mockito.ArgumentMatchers.isNull(),
eq(org.raddatz.familienarchiv.tag.TagOperator.OR));
verify(documentService).getDensity(eq(new DensityFilters(
null, senderId, null,
List.of("Familie", "Urlaub"),
null, null,
org.raddatz.familienarchiv.tag.TagOperator.OR)));
}
@Test
@WithMockUser
void density_forwardsStatusAndQueryText() throws Exception {
when(documentService.getDensity(any(), any(), any(), any(), any(), any(), any())).thenReturn(
when(documentService.getDensity(any())).thenReturn(
new DocumentDensityResult(List.of(), null, null));
mockMvc.perform(get("/api/documents/density")
@@ -1335,13 +1332,9 @@ class DocumentControllerTest {
.param("status", "REVIEWED"))
.andExpect(status().isOk());
verify(documentService).getDensity(
eq("Brief"),
org.mockito.ArgumentMatchers.isNull(),
org.mockito.ArgumentMatchers.isNull(),
org.mockito.ArgumentMatchers.isNull(),
org.mockito.ArgumentMatchers.isNull(),
eq(DocumentStatus.REVIEWED),
eq(org.raddatz.familienarchiv.tag.TagOperator.AND));
verify(documentService).getDensity(eq(new DensityFilters(
"Brief", null, null, null, null,
DocumentStatus.REVIEWED,
org.raddatz.familienarchiv.tag.TagOperator.AND)));
}
}

View File

@@ -56,13 +56,17 @@ class DocumentDensityIntegrationTest {
urlaubTag = tagRepository.save(Tag.builder().name("Urlaub").build());
}
private static DensityFilters noFilters() {
return new DensityFilters(null, null, null, null, null, null, null);
}
@Test
void getDensity_returnsAllMonths_whenNoFiltersApplied() {
save("a", LocalDate.of(1915, 8, 3), null, Set.of());
save("b", LocalDate.of(1915, 8, 17), null, Set.of());
save("c", LocalDate.of(1915, 9, 1), null, Set.of());
DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null);
DocumentDensityResult result = documentService.getDensity(noFilters());
assertThat(result.buckets()).extracting(MonthBucket::month)
.containsExactly("1915-08", "1915-09");
@@ -78,7 +82,7 @@ class DocumentDensityIntegrationTest {
save("c", LocalDate.of(1920, 5, 1), anna, Set.of());
DocumentDensityResult result = documentService.getDensity(
null, hans.getId(), null, null, null, null, null);
new DensityFilters(null, hans.getId(), null, null, null, null, null));
assertThat(result.buckets()).extracting(MonthBucket::month)
.containsExactly("1915-08", "1916-01");
@@ -91,7 +95,7 @@ class DocumentDensityIntegrationTest {
save("b", LocalDate.of(1920, 5, 1), null, Set.of(urlaubTag));
DocumentDensityResult result = documentService.getDensity(
null, null, null, List.of("Familie"), null, null, TagOperator.AND);
new DensityFilters(null, null, null, List.of("Familie"), null, null, TagOperator.AND));
assertThat(result.buckets()).extracting(MonthBucket::month).containsExactly("1915-08");
}
@@ -103,7 +107,7 @@ class DocumentDensityIntegrationTest {
save("c", LocalDate.of(1920, 5, 1), anna, Set.of(familieTag));
DocumentDensityResult result = documentService.getDensity(
null, hans.getId(), null, List.of("Familie"), null, null, TagOperator.AND);
new DensityFilters(null, hans.getId(), null, List.of("Familie"), null, null, TagOperator.AND));
assertThat(result.buckets()).extracting(MonthBucket::month).containsExactly("1915-08");
}
@@ -114,7 +118,7 @@ class DocumentDensityIntegrationTest {
save("b", LocalDate.of(1916, 1, 4), null, Set.of(), DocumentStatus.PLACEHOLDER);
DocumentDensityResult result = documentService.getDensity(
null, null, null, null, null, DocumentStatus.UPLOADED, null);
new DensityFilters(null, null, null, null, null, DocumentStatus.UPLOADED, null));
assertThat(result.buckets()).extracting(MonthBucket::month).containsExactly("1915-08");
}
@@ -124,7 +128,7 @@ class DocumentDensityIntegrationTest {
save("a", LocalDate.of(1915, 8, 3), hans, Set.of());
DocumentDensityResult result = documentService.getDensity(
null, anna.getId(), null, null, null, null, null);
new DensityFilters(null, anna.getId(), null, null, null, null, null));
assertThat(result.buckets()).isEmpty();
assertThat(result.minDate()).isNull();
@@ -136,7 +140,7 @@ class DocumentDensityIntegrationTest {
save("dated", LocalDate.of(1915, 8, 3), null, Set.of());
save("undated", null, null, Set.of());
DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null);
DocumentDensityResult result = documentService.getDensity(noFilters());
assertThat(result.buckets()).extracting(MonthBucket::count).containsExactly(1);
}

View File

@@ -2325,11 +2325,15 @@ class DocumentServiceTest {
// ─── getDensity ────────────────────────────────────────────────────────────
private static DensityFilters anyFilters() {
return new DensityFilters(null, null, null, null, null, null, null);
}
@Test
void getDensity_returnsEmptyResult_whenNoDocumentsMatch() {
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of());
DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null);
DocumentDensityResult result = documentService.getDensity(anyFilters());
assertThat(result.buckets()).isEmpty();
assertThat(result.minDate()).isNull();
@@ -2344,7 +2348,7 @@ class DocumentServiceTest {
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(a, b, c));
when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of());
DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null);
DocumentDensityResult result = documentService.getDensity(anyFilters());
assertThat(result.buckets()).extracting(MonthBucket::month)
.containsExactly("1915-08", "1915-09");
@@ -2360,7 +2364,7 @@ class DocumentServiceTest {
when(documentRepository.findAll(any(Specification.class))).thenReturn(List.of(dated, undated));
when(tagService.expandTagNamesToDescendantIdSets(any())).thenReturn(List.of());
DocumentDensityResult result = documentService.getDensity(null, null, null, null, null, null, null);
DocumentDensityResult result = documentService.getDensity(anyFilters());
assertThat(result.buckets()).extracting(MonthBucket::count).containsExactly(1);
}
@@ -2369,7 +2373,8 @@ class DocumentServiceTest {
void getDensity_shortCircuits_whenFtsReturnsNoMatches() {
when(documentRepository.findRankedIdsByFts("xyz")).thenReturn(List.of());
DocumentDensityResult result = documentService.getDensity("xyz", null, null, null, null, null, null);
DocumentDensityResult result = documentService.getDensity(
new DensityFilters("xyz", null, null, null, null, null, null));
assertThat(result.buckets()).isEmpty();
verify(documentRepository, org.mockito.Mockito.never()).findAll(any(Specification.class));