001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.webSvr.catalogue;
031
032 import java.util.Arrays;
033 import java.util.Collections;
034 import java.util.Iterator;
035 import java.util.List;
036 import java.util.SortedSet;
037 import java.util.TreeSet;
038
039 import org.hd.d.pg2k.webSvr.util.WebConsts;
040
041 /**
042 * Created by IntelliJ IDEA.
043 * User: Damon Hart-Davis
044 * Date: 07-Jun-2003
045 * Time: 08:58:31
046 */
047
048 /**Base bean to help with the pagination for otherwise large pages.
049 * This can parse an input argument indicating the desired page,
050 * and work out which page is meant and return it as an int for the page logic to use.
051 * <p>
052 * This can also generate the labels/values for one or more HTML form submit buttons
053 * to help generate the HTML page.
054 * <p>
055 * This implementation generates sequential page labels for buttons in its simplest form,
056 * but if there are too many buttons to show this then generates a sparser set
057 * that ensures that a user can navigate in few clicks to any page and especially so
058 * to nearby pages as they home in on a target.
059 * <p>
060 * This class is designed to be extended by classes that can put arbitrary labels on
061 * the buttons, for example words to help the user go directly to the desired page.
062 * <p>
063 * If given an unparseable page number as input, this and deriving implementations
064 * should always return a valid page number,
065 * returning the "safe" page number of 1 if they cannot deduce the correct page or a near one.
066 * <p>
067 * This bases its pagination on the properties in WebConsts dictating the maximum number
068 * of items to show on a page and the maximum number of page-choice buttons to show to a user.
069 * <p>
070 * This needs to be told the number of entries to be paginated;
071 * deriving classes may accept other or extra information in lieu.
072 * <p>
073 * This does its basic pagination based on the number of items to be paginated
074 * and the WebConsts that dictate maximum items per page and maximum pages
075 * or choices of pages to go to at once.
076 * In principle these might be user-settable rather than constants.
077 * <p>
078 * This is designed to be used as a bean at page or request scope in a JSP,
079 * though may be usable outside that environment.
080 * It may not be usefully serialisable.
081 * <p>
082 * <strong>THIS CLASS IS NOT THREAD SAFE</strong> since
083 * its usual environment and indeed lifetime
084 * is within the thread of a single servlet/JSP page service
085 * and thread-safety is an unnecessary overhead.
086 * Its methods are not synchronized,
087 * and if it is necessary to share one instance between threads
088 * they must hold a lock on the instance.
089 * <p>
090 * This Serializable so as to be able to be stored in
091 * a servlet session; nothing especially long-lived or sensitive.
092 * <p>
093 * TODO: Should do validation on deserialisation.
094 */
095 public abstract class PaginationBeanBase implements java.io.Serializable
096 {
097 /**Number of items to be paginated; non-negative.
098 * Defaults to zero meaning no items to paginate;
099 * any attempt to set it to a negative number sets it to zero.
100 */
101 private int numberOfItems;
102
103 /**The page number of the current page; strictly positive.
104 * Defaults to 1; any attempt to set it to an invalid value will set it to 1, which is always safe.
105 * <p>
106 * The page number must be positive and less than numberOfPages() unless numberOfPages is 0,
107 * in which case the page number must be exactly 1.
108 * <p>
109 * This property matches the property named by WebConsts.PAGE_NUMBER_PARAMETER
110 * to allow for easy setting of the property from a JSP.
111 */
112 private int pg = 1;
113
114 /**Get number of items to be paginated; non-negative.
115 * Defaults to zero meaning no items to paginate.
116 */
117 final public int getNumberOfItems()
118 {
119 return(numberOfItems);
120 }
121
122 /**Set number of items to be paginated; non-negative.
123 * Defaults to zero meaning no items to paginate;
124 * any attempt to set it to a negative number sets it to zero.
125 */
126 public void setNumberOfItems(int numberOfItems)
127 {
128 if(numberOfItems < 0) { numberOfItems = 0; } // Defeast attempts to make negative.
129 this.numberOfItems = numberOfItems;
130
131 // Force pg to be constrained by the number of items and thus pages.
132 if(pg > getNumberOfPages()) { pg = Math.max(1, getNumberOfPages()); }
133 }
134
135 /**Compute the number of pages based on the numberOfItems to be paginated.
136 * Is zero if there are no items, else strictly positive.
137 * <p>
138 * Uses the WebConsts.MAX_RESULTS_PER_PAGE to determine how many pages will be needed.
139 */
140 final public int getNumberOfPages()
141 {
142 return((numberOfItems + WebConsts.MAX_RESULTS_PER_PAGE - 1) / WebConsts.MAX_RESULTS_PER_PAGE);
143 }
144
145 /**Set the page number of the current page; strictly positive.
146 * Defaults to 1; any attempt to set it to an invalid value will set it to 1, which is always safe.
147 * <p>
148 * The page number must be positive and less than numberOfPages() unless numberOfPages is 0,
149 * in which case the page number must be exactly 1.
150 * <p>
151 * This property name matches the property named by WebConsts.PAGE_NUMBER_PARAMETER
152 * to allow for easy setting of the property from a JSP.
153 */
154 public void setPg(int pg)
155 {
156 if((pg < 1) || (pg > getNumberOfPages())) { pg = 1; } // Avoid setting an invalid value.
157 this.pg = pg;
158 }
159
160 /**Set the page number as a String; if invalid (eg badly-formatted input or null) set page number to 1.
161 * It must always be possible to parse the label for any page whatever set
162 * getPageLabels() returns.
163 * <p>
164 * A deriving class must override this for specialised parses.
165 */
166 public void setPg(final String s)
167 {
168 setPg(getPageNumber(s));
169 }
170
171 /**Find out what page number a given label indicates.
172 * This should be the reverse of getPageLabel().
173 * <p>
174 * On badly-formed or otherwise-unparsable input this should return 1.
175 */
176 public abstract int getPageNumber(String s);
177
178 /**Get the page number of the current page; strictly positive.
179 * Defaults to 1.
180 */
181 final public int getPg()
182 {
183 return(pg);
184 }
185
186 /**Get (index of) lowest-numbered item to display; non-negative.
187 * If numberOfItems() is zero this returns zero,
188 * else this returns less than numberOfItems().
189 * <p>
190 * Computed from the page number and the WebConsts.MAX_RESULTS_PER_PAGE value.
191 */
192 final public int getStartIndex()
193 {
194 return((pg - 1) * WebConsts.MAX_RESULTS_PER_PAGE);
195 }
196
197 /**Get (index of) one beyond highest-numbered item to display; non-negative.
198 * Exactly numberOfItems() for the last page,
199 * else exactly one page more than getStartIndex().
200 * <p>
201 * Computed from the page number and the WebConsts.MAX_RESULTS_PER_PAGE value.
202 */
203 final public int getEndIndex()
204 {
205 return(Math.min(numberOfItems, pg * WebConsts.MAX_RESULTS_PER_PAGE));
206 }
207
208 /**Return the page label for the given page (1 upwards), never null; the page must be in range else the result is undefined.
209 * This will commonly be overridden in deriving classes.
210 * <p>
211 * The label generated must be deterministic and depend only on the attributes set for the bean,
212 * and the same as getPageLabels() would have generated for this given page.
213 * The value for a label must not depend on the current page value,
214 * and it must always be possible to parse the label for any page whatever set
215 * getPageLabels() returns.
216 * <p>
217 * In many cases getPageLabels() can simply be implemented in terms of getPageLabel()
218 * though sometimes efficiency considerations (for example) may preclude that.
219 */
220 public abstract String getPageLabel(int pageNum);
221
222 /**Get an ordered array of page numbers that correspond index-by-index to the page labels returned by getPageLabels().
223 * If there are no more than MAX_RESULTS_PAGES in total than a contigious set of pages
224 * from 1 up to the getNumberOfPages() is returned,
225 * else some subset of pages is returned usually including the first and last items and
226 * the current page and its neighbours, in ascending order.
227 * <p>
228 * The array returned is private to the caller.
229 * <p>
230 * This can be overridden to vary the page distribution details though the general contract
231 * must be maintained.
232 */
233 public int[] getPageNumbers()
234 {
235 final int pageNum = pg;
236 final int totalPages = this.getNumberOfPages();
237
238 // If we can show all the page labels, do so as a contigious list.
239 if(totalPages <= WebConsts.MAX_RESULTS_PAGES)
240 {
241 final int ra[] = new int[totalPages];
242 for(int i = ra.length; --i >= 0; )
243 { ra[i] = i+1; }
244 return(ra);
245 }
246 // We have too many buttons to show, do a sub-set.
247 // Create a SortedSet of them to iterate over.
248 final SortedSet<Integer> pagesToShow = new TreeSet<Integer>();
249
250 // Always include:
251 // * first and last,
252 // * the current page,
253 // * the prev/next and onwards at exponentially greater steps.
254 // Then divide up the rest evenly.
255 pagesToShow.add(Integer.valueOf(1));
256 pagesToShow.add(Integer.valueOf(totalPages));
257 pagesToShow.add(Integer.valueOf(pageNum));
258
259 // Insert pages with exponential spacing from current page...
260 for(int interval = 1; interval < totalPages; interval *= 2)
261 {
262 int remainingPages = WebConsts.MAX_RESULTS_PAGES - pagesToShow.size();
263 if(remainingPages <= 0) { break; }
264 final int pDown = pageNum - interval;
265 if(pDown > 0) { pagesToShow.add(Integer.valueOf(pDown)); }
266
267 remainingPages = WebConsts.MAX_RESULTS_PAGES - pagesToShow.size();
268 if(remainingPages <= 0) { break; }
269 final int pUp = pageNum + interval;
270 if(pUp <= totalPages) { pagesToShow.add(Integer.valueOf(pUp)); }
271 }
272
273 // Insert remaining pages linearly between first and last pages,
274 // assuming them to have already been inserted.
275 int remainingPages = WebConsts.MAX_RESULTS_PAGES - pagesToShow.size();
276 if(remainingPages > 0)
277 {
278 final float interval = (totalPages-1) / (float) (remainingPages+1);
279 while(--remainingPages >= 0)
280 {
281 final int index = 1 +
282 Math.round((remainingPages + 1) * interval);
283
284 // Weed out any invalid page numbers we produce.
285 if((index < 1) || (index >= totalPages)) { continue; }
286
287 pagesToShow.add(Integer.valueOf(index));
288 }
289
290 // Mop up any remaining unused buttons/pages with items working out
291 // linearly from current page.
292 // We assume that the current page has always been included.
293 for(int gap = 1; gap < totalPages; ++gap)
294 {
295 // Do "up" first to slightly redress balance with
296 // exponential stepping above.
297 if((remainingPages = WebConsts.MAX_RESULTS_PAGES - pagesToShow.size()) <= 0) { break; }
298 final int pUp = pageNum + gap;
299 if(pUp <= totalPages) { pagesToShow.add(Integer.valueOf(pUp)); }
300
301 if((remainingPages = WebConsts.MAX_RESULTS_PAGES - pagesToShow.size()) <= 0) { break; }
302 final int pDown = pageNum - gap;
303 if(pDown > 0) { pagesToShow.add(Integer.valueOf(pDown)); }
304 }
305 }
306
307
308 // Convert our SortedSet to an ordered array of int page numbers.
309 final int ra[] = new int[pagesToShow.size()];
310 final Iterator<Integer> it = pagesToShow.iterator();
311 for(int i = 0; i < ra.length; ++i)
312 { ra[i] = (it.next()).intValue(); }
313 return(ra);
314 }
315
316 /**Gets ordered (unmodifiable) List of String labels/tokens for page buttons.
317 * Computes a set of pages and returns String values to use as the values (and legends)
318 * for submit buttons for this class to parse with the setPg(String) routine.
319 * <p>
320 * These labels have to be accepted by setPg(String).
321 * <p>
322 * The labels are unique and identify pages in monotonically-increasing order
323 * as you iterate through the List.
324 * <p>
325 * The labels generated must be deterministic and depend only on the attributes set for the bean,
326 * <strong>possibly including the current page</strong>.
327 * <p>
328 * Usually the first and last items in the List a labels for the first and last pages in the range.
329 * <p>
330 * Usually labels for the current page and those immediately before and after it are in the
331 * result list.
332 * <p>
333 * The base implementation calls getPageLabel() to generate the individual labels,
334 * so it may often be sufficient to override getPageLabel() only in a derived class.
335 * <p>
336 * This will commonly be overridden in deriving classes but the general contract
337 * must be maintained.
338 */
339 public List<String> getPageLabels()
340 {
341 final int ra[] = getPageNumbers();
342 final String sarr[] = new String[ra.length];
343 for(int i = 0; i < ra.length; ++i)
344 { sarr[i] = getPageLabel(ra[i]); }
345 return(Collections.unmodifiableList(Arrays.asList(sarr)));
346 }
347
348
349 /**Unique Serialisation class ID generated by http://random.hd.org/. */
350 private static final long serialVersionUID = -3756427234954341532L;
351 }