/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/jsf/trunk/jsf-widgets/src/java/org/sakaiproject/jsf/renderer/PagerRenderer.java $ * $Id: PagerRenderer.java 116316 2012-11-13 21:31:50Z steve.swinsburg@gmail.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.jsf.renderer; import java.io.IOException; import java.text.MessageFormat; import java.util.Map; import java.util.MissingResourceException; import javax.faces.component.EditableValueHolder; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.jsf.util.LocaleUtil; import org.sakaiproject.jsf.util.RendererUtil; public class PagerRenderer extends Renderer { private static final Log log = LogFactory.getLog(PagerRenderer.class); private static final String BUNDLE_NAME = "org.sakaiproject.jsf.bundle.pager"; public void encodeBegin(FacesContext context, UIComponent component) throws IOException { if (!component.isRendered()) return; // get state ResponseWriter out = context.getResponseWriter(); String clientId = component.getClientId(context); //String formId = getFormId(context, component); int pageSize = getInt(context, component, "pageSize", 0); int totalItems = getInt(context, component, "totalItems", 0); int firstItem = getInt(context, component, "firstItem", 0); int lastItem = getInt(context, component, "lastItem", -1); if (log.isDebugEnabled()) log.debug("encodeBegin: firstItem=" + firstItem + ", pageSize=" + pageSize + ", value=" + getString(context, component, "value", null)); // in case we are rendering before decode()ing we need to adjust the states adjustState(context, component, firstItem, lastItem, pageSize, totalItems, firstItem, lastItem, pageSize); pageSize = getInt(context, component, "pageSize", 0); totalItems = getInt(context, component, "totalItems", 0); firstItem = getInt(context, component, "firstItem", 0); lastItem = getInt(context, component, "lastItem", -1); // get stuff for pageing buttons String idFirst = clientId+"_first"; String idPrev = clientId+"_prev"; String idNext = clientId+"_next"; String idLast = clientId+"_last"; String idPastItem = clientId+"_pastItem"; boolean renderFirst = getBoolean(context, component, "renderFirst", true); boolean renderPrev = getBoolean(context, component, "renderPrev", true); boolean renderNext = getBoolean(context, component, "renderNext", true); boolean renderLast = getBoolean(context, component, "renderLast", true); boolean renderPageSize = getBoolean(context, component, "renderPageSize", true); String labelFirst = getString(context, component, "textFirst", "|<"); String labelPrev = getString(context, component, "textPrev", "<"); String labelNext = getString(context, component, "textNext", ">"); String labelLast = getString(context, component, "textLast", ">|"); String textItem = getString(context, component, "textItem", "items"); String titleFirst = MessageFormat.format( getString(context, component, "titleFirst", "First {0} {1}"), pageSize, textItem); String titlePrev = MessageFormat.format( getString(context, component, "titlePrev", "Previous {0} {1}"), pageSize, textItem); String titleNext = MessageFormat.format( getString(context, component, "titleNext", "Next {0} {1}"), pageSize, textItem); String titleLast = MessageFormat.format( getString(context, component, "titleLast", "Last {0} {1}"), pageSize, textItem); // TODO: Do this elsewhere? (component vs renderer) boolean disabledFirst = (firstItem == 0); boolean disabledPrev = (firstItem == 0); boolean disabledNext = (pageSize == 0) || (firstItem + pageSize >= totalItems); boolean disabledLast = disabledNext; boolean accesskeys = getBoolean(context, component, "accesskeys", false); String accesskeyFirst = (accesskeys) ? "f" : null; String accesskeyPrev = (accesskeys) ? "p" : null; String accesskeyNext = (accesskeys) ? "n" : null; String accesskeyLast = (accesskeys) ? "l" : null; // get stuff for page size selection and display String textPageSize = getString(context, component, "textPageSize", "Show {0}"); String textPageSizeAll = getString(context, component, "textPageSizeAll", "all"); String pageSizesStr = getString(context, component, "pageSizes", "5,10,20,50,100"); String[] pageSizes = pageSizesStr.split(","); String idSelect = clientId+"_pageSize"; String textStatus; if (totalItems > 0) { textStatus = getString(context, component, "textStatus", "Viewing {0} to {1} of {2} {3}"); } else { textStatus = getString(context, component, "textStatusZeroItems", "Viewing 0 {3}"); } Object[] args = new Object[] {String.valueOf(firstItem+1), String.valueOf(lastItem), String.valueOf(totalItems), textItem}; textStatus = MessageFormat.format(textStatus, args); // prepare the dropdown for selecting the // TODO: Probably need to cache this for performance String onchangeHandler = "javascript:this.form.submit(); return false;"; String selectedValue = String.valueOf(pageSize); String[] optionTexts = new String[pageSizes.length+1]; String[] optionValues = new String[pageSizes.length+1]; for (int i=0; i<pageSizes.length; i++) { optionTexts[i] = MessageFormat.format(textPageSize, new Object[] {pageSizes[i]}); optionValues[i] = pageSizes[i]; } optionTexts[pageSizes.length] = MessageFormat.format(textPageSize, new Object[] {textPageSizeAll}); optionValues[pageSizes.length] = "0"; // Output HTML out.startElement("div", null); out.writeAttribute("class", "listNav", null); writeStatus(out, textStatus); writeButton(out, renderFirst, idFirst, labelFirst, disabledFirst, titleFirst, accesskeyFirst); writeButton(out, renderPrev, idPrev, labelPrev, disabledPrev, titlePrev, accesskeyPrev); writeSelect(out, renderPageSize, idSelect, optionTexts, optionValues, selectedValue, onchangeHandler); writeButton(out, renderNext, idNext, labelNext, disabledNext, titleNext, accesskeyNext); writeButton(out, renderLast, idLast, labelLast, disabledLast, titleLast, accesskeyLast); // hidden state that prevents browser reloads from re-performing actions // for example, if the user presses the button for the next page of items, // and then reloads the browser window. out.startElement("input", null); out.writeAttribute("type", "hidden", null); out.writeAttribute("name", idPastItem, null); out.writeAttribute("value", String.valueOf(firstItem), null); out.endElement("input"); out.endElement("div"); } /** Output status display */ private static void writeStatus(ResponseWriter out, String status) throws IOException { out.startElement("div", null); out.writeAttribute("class", "instruction", null); out.writeText(status, null); out.endElement("div"); } /** Output an HTML button */ private static void writeButton(ResponseWriter out, boolean render, String name, String label, boolean disabled, String title, String accesskey) throws IOException { if (!render) return; //SAK-22812 wrap each button with a fieldset and legend, for accessibility out.startElement("fieldset", null); out.startElement("legend", null); out.writeText(title, null); out.endElement("legend"); out.startElement("input", null); out.writeAttribute("type", "submit", null); out.writeAttribute("name", name, null); out.writeAttribute("value", label, null); // TODO: i18n if (!disabled) { out.writeAttribute("title", title, null); if (accesskey != null) out.writeAttribute("accesskey", accesskey, null); //out.writeAttribute("onclick", "javascript:this.form.submit(); return false;", null); } else { out.writeAttribute("disabled", "disabled", null); } out.endElement("input"); out.endElement("fieldset"); out.write("\n"); } /** Output an HTML drop-down select */ private static void writeSelect(ResponseWriter out, boolean render, String selectId, String[] optionTexts, String[] optionValues, String selectedValue, String onchangeHandler) throws IOException { if (!render) return; out.startElement("select", null); out.writeAttribute("name", selectId, null); out.writeAttribute("id", selectId, null); out.writeAttribute("onchange", onchangeHandler, null); out.write("\n"); for (int i=0; i<optionValues.length; i++) { String optionText = optionTexts[i]; String optionValue = optionValues[i]; out.startElement("option", null); if (optionValue.equals(selectedValue)) out.writeAttribute("selected", "selected", null); out.writeAttribute("value", optionValue, null); out.writeText(optionText, null); out.endElement("option"); out.write("\n"); } out.endElement("select"); out.write("\n"); } public void decode(FacesContext context, UIComponent component) { Map req = context.getExternalContext().getRequestParameterMap(); String clientId = component.getClientId(context); String idFirst = clientId+"_first"; String idPrev = clientId+"_prev"; String idNext = clientId+"_next"; String idLast = clientId+"_last"; String idSelect = clientId+"_pageSize"; String idPastItem = clientId+"_pastItem"; int firstItem = getInt(context, component, "firstItem", 0); int lastItem = getInt(context, component, "lastItem", 0); int pageSize = getInt(context, component, "pageSize", 0); int totalItems = getInt(context, component, "totalItems", 0); if (log.isDebugEnabled()) log.debug("decode: firstItem=" + firstItem + ", pageSize=" + pageSize + ", value=" + getString(context, component, "value", null)); int newFirstItem = firstItem; int newLastItem = lastItem; int newPageSize = pageSize; String str = (String) req.get(idPastItem); // only perform actions if the current firstItem from the // request matches the current firstItem state stored on the server. // Prevents browser reloads from performing the same action again. if (str != null && firstItem == Integer.valueOf(str).intValue()) { // TODO: Seperate decoding from calculations (renderer vs component) // check which button was pressed if (req.containsKey(idFirst)) { newFirstItem = 0; } else if (req.containsKey(idPrev)) { newFirstItem = Math.max(firstItem - pageSize, 0); } else if (req.containsKey(idNext)) { newFirstItem = Math.min(firstItem + pageSize, totalItems - 1); } else if (req.containsKey(idLast)) { int lastPage = (totalItems - 1) / pageSize; newFirstItem = lastPage * pageSize; } else if (req.containsKey(idSelect)) { newPageSize = Integer.parseInt((String)req.get(idSelect)); } } adjustState(context, component, firstItem, lastItem, pageSize, totalItems, newFirstItem, newLastItem, newPageSize); } private static String formatValue(int firstItem, int pageSize) { return firstItem + "," + pageSize; } /** * Save the new paging state back to the given component (adjusting firstItem and lastItem first if necessary) */ private static void adjustState(FacesContext context, UIComponent component, int firstItem, int lastItem, int pageSize, int totalItems, int newFirstItem, int newLastItem, int newPageSize) { // recalculate last item newLastItem = Math.min(newFirstItem + newPageSize, totalItems); if (newPageSize <= 0) { // if displaying all items newFirstItem = 0; newLastItem = totalItems; } // we don't count lastItem changing as a full state change (value of this component doesn't change) if (newLastItem != lastItem) RendererUtil.setAttribute(context, component, "lastItem", new Integer(newLastItem)); // send the newly changed values where they need to go if (newPageSize != pageSize) RendererUtil.setAttribute(context, component, "pageSize", new Integer(newPageSize)); if (newFirstItem != firstItem) RendererUtil.setAttribute(context, component, "firstItem", new Integer(newFirstItem)); // Set value, which causes registered valueChangeListener to be called EditableValueHolder evh = (EditableValueHolder) component; String newValue = formatValue(newFirstItem, newPageSize); Object oldValue = (String)evh.getValue(); if (!newValue.equals(oldValue)) { if (oldValue != null) { evh.setSubmittedValue(newValue); evh.setValid(true); } else { // Need to initialize value string based on initial parameters. if (log.isDebugEnabled()) log.debug("initializing value to " + newValue); evh.setValue(newValue); } } } /** * Retrieve an integer value from the component (or widget's * resource bundle if not set on the component). */ private static int getInt(FacesContext context, UIComponent component, String attrName, int def) { Object ret = getFromAttributeOrBundle(context, component, attrName); if (ret instanceof Integer) return ((Integer)ret).intValue(); if (ret instanceof String) return Integer.valueOf((String) ret).intValue(); return def; } /** * Retrieve an boolean value from the component (or widget's * resource bundle if not set on the component). */ private static boolean getBoolean(FacesContext context, UIComponent component, String attrName, boolean def) { Object ret = getFromAttributeOrBundle(context, component, attrName); if (ret instanceof Boolean) return ((Boolean)ret).booleanValue(); if (ret instanceof String) return Boolean.valueOf((String) ret).booleanValue(); return def; } /** * Get a named attribute from the component or the widget resource bundle. * @return The attribute value if it exists in the given component, * or the attribute value from this widget's resource bundle, or * the default if none of those exists. */ private static String getString(FacesContext context, UIComponent component, String attrName, String def) { String ret = (String) getFromAttributeOrBundle(context, component, attrName); if (ret != null) return ret; // otherwise, return the default return def; } /** * Return the attribute value; whether from plain attributes, * ValueBinding, or the widget resource bundle. */ private static Object getFromAttributeOrBundle(FacesContext context, UIComponent component, String name) { // first try the attributes and value bindings Object ret = RendererUtil.getAttribute(context, component, name); if (ret != null) return ret; // next try the widget resource bundle String str = null; try { str = LocaleUtil.getLocalizedString(context, BUNDLE_NAME, "pager_"+name); } catch (MissingResourceException e) { // Returning null is fine here. // TODO Distinguish between the dynamic variables we expect to find as an // attribute and the static settings we expect to find in a resource bundle, // rather than hiding which is which. } if (str != null && str.length() > 0) return str; return null; } }