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        }