001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.frontoffice.search.requesttime.pagination; 017 018import java.util.Optional; 019 020import org.ametys.web.frontoffice.search.SearchService; 021 022/** 023 * The representation of a parameterized pagination (with params {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE} and {@link SearchService#PARAM_NAME_MAX_RESULTS}) 024 * in a search service, and a fixed page index, providing convenient methods to retrieve counts and indexes. 025 */ 026public final class Pagination 027{ 028 private int _pageIndex; 029 private Optional<Integer> _paramResultsPerPage; 030 private Optional<Integer> _paramMaxResults; 031 032 /** 033 * Builds a pagination 034 * @param pageIndex The current page index. Must be greater or equal to 1 035 * @param paramResultsPerPage The (optional) maximum number of allowed results for one page 036 * @param paramMaxResults The (optional) maximum number of allowed results overall 037 */ 038 public Pagination(int pageIndex, Optional<Integer> paramResultsPerPage, Optional<Integer> paramMaxResults) 039 { 040 assert pageIndex > 0; 041 _pageIndex = pageIndex; 042 _paramResultsPerPage = paramResultsPerPage; 043 _paramMaxResults = paramMaxResults; 044 } 045 046 /** 047 * Gets the current page index. 048 * <br>The page index is on final user format, i.e. that the first index is 1 (not 0), etc. 049 * @return the current page index 050 */ 051 public int currentPageIndex() 052 { 053 return _pageIndex; 054 } 055 056 /** 057 * Gets the absolute index for the starting document in the current state (i.e. current page). 058 * 059 * <br>Unlike {@link #currentPageIndex}, the first index is 0. 060 * 061 * <br>For instance, if {@link #currentPageIndex} is 1, then it will always return 0. 062 * 063 * <br>For instance, if {@link #currentPageIndex} is 5 and there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page}, it will return 12 064 * (because page 1 holds results [0,1,2], page 2 holds results [3,4,5] and so on, and page 5 holds results [12,13,14]). 065 * @return the index for the starting document 066 */ 067 public int currentStartDocIndex() 068 { 069 int start = _paramResultsPerPage 070 .map(this::_currentStartDocIndex) 071 .orElse(0); 072 return start; 073 } 074 075 private int _currentStartDocIndex(int resultsPerPage) 076 { 077 return (_pageIndex - 1) * resultsPerPage; 078 } 079 080 /** 081 * Gets the absolute index for the ending document in the current state (i.e. current page). 082 * 083 * <br>For not breaking old expected indexes, this method returns the integer just after the one of the last doc 084 * (and thus this integer is not reached in current page). That's why it is called "exclusive". 085 * 086 * <br>For instance: 087 * <ul> 088 * 089 * <li> 090 * let's say the {@link #currentPageIndex} is 1 (so {@link #currentStartDocIndex} is 0) and 091 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page}, and totalHits=100, 092 * then the page holds results [0,1,2] and this methods returns <b>3</b>. 093 * </li> 094 * 095 * <li> 096 * let's say the {@link #currentPageIndex} is 5 (so {@link #currentStartDocIndex} is 12) and 097 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page}, and totalHits=100, 098 * then the page holds results [12,13,14] and this methods returns <b>15</b>. 099 * </li> 100 * 101 * <li> 102 * let's say the {@link #currentPageIndex} is 5 (so {@link #currentStartDocIndex} is 12) and 103 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page}, and totalHits=13, 104 * then the page holds results [12] and this methods returns <b>13</b>. 105 * </li> 106 * 107 * <li> 108 * let's say the {@link #currentPageIndex} is 5 (so {@link #currentStartDocIndex} is 12) and 109 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page} and there are 14 {@link SearchService#PARAM_NAME_MAX_RESULTS max results}, 110 * and totalHits=100, then the page holds results [12,13] and this methods returns <b>14</b>. 111 * </li> 112 * 113 * </ul> 114 * 115 * @param totalHits the number of total hits 116 * @return the (exclusive) index for the ending document 117 */ 118 public int currentEndDocExclusiveIndex(int totalHits) 119 { 120 return _endDocExclusiveIndex(currentStartDocIndex(), _currentNumberOfDocs(totalHits)); 121 } 122 123 private int _currentNumberOfDocs(int totalHits) 124 { 125 int currentMaxNumberOfDocs = currentMaxNumberOfDocs(); 126 int currentStartDocIndex = currentStartDocIndex(); 127 int potentialEndExclusiveIndex = _endDocExclusiveIndex(currentStartDocIndex, currentMaxNumberOfDocs); 128 return potentialEndExclusiveIndex > totalHits 129 ? totalHits - currentStartDocIndex 130 : currentMaxNumberOfDocs; 131 } 132 133 /** 134 * Gets the maximum number of documents allowed in the current page 135 * 136 * <br>For instance: 137 * <ul> 138 * 139 * <li> 140 * let's say the {@link #currentPageIndex} is 1 (so {@link #currentStartDocIndex} is 0) and 141 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page}, 142 * then the page has the capacity for potential results [0,1,2] and this methods returns <b>3</b>. 143 * </li> 144 * 145 * <li> 146 * let's say the {@link #currentPageIndex} is 5 (so {@link #currentStartDocIndex} is 12) and 147 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page}, 148 * then the page has the capacity for potential results [12,13,14] and then this methods returns <b>3</b>. 149 * </li> 150 * 151 * <li> 152 * let's say the {@link #currentPageIndex} is 5 (so {@link #currentStartDocIndex} is 12) and 153 * there are 3 {@link SearchService#PARAM_NAME_RESULTS_PER_PAGE results per page} and there are 14 {@link SearchService#PARAM_NAME_MAX_RESULTS max results}, 154 * then the page has the capacity for potential results [12,13] and then this methods returns <b>2</b>. 155 * </li> 156 * 157 * </ul> 158 * @return the maximum number of documents allowed in the current page 159 */ 160 public int currentMaxNumberOfDocs() 161 { 162 if (_paramResultsPerPage.isPresent() && _paramMaxResults.isPresent()) 163 { 164 // There is pagination, but also a number of max results 165 int resultsPerPage = _paramResultsPerPage.get(); 166 int start = _currentStartDocIndex(resultsPerPage); 167 int potentialEndIndex = _endDocInclusiveIndex(start, resultsPerPage); 168 169 int maxResults = _paramMaxResults.get(); 170 int maxAllowedEndIndex = maxResults - 1; 171 172 return potentialEndIndex > maxAllowedEndIndex 173 /* 174 * overflow => return (maxResults - start) instead 175 * 176 * you can notice that 177 * potentialEndIndex > maxAllowedEndIndex 178 * <=> start + resultsPerPage - 1 > maxResults - 1 179 * <=> resultsPerPage > maxResults - start 180 * Thus, mathematically, we have less docs for this final page than a 'regular' page 181 */ 182 ? (maxResults - start) 183 /* 184 * no overflow 185 */ 186 : resultsPerPage; 187 } 188 else if (_paramResultsPerPage.isPresent()) 189 { 190 // No max results 191 return _paramResultsPerPage.get(); 192 } 193 else if (_paramMaxResults.isPresent()) 194 { 195 // No pagination 196 return _paramMaxResults.get(); 197 } 198 else 199 { 200 // No pagination, no max results 201 return Integer.MAX_VALUE; 202 } 203 } 204 205 private static int _endDocInclusiveIndex(int startDocIndex, int resultsPerPage) 206 { 207 return _endDocExclusiveIndex(startDocIndex, resultsPerPage) - 1; 208 } 209 210 private static int _endDocExclusiveIndex(int startDocIndex, int resultsPerPage) 211 { 212 return startDocIndex + resultsPerPage; 213 } 214 215 /** 216 * Given the number of total hits, gets the number of pages 217 * @param totalHits the number of total hits 218 * @return the number of pages 219 */ 220 public int numberOfPages(int totalHits) 221 { 222 if (totalHits == 0) 223 { 224 return 1; // technically 0 page, but do as if there is one with 0 hit on it 225 } 226 227 if (!_paramResultsPerPage.isPresent()) 228 { 229 return 1; // only one page 230 } 231 232 int theoricNbResultsPerPage = _paramResultsPerPage.get(); 233 int realTotalHits = _paramMaxResults 234 .map(maxResults -> Math.min(totalHits, maxResults)) 235 .orElse(totalHits); 236 double divisionResult = (double) realTotalHits / (double) theoricNbResultsPerPage; 237 int nbPages = (int) Math.ceil(divisionResult); // upper bound, it is just that the last page may have less results than other pages 238 return nbPages; 239 } 240} 241