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