/* * This software is distributed under the terms of the FSF * Gnu Lesser General Public License (see lgpl.txt). * * This program is distributed WITHOUT ANY WARRANTY. See the * GNU General Public License for more details. */ package com.scooterframework.orm.misc; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import com.scooterframework.common.util.Converters; import com.scooterframework.common.util.Util; import com.scooterframework.orm.sqldataexpress.processor.DataProcessor; import com.scooterframework.orm.sqldataexpress.util.SqlConstants; /** * <p>Paginator class manages pagination of records of a model object. </p> * * <p>Any information on a URL link can be passed to this object through * controlOptions parameter. </p> * * <h3>Specifying Paging <tt>controlOptions</tt></h3> * * <p>The following keys have an impact on the result of pagination:</p> * * <ul> * <li>key_limit "<tt>limit</tt>": specifies limit of a page. Default is 10.</li> * <li>key_offset "<tt>offset</tt>": specifies offset of a page. Default is 0.</li> * <li>key_npage "<tt>npage</tt>": specifies the number of the page to be opened by this click. Default is 1.</li> * <li>key_order_by "<tt>order_by</tt>": order by clause.</li> * <li>key_sort "<tt>sort</tt>": column to be sorted.</li> * <li>key_order "<tt>order</tt>": sort direction, either "up" (default) or "down".</li> * </ul> * * <p>The following keys are for information only: * <ul> * <li>key_cpage "<tt>cpage</tt>": specifies the origin page number of the current click. Default is 1.</li> * <li>key_link "<tt>r</tt>": specifies the origin place of the current click.</li> * </ul> * </p> * * <p>Notes: * <ol> * <li>When both <tt>npage</tt> and <tt>offset</tt> exists, the latter is * ignored as it will be derived from <tt>npage</tt>.</li> * <li>Either use key_order_by or use key_sort and key_order together.</li> * <li>If a key/value pair is not used by the paginator, it will reappear in * query string outputs.</li> * <li>In addition, all SQL related information can be passed to the * paginator through its PageListSource instance.</li> * </ol></p> * * <p>It is easier to specify paging <tt>controlOptions</tt> as a string:</p> * <pre> * //Skip the first 250 records and returns the next 50 records. * String controlOptions = "limit=50, offset=250"; * </pre> * * <p>Usage example:</p> * <pre> * Paginator page = new Paginator(new JdbcPageListSource(modelClass), controlOptions); * List pagedRecords = page.getRecordList(); * </pre> * * @author (Fei) John Chen */ public class Paginator { /** * <p>Constructs a Paginator object that manages a model entity's * pagination. Always recounts the total records.</p> * * <p>String controlOptions is a string of name and value pairs * separated by "=" sign. The default delimiter string to separate * name-value pairs is ",|&". </p> * * <p>String controlOptions may have the following format: </p> * <pre> * cpage=2,limit=10,... * or cpage=2|limit=10|... * or cpage=2&limit=10&... * </pre> * * @param pls PageListSource. * @param controlOptions String of control information. */ public Paginator(PageListSource pls, String controlOptions) { this.pls = pls; Map<String, String> map = new HashMap<String, String>(); map.putAll(Converters.convertStringToMap(controlOptions)); this.controlOptions = map; initialize(pls, this.controlOptions); } /** * <p>Constructs a Paginator object that manages a model entity's * pagination. Always recounts the total records.</p> * * @param pls PageListSource. * @param controlOptions Map of control information. */ public Paginator(PageListSource pls, Map<String, ?> controlOptions) { this.pls = pls; this.controlOptions = Converters.convertMapToMapSS(controlOptions); if (controlOptions == null) controlOptions = new HashMap<String, String>(); initialize(pls, this.controlOptions); } /** * Return maximum number of records per page */ public int getLimit() { return limit; } /** * Return offset */ public int getOffset() { return offset; } /** * Return the number of records on the current page */ public int getCurrentPageSize() { return (recordList != null)?recordList.size():0; } /** * Return total number of records */ public int getTotalCount() { return totalCount; } /** * Return total number of pages */ public int getPageCount() { return pageCount; } /** * Return origin page number. * * Page number starts from 1. */ public int getOriginPage() { return opage; } /** * Return current page number. * * Page number starts from 1. */ public int getCurrentPage() { return cpage; } /** * Return index number of the first record on the current page */ public int getStartIndex() { return getOffset() + 1; } /** * Return index number of the last record on the current page */ public int getEndIndex() { int endIndex = getOffset() + getLimit(); if (endIndex > getTotalCount()) endIndex = getTotalCount(); return endIndex; } /** * Return query string for link of the origin page */ public String getQueryStringOrigin() { StringBuilder qs = new StringBuilder(); qs.append("r=").append(ref); qs.append("&npage=").append(opage); qs.append("&limit=").append(limit); qs.append("&cpage=").append(cpage); appendExclude(qs, "r, npage, cpage, limit"); return qs.toString(); } /** * Return query string for link of a page * * pageNumber starts from 1 to total page count. */ public String getQueryStringPage(int pageNumber) { if (pageNumber > pageCount || pageNumber < 1) return ""; StringBuilder qs = new StringBuilder(); qs.append("r=").append(link_value_page); qs.append("&npage=").append(pageNumber); qs.append("&limit=").append(limit); qs.append("&cpage=").append(cpage); appendExclude(qs, "r, npage, cpage, limit"); return qs.toString(); } /** * Return query string for link "first" */ public String getQueryStringFirst() { if (cpage == 1) return ""; StringBuilder qs = new StringBuilder(); qs.append("r=").append(link_value_first); qs.append("&npage=1"); qs.append("&limit=").append(limit); qs.append("&cpage=").append(cpage); appendExclude(qs, "r, npage, cpage, limit"); return qs.toString(); } /** * Return query string for link "previous" */ public String getQueryStringPrevious() { if (!hasPreviousPage()) return ""; StringBuilder qs = new StringBuilder(); qs.append("r=").append(link_value_previous); qs.append("&npage=").append(cpage-1); qs.append("&limit=").append(limit); qs.append("&cpage=").append(cpage); appendExclude(qs, "r, npage, cpage, limit"); return qs.toString(); } /** * Return query string for link "next" */ public String getQueryStringNext() { if (!hasLastPage()) return ""; StringBuilder qs = new StringBuilder(); qs.append("r=").append(link_value_next); qs.append("&npage=").append(cpage+1); qs.append("&limit=").append(limit); qs.append("&cpage=").append(cpage); appendExclude(qs, "r, npage, cpage, limit"); return qs.toString(); } /** * Return query string for link "last" */ public String getQueryStringLast() { if (!hasLastPage()) return ""; StringBuilder qs = new StringBuilder(); qs.append("r=").append(link_value_last); qs.append("&npage=").append(pageCount); qs.append("&limit=").append(limit); qs.append("&cpage=").append(cpage); appendExclude(qs, "r, npage, cpage, limit"); return qs.toString(); } /** * Check if the paginator is on the first page. * @return true for first page */ public boolean isFirstPage() { return cpage == 1; } /** * Check if the paginator is on the last page. * @return true for last page */ public boolean isLastPage() { return cpage == pageCount; } /** * Check if there is link on previous page. * @return true for having link */ public boolean hasPreviousPage() { return cpage > 1; } /** * Check if there is link on last page. * @return true for having link */ public boolean hasLastPage() { return cpage < pageCount; } public List<?> getRecordList() { return recordList; } /** * <p>Sets html keys that do not have to appear in url. </p> * * <p><tt>excludedKeys</tt> consists of comma separated keys.</p> * * @param excludedKeys */ public void setExcludedKeys(String excludedKeys) { this.excludedKeys = excludedKeys; } public String toString() { String newLine = "\r\n"; StringBuilder sb = new StringBuilder(); sb.append(" limit: ").append(this.getLimit()).append(newLine); sb.append(" offset: ").append(this.getOffset()).append(newLine); sb.append(" total pages: ").append(this.getPageCount()).append(newLine); sb.append(" current page: ").append(this.getCurrentPage()).append(newLine); sb.append("total records: ").append(this.getTotalCount()).append(newLine); sb.append(" start index: ").append(this.getStartIndex()).append(newLine); sb.append(" end index: ").append(this.getEndIndex()).append(newLine); sb.append(" uri first: ").append(this.getQueryStringFirst()).append(newLine); sb.append(" uri previous: ").append(this.getQueryStringPrevious()).append(newLine); sb.append(" uri next: ").append(this.getQueryStringNext()).append(newLine); sb.append(" uri last: ").append(this.getQueryStringLast()).append(newLine); List<?> recordList = getRecordList(); if (recordList != null) { int count = 0; Iterator<?> it = recordList.iterator(); while(it.hasNext()) { count++; Object data = it.next(); sb.append("record #").append(count).append(" content: ").append(newLine); sb.append(data).append(newLine); } } return sb.toString(); } /** * Returns a Paginator instance for the next page. */ public Paginator nextPage() { Map<String, String> options = new HashMap<String, String>(); options.putAll(controlOptions); options.put(key_npage, cpage + 1 + ""); options.put(key_cpage, cpage + ""); options.remove(key_offset); return new Paginator(pls, options); } protected void initialize(PageListSource pls, Map<String, String> options) { limit = Util.getIntValue(options, key_limit, DEFAULT_PAGE_LIMIT); if (options.containsKey(key_npage)) { cpage = Util.getIntValue(options, key_npage, 1); offset = (cpage - 1) * limit; if (totalCounted && offset >= totalCount) offset = totalCount - limit; offset = (offset > 0)?offset:0; } else if (options.containsKey(key_offset)) { offset = Util.getIntValue(options, key_offset, 0); cpage = 1 + (offset/limit); } opage = Util.getIntValue(options, key_cpage, 1); ref = Util.getStringValue(options, key_link, ""); pls.setInputs(options);//in case some other SQL conditions such as order_by, sort, order pls.setLimit(limit); pls.setOffset(offset); pls.execute(); totalCount = pls.getTotalCount(); recordList = pls.getRecordList(); totalCounted = true; pageCount = countPages(totalCount); } protected int countPages(int totalRecords) { return (int)Math.ceil(totalRecords/(limit*1.0)); } /** * <p>Append all parameters in the options map except those in the * excludeList and internal parameters.</p> * * <p>Internal parameters have key names starting with value of * DataProcessor.framework_input_key_prefix.</p> * * @param qs * @param excludeString */ private void appendExclude(StringBuilder qs, String excludeString) { if (excludedKeys != null) { excludeString = (excludeString != null)? (excludeString + ", " + excludedKeys):excludedKeys; } List<String> excludeList = Converters.convertStringToList(excludeString); for (Map.Entry<String, String> entry : controlOptions.entrySet()) { String key = entry.getKey(); if (excludeList.contains(key) || key.startsWith(DataProcessor.framework_input_key_prefix) || key.startsWith("scooter.") || key.startsWith("_") || key.startsWith("org.mortbay.jetty")) continue; qs.append("&").append(key).append("=").append(entry.getValue()); } } public static final String key_link = "r"; public static final String link_value_page = "page"; public static final String link_value_first = "first"; public static final String link_value_previous = "previous"; public static final String link_value_next = "next"; public static final String link_value_last = "last"; public static final String key_limit = "limit"; public static final String key_offset = "offset"; public static final String key_cpage = "cpage"; public static final String key_npage = "npage"; /** * <p>Key <tt>group_by</tt> represents <tt>GROUP BY</tt> clause in SQL. </p> * * <p>For example, "<tt>group_by=id, name</tt>" will be translated to SQL * query as "GROUP BY id, name".</p> */ public static final String key_group_by = SqlConstants.key_group_by; /** * <p>Key <tt>having</tt> represents <tt>HAVING</tt> clause in SQL. This is * usually used with <tt>group_by</tt> together.</p> * * <p>The <tt>HAVING</tt> clause was added to SQL because the * <tt>WHERE</tt> keyword could not be used with aggregate functions.</p> * * <p>For example, "<tt>having=sum(price)<100</tt>" will be translated to * sql query as "HAVING sum(price)<100".</p> */ public static final String key_having = SqlConstants.key_having; /** * <p>Key <tt>order_by</tt> represents <tt>ORDER BY</tt> clause in SQL. </p> * * <p>For example, "<tt>order_by=age desc</tt>" will be translated to SQL * query as "ORDER BY age desc".</p> */ public static final String key_order_by = SqlConstants.key_order_by; /** * <p>Key <tt>sort</tt> indicates column names to sort. </p> * * <p>For example, "<tt>sort=first_name</tt>" will be translated to sql * query as "<tt>order by first_name</tt>".</p> */ public static final String key_sort = SqlConstants.key_sort; /** * <p>Key <tt>order</tt> represents direction of sort. If the query * result set is in descending order, use value "<tt>Down</tt>". Otherwise * by default the query results are in ascending order.</p> */ public static final String key_order = SqlConstants.key_order; protected PageListSource pls; protected Map<String, String> controlOptions = new HashMap<String, String>(); public static final int DEFAULT_PAGE_LIMIT = DataProcessor.DEFAULT_PAGINATION_LIMIT; /** * Maximum number of records per page */ protected int limit = DEFAULT_PAGE_LIMIT; /** * Number of records to skip */ protected int offset = 0; /** * Link reference */ protected String ref = ""; /** * Origin page number */ protected int opage = 1; /** * Current page number */ protected int cpage = 1; /** * New page number */ protected int npage = 1; /** * Sort column name */ protected String sort = ""; /** * Sort order direction: up or down */ protected String order = "up"; /** * Total number of records */ protected int totalCount; /** * Indicates whether totalCount has been calculated. */ private boolean totalCounted = false; /** * Total number of pages */ protected int pageCount; /** * paged record list */ protected List<?> recordList; protected String excludedKeys; }