001    package hirondelle.web4j.ui.tag;
002    
003    import java.util.regex.Pattern;
004    import java.util.regex.Matcher;
005    import java.util.logging.Logger;
006    
007    import hirondelle.web4j.database.SqlId;
008    import hirondelle.web4j.Controller;
009    import hirondelle.web4j.util.Consts;
010    import hirondelle.web4j.util.Regex;
011    import hirondelle.web4j.util.Util;
012    import hirondelle.web4j.util.EscapeChars;
013    
014    /**
015     Generate links for paging through a long list of records.
016     
017     <P>With this class, paging is implemented in two steps :
018     <ul>
019       <li>emitting the proper links (with the proper request parameters) to identify each page 
020       <li>extracting the data from the database, using the given request parameters, and perhaps subsetting the <tt>ResultSet</tt>
021      </ul>
022      
023     The first step is performed by this tag, but the second step is not (see below). 
024     For an example of paging, please see the 'Discussion' feature of the <i>Fish &amp; Chips Club</i> application.
025     
026     <h3>Emitting Links For Paging</h3>
027     <P>This tag works by emitting various links which <i>are modifications of the current URI</i>.
028     
029     <P><b>Example</b>
030     <PRE>
031     &lt;w:pager pageFrom="PageIndex"&gt; 
032      &lt;a href="placeholder_first_link"&gt;First&lt;/a&gt; | 
033      &lt;a href="placeholder_next_link"&gt;Next&lt;/a&gt; | 
034      &lt;a href="placeholder_previous_link"&gt;Previous&lt;/a&gt; | 
035     &lt;/w:pager&gt;
036     </PRE>
037     
038     <P>The <tt>placeholder_xxx</tt> items act as placeholders. 
039     <em>They are replaced by this tag with modified values of the current URI.</em> 
040     The &lt;w:pager&gt; tag has a single <tt>pageFrom</tt> attribute.
041     It tells this tag which request parameter it will need to modify, in order to generate the various links. 
042     This modified request parameter represents the page index, in the range <tt>1..9999</tt> : for example, <tt>PageIndex=1</tt>, 
043     <tt>PageIndex=2</tt>, and so on.
044     
045     <P>For example, if the following URI displays "page 5" :
046     <PRE>http://www.blah.com/main/SomeAction.do?PageIndex=5&Criteria=Blah</PRE>
047     then this tag will let you emit the following links, derived from the above URI simply by replacing the value of the <tt>PageIndex</tt> request parameter :
048     <PRE>http://www.blah.com/main/SomeAction.do?PageIndex=1&Criteria=Blah
049    http://www.blah.com/main/SomeAction.do?PageIndex=6&Criteria=Blah
050    http://www.blah.com/main/SomeAction.do?PageIndex=4&Criteria=Blah</PRE>
051     
052      <P>Of course, these generated links don't do the actual subsetting of the data. 
053      Rather, these links simply <i>alter the current URI to use the desired value for the page index request parameter.</i> 
054      The resulting request parameters are then used elsewhere to extract the desired data (as described below).
055      
056     <P><b>Link Suppression</b>
057     <P>If the emission of a link would create a 'link to self', then this tag will not emit the link. 
058     For example, if you are already on the first page, then the First and Previous links will not be emitted as links - only the bare text, without the link, is emitted.  
059       
060     
061    <h3>Extracting and Subsetting the Data</h3>
062     There are three alternatives to implementing the subsetting of the <tt>ResultSet</tt> : 
063     <ul>
064       <li>in the JSP itself
065       <li>in the Data Access Object (DAO)
066       <li>directly in the underlying <tt>SELECT</tt> statement
067     </ul>
068     
069     <P><b>Subsetting in the JSP</b><br>
070    <ul>
071     <li>this is the simplest style
072     <li>it can be applied quickly, to add paging to any existing listing
073     <li>the DAO performs a single, unchanging <tt>SELECT</tt> that always returns the <i>same</i> set of records for all pages
074     <li>it requires only a single change to the {@link hirondelle.web4j.action.Action} - the addition of a 
075     <tt>public static final</tt> {@link hirondelle.web4j.request.RequestParameter} field for the page index parameter
076     <li>the JSP performs the subsetting to display a single page at a time, simply using <tt>&lt;c:out&gt;</tt>
077    </ul>
078    
079     The JSP performs the subsetting with simple JSTL.
080     In the following example, the page size is hard-coded to 25 items per page.
081     The <tt>&lt;c:out&gt;</tt> tag has <tt>begin</tt> and <tt>end</tt> attributes which can control the range of rendered items : 
082    <PRE>
083    &lt;c:forEach 
084      var="item" 
085      items="${items}" 
086      begin="${25 * (param.PageIndex - 1)}" 
087      end="${25 * param.PageIndex - 1}"
088    &gt;
089      ...display each item...
090    &lt;/c:forEach&gt;
091    </PRE>
092    
093    Note the different expressions for begin and end :
094    <PRE>begin = 25 * (PageIndex - 1)
095    end = (25 * PageIndex) - 1
096    </PRE>
097    
098    which give the following values :
099    <P>
100    <table border='1' cellspacing='0' cellpadding='3'>
101     <tr>
102      <th>Page</th>
103      <th>Begin</th>
104      <th>End</th>
105     </tr>
106     <tr>
107      <td>1</td>
108      <td>0</td>
109      <td>24</td>
110     </tr>
111     <tr>
112      <td>2</td>
113      <td>25</td>
114      <td>49</td>
115     </tr>
116     <tr>
117      <td>...</td>
118      <td>...</td>
119      <td>...</td>
120     </tr>
121    </table>
122    
123    <P>An alternate variation would be to allow the page <i>size</i> to also come from a request parameter :
124    <PRE>
125    &lt;c:forEach 
126      var="item" 
127      items="${items}" 
128      begin="${param.PageSize * (param.PageIndex - 1)}" 
129      end="${param.PageSize * param.PageIndex - 1}"
130    &gt;
131      ...display each line...
132    &lt;/c:forEach&gt;
133    </PRE>
134    
135    
136      <P><b>Subsetting in the DAO</b><br>
137    <ul>
138     <li>the full <tt>ResultSet</tt> is still returned by the <tt>SELECT</tt>, as in the JSP case above 
139     <li>however, this time the DAO performs the subsetting, using 
140     {@link hirondelle.web4j.database.Db#listRange(Class, SqlId, Integer, Integer, Object[])}. This avoids  
141     the cost of unnecessarily parsing records into Model Objects.
142     <li>the <tt>&lt;c:out&gt;</tt> tag does not use any <tt>begin</tt> and <tt>end</tt> attributes, since the subsetting 
143     has already been done.
144    </ul>
145     
146     <P><b>Subsetting in the SELECT</b><br>
147    <ul>
148     <li>the underlying <tt>SELECT</tt> is constructed to return only the desired records. (Such a <tt>SELECT</tt>
149     may be difficult to construct, according to the capabilities of the target database.)
150     <li>the <tt>SELECT</tt> will likely need two request parameters to form the proper query: a page index and
151     a page size, from which <tt>start</tt> and <tt>end</tt> indices may be calculated 
152     <li>the DAO and JSP are implemented as in any regular listing 
153    </ul>
154     
155    <P>Here are the kinds of calculations you may need when constructing such a SELECT statement   
156    <P>For items enumerated with a 0-based index :
157     <PRE>
158     start_index = page_size * (page_index - 1)
159     end_index = page_size * page_index - 1
160     </PRE>
161     
162     <P>For items enumerated with a 1-based index : 
163     <PRE>
164     start_index = page_size * (page_index - 1) + 1
165     end_index = page_size * page_index
166     </PRE>
167     
168     <h3>Setting In <tt>web.xml</tt></h3>
169     Note that there is a <tt>MaxRows</tt> setting in <tt>web.xml</tt> which controls the maximum number of records returned by 
170     a <tt>SELECT</tt>. 
171    */
172    public final class Pager extends TagHelper {
173    
174      /**
175       Name of request parameter that holds the index of the current page. 
176       
177       <P>This request parameter takes values in the range <tt>1..9999</tt>. 
178       
179        <P>(The name of this method is confusing. It should rather be named <tt>setPageIndex</tt>.)
180      */
181      public void setPageFrom(String aPageIndexParamName) {
182        checkForContent("PageFrom", aPageIndexParamName);
183        fPageIndexParamName = aPageIndexParamName.trim();
184        fPageIdxPattern = Pattern.compile(fPageIndexParamName + "=(?:\\d){1,4}");
185      }
186    
187      /** See class comment. */
188      @Override protected String getEmittedText(String aOriginalBody) {
189        fCurrentPageIdx = getNumericParamValue(fPageIndexParamName);
190        fCurrentURI = getCurrentURI();
191        fLogger.fine("Current URI: " + fCurrentURI);
192        fLogger.fine("Current Page : " + fCurrentPageIdx);
193        String firstURI = getFirstURI();
194        String nextURI = getNextURI();
195        String previousURI = getPreviousURI();
196        String result = getBodyWithUpdatedPlaceholders(aOriginalBody, firstURI, nextURI, previousURI);
197        return result;
198      }
199    
200      // PRIVATE //
201    
202      private String fPageIndexParamName;
203      private Pattern fPageIdxPattern;
204      private String fCurrentURI;
205      private int fCurrentPageIdx;
206      
207      private static final String NO_LINK = Consts.EMPTY_STRING;
208      private static final int FIRST_PAGE = 1;
209      private static final String PLACEHOLDER_FIRST_LINK = "placeholder_first_link";
210      private static final String PLACEHOLDER_NEXT_LINK = "placeholder_next_link";
211      private static final String PLACEHOLDER_PREVIOUS_LINK = "placeholder_previous_link";
212      /** Regex for an anchor tag 'A'. */
213      private static final Pattern LINK_PATTERN = Pattern.compile(Regex.LINK, Pattern.CASE_INSENSITIVE);
214      private static final Logger fLogger = Util.getLogger(Pager.class);
215    
216      private String getCurrentURI() {
217        return (String)getRequest().getAttribute(Controller.CURRENT_URI);
218      }
219    
220      private boolean isFirstPage() {
221        return fCurrentPageIdx == FIRST_PAGE;
222      }
223      
224      private int getNumericParamValue(String aParamName) {
225        String value = getRequest().getParameter(aParamName);
226        Integer result = Integer.valueOf(value);
227        if (result < 1) { 
228          throw new IllegalArgumentException("Param named " + Util.quote(aParamName) + " must be >= 1. Value: " + Util.quote(result) + ". Page Name: " + getPageName()); 
229        }
230        return result.intValue();
231      }
232    
233      private String getFirstURI() {
234        return isFirstPage() ? NO_LINK : forPage(FIRST_PAGE); 
235      }
236    
237      private String getNextURI() {
238        return  forPage(fCurrentPageIdx + 1); 
239      }
240      
241      private String getPreviousURI() {
242        return isFirstPage() ? NO_LINK : forPage(fCurrentPageIdx - 1); 
243      }
244      
245      private String forPage(int aNewPageIndex){
246        String result = null;
247        StringBuffer uri = new StringBuffer();
248        Matcher matcher = fPageIdxPattern.matcher(fCurrentURI);
249        while ( matcher.find() ){
250          matcher.appendReplacement(uri, getReplacement(aNewPageIndex));
251        }
252        matcher.appendTail(uri);
253        result = getResponse().encodeURL(uri.toString());
254        return EscapeChars.forHTML(result);
255      }
256      
257      private String getReplacement(int aNewPageIdx){
258        return EscapeChars.forReplacementString(fPageIndexParamName + "=" + aNewPageIdx);
259      }
260      
261      private String getBodyWithUpdatedPlaceholders(String aOriginalBody, String aFirstURI, String aNextURI, String aPreviousURI){
262        fLogger.fine("First URI: " + aFirstURI);
263        fLogger.fine("Next URI: " + aNextURI);
264        fLogger.fine("Previous URI: " + aPreviousURI);
265        
266        StringBuffer result = new StringBuffer();
267        Matcher matcher = LINK_PATTERN.matcher(aOriginalBody);
268        while ( matcher.find() ){
269          fLogger.finest("Getting href as first group.");
270          String href = matcher.group(Regex.FIRST_GROUP);
271          String replacement = null;
272          if ( PLACEHOLDER_FIRST_LINK.equals(href) ){
273            replacement = aFirstURI;
274          }
275          else if ( PLACEHOLDER_NEXT_LINK.equals(href) ){
276            replacement = aNextURI;
277          }
278          else if ( PLACEHOLDER_PREVIOUS_LINK.equals(href) ){
279            replacement = aPreviousURI;
280          }
281          else {
282            String message = "Body of pager tag can only contain links to special placeholder text. Page Name "+ getPageName();
283            fLogger.severe(message);
284            throw new IllegalArgumentException(message);
285          }
286          matcher.appendReplacement(result, getReplacementHref(matcher, replacement));
287        }
288        matcher.appendTail(result);
289        return result.toString();
290      }
291      
292      private String getReplacementHref(Matcher aMatcher, String aReplacementHref){
293        String result = null;
294        int LINK_BODY = 3;
295        int ATTRS_AFTER_HREF = 2;
296        if ( Util.textHasContent(aReplacementHref) ){
297          result = "<A HREF=\"" + aReplacementHref + "\" " + aMatcher.group(ATTRS_AFTER_HREF)+" >" + aMatcher.group(LINK_BODY) + "</A>";
298        }
299        else {
300          result = aMatcher.group(LINK_BODY);
301        }
302        return EscapeChars.forReplacementString(result);
303      }
304    }