Skip to content

Commit 001059e

Browse files
committed
Added pagination to loading series [#2331]
1 parent 2961977 commit 001059e

File tree

21 files changed

+542
-200
lines changed

21 files changed

+542
-200
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* ComiXed - A digital comic book library management application.
3+
* Copyright (C) 2025, The ComiXed Project
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses>
17+
*/
18+
19+
package org.comixedproject.model.net.collections;
20+
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import lombok.AllArgsConstructor;
23+
import lombok.Getter;
24+
import lombok.NoArgsConstructor;
25+
26+
@NoArgsConstructor
27+
@AllArgsConstructor
28+
public class LoadSeriesListRequest {
29+
@JsonProperty("pageIndex")
30+
@Getter
31+
private int pageIndex;
32+
33+
@JsonProperty("pageSize")
34+
@Getter
35+
private int pageSize;
36+
37+
@JsonProperty("sortBy")
38+
@Getter
39+
private String sortBy;
40+
41+
@JsonProperty("sortDirection")
42+
@Getter
43+
private String sortDirection;
44+
45+
@Override
46+
public String toString() {
47+
return "LoadSeriesListRequest{"
48+
+ "pageIndex="
49+
+ pageIndex
50+
+ ", pageSize="
51+
+ pageSize
52+
+ ", sortBy='"
53+
+ sortBy
54+
+ '\''
55+
+ ", sortDirection='"
56+
+ sortDirection
57+
+ '\''
58+
+ '}';
59+
}
60+
}

comixed-model/src/main/java/org/comixedproject/model/net/collections/LoadSeriesListResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ public class LoadSeriesListResponse {
3535
@JsonProperty("series")
3636
@Getter
3737
private List<SeriesDetail> seriesDetails;
38+
39+
@JsonProperty("totalSeries")
40+
@Getter
41+
private int totalSeries;
3842
}

comixed-repositories/src/main/java/org/comixedproject/repositories/collections/SeriesDetailsRepository.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.comixedproject.model.collections.SeriesDetail;
2222
import org.comixedproject.model.collections.SeriesDetailId;
2323
import org.springframework.data.jpa.repository.JpaRepository;
24+
import org.springframework.data.jpa.repository.Query;
2425
import org.springframework.stereotype.Repository;
2526

2627
/**
@@ -30,4 +31,12 @@
3031
* @author Darryl L. Pierce
3132
*/
3233
@Repository
33-
public interface SeriesDetailsRepository extends JpaRepository<SeriesDetail, SeriesDetailId> {}
34+
public interface SeriesDetailsRepository extends JpaRepository<SeriesDetail, SeriesDetailId> {
35+
/**
36+
* Returns the number of unique series in the database.
37+
*
38+
* @return the series count
39+
*/
40+
@Query("SELECT COUNT(s) FROM SeriesDetail s")
41+
int getSeriesCount();
42+
}

comixed-rest/src/main/java/org/comixedproject/rest/collections/SeriesDetailController.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import lombok.extern.log4j.Log4j2;
2424
import org.comixedproject.model.collections.Issue;
2525
import org.comixedproject.model.net.collections.LoadSeriesDetailRequest;
26+
import org.comixedproject.model.net.collections.LoadSeriesListRequest;
2627
import org.comixedproject.model.net.collections.LoadSeriesListResponse;
2728
import org.comixedproject.service.collections.SeriesDetailService;
2829
import org.springframework.beans.factory.annotation.Autowired;
@@ -53,9 +54,15 @@ public class SeriesDetailController {
5354
consumes = MediaType.APPLICATION_JSON_VALUE)
5455
@PreAuthorize("hasRole('READER')")
5556
@Timed(value = "comixed.series.load-list")
56-
public LoadSeriesListResponse loadSeriesList() {
57-
log.info("Loading series");
58-
return new LoadSeriesListResponse(this.seriesDetailService.getSeriesList());
57+
public LoadSeriesListResponse loadSeriesList(@RequestBody final LoadSeriesListRequest request) {
58+
log.info("Loading series: {}", request);
59+
final int pageIndex = request.getPageIndex();
60+
final int pageSize = request.getPageSize();
61+
final String sortBy = request.getSortBy();
62+
final String sortDirection = request.getSortDirection();
63+
return new LoadSeriesListResponse(
64+
this.seriesDetailService.getSeriesList(pageIndex, pageSize, sortBy, sortDirection),
65+
this.seriesDetailService.getSeriesCount());
5966
}
6067

6168
/**

comixed-rest/src/test/java/org/comixedproject/rest/collections/SeriesDetailControllerTest.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.comixedproject.model.collections.Issue;
2626
import org.comixedproject.model.collections.SeriesDetail;
2727
import org.comixedproject.model.net.collections.LoadSeriesDetailRequest;
28+
import org.comixedproject.model.net.collections.LoadSeriesListRequest;
2829
import org.comixedproject.model.net.collections.LoadSeriesListResponse;
2930
import org.comixedproject.service.collections.SeriesDetailService;
3031
import org.junit.Test;
@@ -36,6 +37,10 @@
3637

3738
@RunWith(MockitoJUnitRunner.class)
3839
public class SeriesDetailControllerTest {
40+
private static final int TEST_PAGE_INDEX = 3;
41+
private static final int TEST_PAGE_SIZE = 25;
42+
private static final String TEST_SORT_BY = "name";
43+
private static final String TEST_SORT_DIRECTION = "asc";
3944
private static final String TEST_PUBLISHER = "Publisher Name";
4045
private static final String TEST_SERIES = "SeriesDetail Name";
4146
private static final String TEST_VOLUME = "2022";
@@ -47,14 +52,21 @@ public class SeriesDetailControllerTest {
4752

4853
@Test
4954
public void testLoadSeriesList() {
50-
Mockito.when(seriesDetailService.getSeriesList()).thenReturn(seriesDetailList);
55+
Mockito.when(
56+
seriesDetailService.getSeriesList(
57+
Mockito.anyInt(), Mockito.anyInt(), Mockito.anyString(), Mockito.anyString()))
58+
.thenReturn(seriesDetailList);
5159

52-
final LoadSeriesListResponse result = controller.loadSeriesList();
60+
final LoadSeriesListResponse result =
61+
controller.loadSeriesList(
62+
new LoadSeriesListRequest(
63+
TEST_PAGE_INDEX, TEST_PAGE_SIZE, TEST_SORT_BY, TEST_SORT_DIRECTION));
5364

5465
assertNotNull(result);
5566
assertSame(seriesDetailList, result.getSeriesDetails());
5667

57-
Mockito.verify(seriesDetailService, Mockito.times(1)).getSeriesList();
68+
Mockito.verify(seriesDetailService, Mockito.times(1))
69+
.getSeriesList(TEST_PAGE_INDEX, TEST_PAGE_SIZE, TEST_SORT_BY, TEST_SORT_DIRECTION);
5870
}
5971

6072
@Test

comixed-services/src/main/java/org/comixedproject/service/collections/SeriesDetailService.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import org.comixedproject.model.collections.SeriesDetail;
2525
import org.comixedproject.repositories.collections.SeriesDetailsRepository;
2626
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.data.domain.PageRequest;
28+
import org.springframework.data.domain.Sort;
2729
import org.springframework.stereotype.Service;
2830
import org.springframework.transaction.annotation.Transactional;
31+
import org.springframework.util.StringUtils;
2932

3033
/**
3134
* <code>SeriesDetailService</code> provides methods for working with instances of {@link
@@ -45,9 +48,23 @@ public class SeriesDetailService {
4548
* @return the list of series
4649
*/
4750
@Transactional(readOnly = true)
48-
public List<SeriesDetail> getSeriesList() {
51+
public List<SeriesDetail> getSeriesList(
52+
final int pageIndex, final int pageSize, final String sortBy, final String sortDirection) {
4953
log.debug("Loading series list");
50-
return this.seriesDetailsRepository.findAll();
54+
return this.seriesDetailsRepository
55+
.findAll(PageRequest.of(pageIndex, pageSize, this.doCreateSort(sortBy, sortDirection)))
56+
.stream()
57+
.toList();
58+
}
59+
60+
/**
61+
* Returns the number of series in the database.
62+
*
63+
* @return the series count
64+
*/
65+
@Transactional
66+
public int getSeriesCount() {
67+
return this.seriesDetailsRepository.getSeriesCount();
5168
}
5269

5370
/**
@@ -64,4 +81,26 @@ public List<Issue> loadSeriesDetail(
6481
log.debug("Loading series detail: publisher={} name={} volume={}", publisher, name, volume);
6582
return this.issueService.getAll(publisher, name, volume);
6683
}
84+
85+
private Sort doCreateSort(final String sortBy, final String sortDirection) {
86+
if (!StringUtils.hasLength(sortBy) || !StringUtils.hasLength(sortDirection)) {
87+
return Sort.unsorted();
88+
}
89+
90+
String fieldName;
91+
switch (sortBy) {
92+
case "publisher" -> fieldName = "id.publisher";
93+
case "name" -> fieldName = "id.series";
94+
case "volume" -> fieldName = "id.volume";
95+
case "in-library" -> fieldName = "inLibrary";
96+
case "total-comics" -> fieldName = "totalIssues";
97+
default -> fieldName = "id.publisher";
98+
}
99+
100+
Sort.Direction direction = Sort.Direction.DESC;
101+
if (sortDirection.equals("asc")) {
102+
direction = Sort.Direction.ASC;
103+
}
104+
return Sort.by(direction, fieldName);
105+
}
67106
}

comixed-services/src/test/java/org/comixedproject/service/collections/SeriesDetailServiceTest.java

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,54 +18,149 @@
1818

1919
package org.comixedproject.service.collections;
2020

21-
import static junit.framework.TestCase.assertNotNull;
22-
import static junit.framework.TestCase.assertSame;
21+
import static junit.framework.TestCase.*;
2322

2423
import java.util.ArrayList;
2524
import java.util.List;
25+
import java.util.stream.Stream;
2626
import org.comixedproject.model.collections.Issue;
2727
import org.comixedproject.model.collections.SeriesDetail;
2828
import org.comixedproject.model.collections.SeriesDetailId;
2929
import org.comixedproject.repositories.collections.SeriesDetailsRepository;
3030
import org.junit.Before;
3131
import org.junit.Test;
3232
import org.junit.runner.RunWith;
33-
import org.mockito.InjectMocks;
34-
import org.mockito.Mock;
35-
import org.mockito.Mockito;
33+
import org.mockito.*;
3634
import org.mockito.junit.MockitoJUnitRunner;
35+
import org.springframework.data.domain.Page;
36+
import org.springframework.data.domain.Pageable;
3737

3838
@RunWith(MockitoJUnitRunner.class)
3939
public class SeriesDetailServiceTest {
40+
private static final int TEST_PAGE_INDEX = 3;
41+
private static final int TEST_PAGE_SIZE = 25;
42+
private static final String TEST_SORT_BY = "name";
43+
private static final String TEST_SORT_DIRECTION = "asc";
4044
private static final String TEST_PUBLISHER = "The publisher";
4145
private static final String TEST_SERIES = "The series";
4246
private static final String TEST_VOLUME = "2022";
4347
private static final Long TEST_ISSUE_COUNT = 29L;
48+
private static final int TEST_SERIES_COUNT = 237;
4449

4550
@InjectMocks private SeriesDetailService service;
4651
@Mock private SeriesDetailsRepository seriesDetailsRepository;
4752
@Mock private IssueService issueService;
53+
@Mock private Stream<SeriesDetail> seriesDetailStream;
54+
@Mock private Page<SeriesDetail> seriesDetailPage;
4855
@Mock private List<Issue> issueList;
4956

57+
@Captor private ArgumentCaptor<Pageable> pageableArgumentCaptor;
58+
5059
private List<SeriesDetail> seriesDetailList = new ArrayList<>();
5160

5261
@Before
5362
public void setUp() {
63+
Mockito.when(seriesDetailPage.stream()).thenReturn(seriesDetailStream);
64+
Mockito.when(seriesDetailStream.toList()).thenReturn(seriesDetailList);
5465
seriesDetailList.add(
5566
new SeriesDetail(
5667
new SeriesDetailId(TEST_PUBLISHER, TEST_SERIES, TEST_VOLUME), TEST_ISSUE_COUNT));
5768
}
5869

5970
@Test
60-
public void testLoadSeriesList() {
61-
Mockito.when(seriesDetailsRepository.findAll()).thenReturn(seriesDetailList);
71+
public void testLoadSeriesList_sortedByPublisher() {
72+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
73+
.thenReturn(seriesDetailPage);
74+
75+
doLoadSeriesList_sorted("publisher");
76+
}
77+
78+
@Test
79+
public void testLoadSeriesList_sortedByName() {
80+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
81+
.thenReturn(seriesDetailPage);
6282

63-
final List<SeriesDetail> result = service.getSeriesList();
83+
doLoadSeriesList_sorted("name");
84+
}
85+
86+
@Test
87+
public void testLoadSeriesList_sortedByVolume() {
88+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
89+
.thenReturn(seriesDetailPage);
90+
91+
doLoadSeriesList_sorted("volume");
92+
}
93+
94+
@Test
95+
public void testLoadSeriesList_sortedByLibraryCount() {
96+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
97+
.thenReturn(seriesDetailPage);
98+
99+
doLoadSeriesList_sorted("in-library");
100+
}
101+
102+
@Test
103+
public void testLoadSeriesList_sortedByTotalComics() {
104+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
105+
.thenReturn(seriesDetailPage);
106+
107+
doLoadSeriesList_sorted("total-comics");
108+
}
109+
110+
@Test
111+
public void testLoadSeriesList_sortedByUnknown() {
112+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
113+
.thenReturn(seriesDetailPage);
114+
115+
doLoadSeriesList_sorted("farkle");
116+
}
117+
118+
private void doLoadSeriesList_sorted(final String publisher) {
119+
for (int index = 0; index < 2; index++) {
120+
final List<SeriesDetail> result =
121+
service.getSeriesList(
122+
TEST_PAGE_INDEX, TEST_PAGE_SIZE, publisher, index == 0 ? "asc" : "desc");
123+
124+
assertNotNull(result);
125+
assertSame(seriesDetailList, result);
126+
127+
final Pageable pageRequest = pageableArgumentCaptor.getValue();
128+
assertEquals(TEST_PAGE_INDEX, pageRequest.getPageNumber());
129+
assertEquals(TEST_PAGE_SIZE, pageRequest.getPageSize());
130+
assertTrue(TEST_SORT_BY, pageRequest.getSort().isSorted());
131+
132+
Mockito.verify(seriesDetailsRepository, Mockito.times(1)).findAll(pageRequest);
133+
}
134+
}
135+
136+
@Test
137+
public void testLoadSeriesList_unsorted() {
138+
Mockito.when(seriesDetailsRepository.findAll(pageableArgumentCaptor.capture()))
139+
.thenReturn(seriesDetailPage);
140+
141+
final List<SeriesDetail> result =
142+
service.getSeriesList(TEST_PAGE_INDEX, TEST_PAGE_SIZE, "", "");
64143

65144
assertNotNull(result);
66145
assertSame(seriesDetailList, result);
67146

68-
Mockito.verify(seriesDetailsRepository, Mockito.times(1)).findAll();
147+
final Pageable pageRequest = pageableArgumentCaptor.getValue();
148+
assertEquals(TEST_PAGE_INDEX, pageRequest.getPageNumber());
149+
assertEquals(TEST_PAGE_SIZE, pageRequest.getPageSize());
150+
assertFalse(TEST_SORT_BY, pageRequest.getSort().isSorted());
151+
152+
Mockito.verify(seriesDetailsRepository, Mockito.times(1)).findAll(pageRequest);
153+
}
154+
155+
@Test
156+
public void testLoadSeriesCount() {
157+
Mockito.when(seriesDetailsRepository.getSeriesCount()).thenReturn(TEST_SERIES_COUNT);
158+
159+
final int result = service.getSeriesCount();
160+
161+
assertEquals(TEST_SERIES_COUNT, result);
162+
163+
Mockito.verify(seriesDetailsRepository, Mockito.times(1)).getSeriesCount();
69164
}
70165

71166
@Test

0 commit comments

Comments
 (0)