Skip to content

Commit c6f7520

Browse files
committed
/site/events: add pagination.
Fix #359
1 parent 5589a89 commit c6f7520

File tree

10 files changed

+553
-23
lines changed

10 files changed

+553
-23
lines changed

src/main/java/ru/mystamps/web/controller/SiteController.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.stereotype.Controller;
2424
import org.springframework.ui.Model;
2525
import org.springframework.web.bind.annotation.RequestMapping;
26+
import org.springframework.web.bind.annotation.RequestParam;
2627

2728
import lombok.RequiredArgsConstructor;
2829

@@ -36,13 +37,15 @@
3637
import ru.mystamps.web.service.SeriesService;
3738
import ru.mystamps.web.service.SuspiciousActivityService;
3839
import ru.mystamps.web.util.LocaleUtils;
40+
import ru.mystamps.web.util.Pager;
3941

4042
@Controller
4143
@RequiredArgsConstructor
4244
public class SiteController {
4345

4446
private static final int AMOUNT_OF_RECENTLY_ADDED_SERIES = 10; // NOPMD: LongVariable
4547
private static final int AMOUNT_OF_RECENTLY_CREATED_COLLECTIONS = 10; // NOPMD: LongVariable
48+
private static final int RECORDS_PER_PAGE = 50;
4649

4750
private final CategoryService categoryService;
4851
private final CollectionService collectionService;
@@ -81,10 +84,17 @@ public String showIndexPage(Model model, Locale userLocale) {
8184
* @author Slava Semushin
8285
*/
8386
@RequestMapping(Url.SITE_EVENTS_PAGE)
84-
public void viewSiteEvents(Model model) {
87+
public void viewSiteEvents(
88+
@RequestParam(value = "page", defaultValue = "1") int pageNum,
89+
Model model) {
90+
91+
int page = Math.max(1, pageNum);
92+
long activitiesRecords = suspiciousActivityService.countAll();
8593
List<SuspiciousActivityDto> activities =
86-
suspiciousActivityService.findSuspiciousActivities();
94+
suspiciousActivityService.findSuspiciousActivities(page, RECORDS_PER_PAGE);
95+
Pager pager = new Pager(activitiesRecords, RECORDS_PER_PAGE, page);
8796

97+
model.addAttribute("pager", pager);
8898
model.addAttribute("activities", activities);
8999
}
90100

src/main/java/ru/mystamps/web/dao/SuspiciousActivityDao.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424

2525
public interface SuspiciousActivityDao {
2626
void add(AddSuspiciousActivityDbDto activity);
27-
List<SuspiciousActivityDto> findAll();
27+
long countAll();
28+
List<SuspiciousActivityDto> findAll(int page, int recordsPerPage);
2829
}

src/main/java/ru/mystamps/web/dao/impl/JdbcSuspiciousActivityDao.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ public class JdbcSuspiciousActivityDao implements SuspiciousActivityDao {
4141
@Value("${suspicious_activity.create}")
4242
private String addSuspiciousActivitySql;
4343

44+
@SuppressWarnings("PMD.LongVariable")
45+
@Value("${suspicious_activity.count_all}")
46+
private String countAllSuspiciousActivitiesSql;
47+
4448
@Value("${suspicious_activity.find_all}")
4549
private String findAllSuspiciousActivitiesSql;
4650

@@ -68,14 +72,28 @@ public void add(AddSuspiciousActivityDbDto activity) {
6872
);
6973
}
7074

75+
@Override
76+
public long countAll() {
77+
return jdbcTemplate.queryForObject(
78+
countAllSuspiciousActivitiesSql,
79+
Collections.<String, Object>emptyMap(),
80+
Long.class
81+
);
82+
}
83+
7184
/**
7285
* @author Sergey Chechenev
86+
* @author Slava Semushin
7387
*/
7488
@Override
75-
public List<SuspiciousActivityDto> findAll() {
89+
public List<SuspiciousActivityDto> findAll(int page, int recordsPerPage) {
90+
Map<String, Object> params = new HashMap<>();
91+
params.put("limit", recordsPerPage);
92+
params.put("offset", (page - 1) * recordsPerPage);
93+
7694
return jdbcTemplate.query(
7795
findAllSuspiciousActivitiesSql,
78-
Collections.emptyMap(),
96+
params,
7997
RowMappers::forSuspiciousActivityDto
8098
);
8199
}

src/main/java/ru/mystamps/web/service/SuspiciousActivityService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323

2424
/**
2525
* @author Sergey Chechenev
26+
* @author Slava Semushin
2627
*/
2728
public interface SuspiciousActivityService {
28-
List<SuspiciousActivityDto> findSuspiciousActivities();
29+
long countAll();
30+
List<SuspiciousActivityDto> findSuspiciousActivities(int page, int recordsPerPage);
2931
}

src/main/java/ru/mystamps/web/service/SuspiciousActivityServiceImpl.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import java.util.List;
2121

22+
import org.apache.commons.lang3.Validate;
23+
2224
import org.springframework.transaction.annotation.Transactional;
2325

2426
import org.springframework.security.access.prepost.PreAuthorize;
@@ -31,6 +33,7 @@
3133

3234
/**
3335
* @author Sergey Chechenev
36+
* @author Slava Semushin
3437
*/
3538
@RequiredArgsConstructor
3639
public class SuspiciousActivityServiceImpl implements SuspiciousActivityService {
@@ -39,7 +42,18 @@ public class SuspiciousActivityServiceImpl implements SuspiciousActivityService
3942
@Override
4043
@Transactional(readOnly = true)
4144
@PreAuthorize(HasAuthority.VIEW_SITE_EVENTS)
42-
public List<SuspiciousActivityDto> findSuspiciousActivities() {
43-
return suspiciousActivityDao.findAll();
45+
public long countAll() {
46+
return suspiciousActivityDao.countAll();
4447
}
48+
49+
@Override
50+
@Transactional(readOnly = true)
51+
@PreAuthorize(HasAuthority.VIEW_SITE_EVENTS)
52+
public List<SuspiciousActivityDto> findSuspiciousActivities(int page, int recordsPerPage) {
53+
Validate.isTrue(page > 0, "Page must be greater than zero");
54+
Validate.isTrue(recordsPerPage > 0, "RecordsPerPage must be greater than zero");
55+
56+
return suspiciousActivityDao.findAll(page, recordsPerPage);
57+
}
58+
4559
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright (C) 2009-2016 Slava Semushin <slava.semushin@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.util;
19+
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
24+
import org.apache.commons.lang3.Validate;
25+
26+
import lombok.Getter;
27+
import lombok.ToString;
28+
29+
// too many "PMD.SingularField" here and yes, I know that it's too complex :-(
30+
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ModifiedCyclomaticComplexity" })
31+
@ToString
32+
public class Pager {
33+
// be very careful when you're changing this value
34+
private static final int MAX_ITEMS = 5;
35+
36+
private static final int MAX_ITEMS_BEFORE_CURRENT = MAX_ITEMS - 1;
37+
private static final int MAX_ITEMS_AFTER_CURRENT = MAX_ITEMS - 1;
38+
39+
private static final int ITEMS_BEFORE_CURRENT = MAX_ITEMS / 2;
40+
private static final int ITEMS_AFTER_CURRENT = MAX_ITEMS / 2;
41+
42+
private static final int FIRST_PAGE = 1;
43+
44+
// this field is shown in toString() and useful when debugging unit tests
45+
@SuppressWarnings("PMD.SingularField")
46+
private final int totalRecords;
47+
48+
// this field is shown in toString() and useful when debugging unit tests
49+
@SuppressWarnings("PMD.SingularField")
50+
private final int totalPages;
51+
52+
// this field is shown in toString() and useful when debugging unit tests
53+
@SuppressWarnings({ "PMD.SingularField", "PMD.UnusedPrivateField" })
54+
private final int recordsPerPage;
55+
56+
// this field is using in the view (hence its getter)
57+
@SuppressWarnings("PMD.SingularField")
58+
@Getter
59+
private final int currentPage;
60+
61+
// this field is using in the view (hence its getter)
62+
@SuppressWarnings("PMD.SingularField")
63+
@Getter
64+
private final List<Integer> items;
65+
66+
// this field is using in the view (hence its getter)
67+
@SuppressWarnings("PMD.SingularField")
68+
@Getter
69+
private final Integer prev;
70+
71+
// this field is using in the view (hence its getter)
72+
@SuppressWarnings("PMD.SingularField")
73+
@Getter
74+
private final Integer next;
75+
76+
public Pager(long totalRecords, int recordsPerPage, int currentPage) {
77+
Validate.isTrue(totalRecords >= 0, "Total records must be greater than or equal to zero");
78+
Validate.isTrue(recordsPerPage > 0, "Records per page must be greater than zero");
79+
Validate.isTrue(currentPage > 0, "Current page must be greater than zero");
80+
81+
this.totalRecords = Math.toIntExact(totalRecords);
82+
this.totalPages = countTotalPages(this.totalRecords, recordsPerPage);
83+
this.recordsPerPage = recordsPerPage;
84+
this.currentPage = currentPage;
85+
this.items = createItems(this.totalRecords, recordsPerPage, currentPage, this.totalPages);
86+
this.prev = findPrev(currentPage);
87+
this.next = findNext(currentPage, this.totalPages);
88+
}
89+
90+
private static int countTotalPages(int totalRecords, int recordsPerPage) {
91+
return (int)Math.ceil(totalRecords / recordsPerPage);
92+
}
93+
94+
private static Integer findPrev(int currentPage) {
95+
if (currentPage == FIRST_PAGE) {
96+
return null;
97+
}
98+
99+
return Integer.valueOf(currentPage - 1);
100+
}
101+
102+
private static Integer findNext(int currentPage, int totalPages) {
103+
if (currentPage == totalPages) {
104+
return null;
105+
}
106+
107+
return Integer.valueOf(currentPage + 1);
108+
}
109+
110+
// I hope that we'll fix these one day
111+
@SuppressWarnings({ "PMD.ModifiedCyclomaticComplexity", "PMD.NPathComplexity" })
112+
private static List<Integer> createItems(
113+
int totalRecords,
114+
int recordsPerPage,
115+
int currentPage,
116+
int totalPages) {
117+
118+
if (totalRecords <= recordsPerPage) {
119+
return Collections.singletonList(FIRST_PAGE);
120+
}
121+
122+
int prevItemsCnt = 0;
123+
for (int i = MAX_ITEMS_BEFORE_CURRENT; i >= 1; i--) {
124+
int page = currentPage - i;
125+
if (page >= FIRST_PAGE) {
126+
prevItemsCnt++;
127+
}
128+
}
129+
130+
int nextItemsCnt = 0;
131+
for (int i = 1; i <= MAX_ITEMS_AFTER_CURRENT; i++) {
132+
int page = currentPage + i;
133+
if (page <= totalPages) {
134+
nextItemsCnt++;
135+
}
136+
}
137+
138+
// we've added too much to a both sides
139+
if (prevItemsCnt > ITEMS_BEFORE_CURRENT && nextItemsCnt > ITEMS_AFTER_CURRENT) {
140+
while (prevItemsCnt > ITEMS_BEFORE_CURRENT) {
141+
prevItemsCnt--;
142+
}
143+
while (nextItemsCnt > ITEMS_AFTER_CURRENT) {
144+
nextItemsCnt--;
145+
}
146+
147+
// CheckStyle: ignore LineLength for next 3 lines
148+
// we've added too much to the beginning
149+
} else if (prevItemsCnt > ITEMS_BEFORE_CURRENT && nextItemsCnt <= ITEMS_AFTER_CURRENT) {
150+
while (prevItemsCnt > ITEMS_BEFORE_CURRENT && (prevItemsCnt + nextItemsCnt + 1) > MAX_ITEMS) {
151+
prevItemsCnt--;
152+
}
153+
154+
// CheckStyle: ignore LineLength for next 3 lines
155+
// we've added too much to the end
156+
} else if (nextItemsCnt > ITEMS_AFTER_CURRENT && prevItemsCnt <= ITEMS_BEFORE_CURRENT) {
157+
while (nextItemsCnt > ITEMS_AFTER_CURRENT && (prevItemsCnt + nextItemsCnt + 1) > MAX_ITEMS) {
158+
nextItemsCnt--;
159+
}
160+
}
161+
162+
List<Integer> items = new ArrayList<>();
163+
int fromPage = currentPage - prevItemsCnt;
164+
int toPage = currentPage + nextItemsCnt;
165+
for (int page = fromPage; page <= toPage; page++) {
166+
items.add(page);
167+
}
168+
169+
Validate.validState(
170+
items.size() <= MAX_ITEMS,
171+
"Size of items (%s) must be <= %d when "
172+
+ "totalRecords = %d, recordsPerPage = %d, currentPage = %d and totalPages = %d",
173+
items, MAX_ITEMS, totalRecords, recordsPerPage, currentPage, totalPages
174+
);
175+
176+
return items;
177+
}
178+
179+
}

src/main/resources/sql/suspicious_activity_dao_queries.properties

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ SELECT sat.id \
2121
FROM suspicious_activities_types sat \
2222
WHERE sat.name = :type
2323

24+
suspicious_activity.count_all = \
25+
SELECT COUNT(*) \
26+
FROM suspicious_activities
27+
2428
suspicious_activity.find_all = \
25-
SELECT \
26-
sat.name AS activity_name \
27-
, sa.occurred_at \
28-
, sa.page \
29-
, sa.method \
30-
, u.login AS user_login \
31-
, sa.ip \
32-
, sa.referer_page \
33-
, sa.user_agent \
34-
FROM suspicious_activities sa \
35-
JOIN suspicious_activities_types sat \
36-
ON sa.type_id = sat.id \
29+
SELECT sat.name AS activity_name \
30+
, sa.occurred_at \
31+
, sa.page \
32+
, sa.method \
33+
, u.login AS user_login \
34+
, sa.ip \
35+
, sa.referer_page \
36+
, sa.user_agent \
37+
FROM suspicious_activities sa \
38+
JOIN suspicious_activities_types sat \
39+
ON sa.type_id = sat.id \
3740
LEFT JOIN users u \
38-
ON sa.user_id = u.id
41+
ON sa.user_id = u.id \
42+
LIMIT :limit \
43+
OFFSET :offset

src/main/webapp/WEB-INF/views/site/events.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,39 @@ <h3 class="text-center" th:text="${#strings.capitalize(suspicious_activities)}">
180180
</table>
181181
</div>
182182
</div>
183+
184+
<div th:if="${not #lists.isEmpty(activities)}" class="row text-center">
185+
<nav>
186+
<ul class="pagination">
187+
<li class="disabled" th:class="${pager.prev == null}? 'disabled'">
188+
<span aria-hidden="true" th:if="${pager.prev == null}">&laquo;</span>
189+
<!--/*/
190+
<a href="#" aria-label="Previous"
191+
th:if="${pager.prev != null}"
192+
th:href="@{${SITE_EVENTS_PAGE}(page=${pager.prev})}">
193+
<span aria-hidden="true">&laquo;</span>
194+
</a>
195+
/*/-->
196+
</li>
197+
<li class="active" th:each="item : ${pager.items}" th:class="${item == pager.currentPage}? 'active'">
198+
<a href="#" th:href="@{${SITE_EVENTS_PAGE}(page=${item})}"
199+
th:utext="|${item} &lt;span class='sr-only'&gt;(current)&lt;/span&gt;|">
200+
1 <span class="sr-only">(current)</span>
201+
</a>
202+
</li>
203+
<li class="disabled" th:class="${pager.next == null}? 'disabled'">
204+
<span aria-hidden="true" th:if="${pager.next == null}">&raquo;</span>
205+
<!--/*/
206+
<a href="#" aria-label="Next"
207+
th:if="${pager.next != null}"
208+
th:href="@{${SITE_EVENTS_PAGE}(page=${pager.next})}">
209+
<span aria-hidden="true">&raquo;</span>
210+
</a>
211+
/*/-->
212+
</li>
213+
</ul>
214+
</nav>
215+
</div>
183216
</div>
184217
</div>
185218
<div class="row">

0 commit comments

Comments
 (0)