/*
* Copyright (C) 2009-2017 Slava Semushin <slava.semushin@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ru.mystamps.web.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.Validate;
import lombok.Getter;
import lombok.ToString;
// yes, I know that it's too complex :-(
@SuppressWarnings("PMD.ModifiedCyclomaticComplexity")
@ToString
public class Pager {
// be very careful when you're changing this value
private static final int MAX_ITEMS = 5;
private static final int MAX_ITEMS_BEFORE_CURRENT = MAX_ITEMS - 1;
private static final int MAX_ITEMS_AFTER_CURRENT = MAX_ITEMS - 1;
private static final int ITEMS_BEFORE_CURRENT = MAX_ITEMS / 2;
private static final int ITEMS_AFTER_CURRENT = MAX_ITEMS / 2;
private static final int FIRST_PAGE = 1;
// this field is shown in toString() and useful when debugging unit tests
@SuppressWarnings("PMD.SingularField")
private final int totalRecords;
// this field is shown in toString() and useful when debugging unit tests
@SuppressWarnings("PMD.SingularField")
private final int totalPages;
// this field is shown in toString() and useful when debugging unit tests
@SuppressWarnings({ "PMD.SingularField", "PMD.UnusedPrivateField" })
private final int recordsPerPage;
// this field is using in the view (hence its getter)
@Getter
private final int currentPage;
// this field is using in the view (hence its getter)
@Getter
private final List<Integer> items;
// this field is using in the view (hence its getter)
@Getter
private final Integer prev;
// this field is using in the view (hence its getter)
@Getter
private final Integer next;
public Pager(long totalRecords, int recordsPerPage, int currentPage) {
Validate.isTrue(totalRecords >= 0, "Total records must be greater than or equal to zero");
Validate.isTrue(recordsPerPage > 0, "Records per page must be greater than zero");
Validate.isTrue(currentPage > 0, "Current page must be greater than zero");
this.totalRecords = Math.toIntExact(totalRecords);
this.totalPages = countTotalPages(this.totalRecords, recordsPerPage);
this.recordsPerPage = recordsPerPage;
this.currentPage = currentPage;
this.items = createItems(this.totalRecords, recordsPerPage, currentPage, this.totalPages);
this.prev = findPrev(currentPage);
this.next = findNext(currentPage, this.totalPages);
}
private static int countTotalPages(int totalRecords, int recordsPerPage) {
int totalPages = totalRecords / recordsPerPage;
if (totalRecords % recordsPerPage > 0) {
totalPages++;
}
return totalPages;
}
private static Integer findPrev(int currentPage) {
if (currentPage == FIRST_PAGE) {
return null;
}
return Integer.valueOf(currentPage - 1);
}
private static Integer findNext(int currentPage, int totalPages) {
if (currentPage == totalPages) {
return null;
}
return Integer.valueOf(currentPage + 1);
}
// I hope that we'll fix these one day
@SuppressWarnings("PMD.NPathComplexity")
private static List<Integer> createItems(
int totalRecords,
int recordsPerPage,
int currentPage,
int totalPages) {
if (totalRecords <= recordsPerPage) {
return Collections.singletonList(FIRST_PAGE);
}
int prevItemsCnt = 0;
for (int i = 1; i <= MAX_ITEMS_BEFORE_CURRENT; i++) {
int page = currentPage - i;
if (page < FIRST_PAGE) {
break;
}
prevItemsCnt++;
}
int nextItemsCnt = 0;
for (int i = 1; i <= MAX_ITEMS_AFTER_CURRENT; i++) {
int page = currentPage + i;
if (page > totalPages) {
break;
}
nextItemsCnt++;
}
// we've added too much to a both sides
if (prevItemsCnt > ITEMS_BEFORE_CURRENT && nextItemsCnt > ITEMS_AFTER_CURRENT) {
while (prevItemsCnt > ITEMS_BEFORE_CURRENT) {
prevItemsCnt--;
}
while (nextItemsCnt > ITEMS_AFTER_CURRENT) {
nextItemsCnt--;
}
// CheckStyle: ignore LineLength for next 3 lines
// we've added too much to the beginning
} else if (prevItemsCnt > ITEMS_BEFORE_CURRENT && nextItemsCnt <= ITEMS_AFTER_CURRENT) {
while (prevItemsCnt > ITEMS_BEFORE_CURRENT && (prevItemsCnt + nextItemsCnt + 1) > MAX_ITEMS) {
prevItemsCnt--;
}
// CheckStyle: ignore LineLength for next 3 lines
// we've added too much to the end
} else if (nextItemsCnt > ITEMS_AFTER_CURRENT && prevItemsCnt <= ITEMS_BEFORE_CURRENT) {
while (nextItemsCnt > ITEMS_AFTER_CURRENT && (prevItemsCnt + nextItemsCnt + 1) > MAX_ITEMS) {
nextItemsCnt--;
}
}
List<Integer> items = new ArrayList<>();
int fromPage = currentPage - prevItemsCnt;
int toPage = currentPage + nextItemsCnt;
for (int page = fromPage; page <= toPage; page++) {
items.add(page);
}
Validate.validState(
items.size() <= MAX_ITEMS,
"Size of items (%s) must be <= %d when "
+ "totalRecords = %d, recordsPerPage = %d, currentPage = %d and totalPages = %d",
items, MAX_ITEMS, totalRecords, recordsPerPage, currentPage, totalPages
);
return items;
}
}