/** * Copyright (c) 2009--2016 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package com.redhat.rhn.frontend.taglibs.list; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.jsp.JspException; import javax.servlet.jsp.PageContext; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.redhat.rhn.common.localization.LocalizationService; import com.redhat.rhn.common.util.DynamicComparator; import com.redhat.rhn.common.util.MethodUtil; import com.redhat.rhn.common.util.StringUtil; import com.redhat.rhn.frontend.html.HtmlTag; import com.redhat.rhn.frontend.struts.Expandable; import com.redhat.rhn.frontend.struts.RequestContext; /** * Provides a bunch of helper methods to make working with lists easier from a * custom tag POV. * * @version $Rev $ */ public class DataSetManipulator { private static Logger log = Logger.getLogger(DataSetManipulator.class); private static final String[] LINK_PREFIXES = {"_first", "_prev", "_next", "_last"}; private final int pageSize; private List dataset; private ListFilter filter; private String filterBy; private String filterValue; private int totalDataSetSize; private final HttpServletRequest request; private final String uniqueName; private int pageNumber = -1; private String alphaCol; private int alphaPosition = -1; private boolean ascending = true; private final int unfilteredDataSize; private final boolean parentIsAnElement; private String defaultSortAttribute; public static final String ICON_FIRST = "fa fa-angle-double-left"; public static final String ICON_PREV = "fa fa-angle-left"; public static final String ICON_NEXT = "fa fa-angle-right"; public static final String ICON_LAST = "fa fa-angle-double-right"; /** * Constructor * @param pageSizeIn page size of the list * @param datasetIn dataset to be displayed * @param requestIn HttpServletRequest of the caller * @param listNameIn name of the list * @param parentIsElement true of the parent value * in the list should be considered as an element * this is useful for tree like data */ public DataSetManipulator(int pageSizeIn, List datasetIn, HttpServletRequest requestIn, String listNameIn, boolean parentIsElement) { pageSize = pageSizeIn; dataset = datasetIn; request = requestIn; uniqueName = listNameIn; totalDataSetSize = dataset.size(); unfilteredDataSize = dataset.size(); parentIsAnElement = parentIsElement; } private List expand(List data) { List expanded = new LinkedList(); for (Object obj : data) { expanded.add(obj); if (obj instanceof Expandable) { Expandable ex = (Expandable) obj; List children = ex.expand(); expanded.addAll(children); } } return expanded; } /** * Get the total (non-filtered, non-paginated) dataset size * @return total size */ public int getTotalDataSetSize() { return totalDataSetSize; } /** * Find a page-worth of data * @return list representing one page of data */ public List getPage() { List retval = new LinkedList(); if (pageSize > 0) { int startOffset = getCurrentPageNumber() * pageSize; if (startOffset > dataset.size()) { startOffset = dataset.size() - 1; } if (startOffset < 0) { startOffset = 0; } int endOffset = startOffset + pageSize; if (endOffset > dataset.size()) { endOffset = dataset.size(); } retval = dataset.subList(startOffset, endOffset); } else { retval.addAll(dataset); } return expand(retval); } /** * Return everything in the list, not just enough for a single page * @return List representing all data available */ public List getAllData() { List retval = new LinkedList(); retval.addAll(dataset); return expand(retval); } /** * Returns the starting element index for a page (1 based) * @return int */ private int getPageStartIndex() { //no data ==> no start index... if (getTotalDataSetSize() == 0) { return 0; } int startOffset = getCurrentPageNumber() * pageSize; if (startOffset < 0) { startOffset = 0; } List parentList = dataset.subList(0, startOffset); List data = expand(parentList); int ret = data.size() + 1; if (!parentIsAnElement) { ret = ret - parentList.size(); } return ret; } /** * Returns the ending element index for a page (1 based) * @return int */ private int getPageEndIndex() { int startOffset = getCurrentPageNumber() * pageSize; if (startOffset < 0) { startOffset = 0; } int endOffset = startOffset + pageSize; if (endOffset > dataset.size()) { endOffset = dataset.size(); } List parentList = dataset.subList(0, endOffset); List data = expand(parentList); if (!parentIsAnElement) { return data.size() - parentList.size(); } return data.size(); } private int getExpandedDataSize() { if (!parentIsAnElement) { return expand(dataset).size() - dataset.size(); } return expand(dataset).size(); } /** * Returns the pagination message (1 - 2 of 3 for example) * @return the pagination message */ public String getPaginationMessage() { LocalizationService ls = LocalizationService.getInstance(); return ls.getMessage("message.range", getPageStartIndex(), getPageEndIndex(), getExpandedDataSize()); } /** * Determines the current page number based on URL params * @return current page number */ private int getCurrentPageNumber() { if (AlphaBarHelper.getInstance().isSelected(uniqueName, request)) { int pos = findAlphaPosition(); pageNumber = pos / pageSize; return pageNumber; } if (pageNumber == -1) { String param = null; param = getPaginationParam(request, uniqueName); String value = null; if (param != null && request.getParameter(param) != null) { value = request.getParameter(param); } if (value == null) { value = "0"; } else if (value.equalsIgnoreCase("first")) { value = "0"; } else if (value.equalsIgnoreCase("last")) { if (totalDataSetSize == 0) { value = "0"; } else { value = String.valueOf((totalDataSetSize - 1) / pageSize); } } try { pageNumber = Integer.parseInt(value); } catch (NumberFormatException e) { pageNumber = 0; } } return pageNumber; } /** * Binds information pertaining to pagination to the request */ public void bindPaginationInfo() { request.setAttribute("pageNum", String.valueOf(getCurrentPageNumber())); } /** * Returns the pagination param (|<, <, >, >|) that was selected * returns null if no pagination action was selected. * @param request the http servlet request * @param uniqueName the unique name of list to check * @return the selected pagination param or null if none was selected. */ static String getPaginationParam(ServletRequest request, String uniqueName) { for (int x = 0; x < LINK_PREFIXES.length; x++) { String imgLink = "list_" + uniqueName + "_page" + LINK_PREFIXES[x]; if (request.getParameter(imgLink) != null) { return "list_" + uniqueName + "_page" + LINK_PREFIXES[x]; } } return null; } /** * Returns the next page number * @return next page number or -1 if none */ public int getNextPageNumber() { int retval = -1; if (getCurrentPageNumber() == 0) { if (totalDataSetSize > pageSize) { retval = getCurrentPageNumber() + 1; } } else { if ((getCurrentPageNumber() * pageSize) + pageSize < (totalDataSetSize)) { retval = getCurrentPageNumber() + 1; } } return retval; } /** * Returns the previous page number * @return previous page number of -1 if none */ public int getPrevPageNumber() { int retval = -1; if (getCurrentPageNumber() > 0) { if (((getCurrentPageNumber() * pageSize) - pageSize) > -1) { retval = getCurrentPageNumber() - 1; } } return retval; } /** * Is the current page the first page? * @return answer to that burning question */ public boolean isFirstPage() { return getCurrentPageNumber() == 0; } /** * Is the current page the last page? * @return answer to that burning question */ public boolean isLastPage() { int maxPage = (dataset.size() / pageSize) - 1; // Add a page for overflow, since the dataset is not // evenly divisible by the pagesize if (dataset.size() % pageSize > 0) { maxPage++; } return getCurrentPageNumber() == maxPage; } /** * Sorts the dataset in place */ public void sort() { String sortKey = ListTagUtil.makeSortByLabel(uniqueName); String sortDirectionKey = ListTagUtil.makeSortDirLabel(uniqueName); String sortAttribute = StringUtils.defaultString(request.getParameter(sortKey)); String sortDir = StringUtils.defaultString(request.getParameter(sortDirectionKey)); if (!sortDir.equals(RequestContext.SORT_ASC) && !sortDir.equals(RequestContext.SORT_DESC)) { sortDir = ascending ? RequestContext.SORT_ASC : RequestContext.SORT_DESC; } if (AlphaBarHelper.getInstance().isSelected(uniqueName, request)) { Collections.sort(dataset, new DynamicComparator(alphaCol, RequestContext.SORT_ASC)); } else if (!StringUtils.isBlank(sortAttribute)) { try { Collections.sort(dataset, new DynamicComparator(sortAttribute, sortDir)); } catch (IllegalArgumentException iae) { log.warn("Unable to sort dataset according to: " + sortAttribute); Collections.sort(dataset, new DynamicComparator(defaultSortAttribute, sortDir)); } } else if (!StringUtils.isBlank(defaultSortAttribute)) { Collections.sort(dataset, new DynamicComparator(defaultSortAttribute, ascending ? RequestContext.SORT_ASC : RequestContext.SORT_DESC)); } } /** * Filters the dataset based on filter criteria * @param f ListFilter instance * @param context the page context to write to * @throws JspException if failure to write to pageContext */ public void filter(ListFilter f, PageContext context) throws JspException { String filterByKey = ListTagUtil.makeFilterByLabel(uniqueName); filterBy = request.getParameter(filterByKey); filterValue = ListTagHelper.getFilterValue(request, uniqueName); if (f == null || filterBy == null || filterBy.length() == 0 || filterValue == null || filterValue.length() == 0) { return; } filter = f; HtmlTag filterClass = new HtmlTag("input"); filterClass.setAttribute("type", "hidden"); filterClass.setAttribute("name", ListTagUtil.makeFilterClassLabel(uniqueName)); filterClass.setAttribute("value", f.getClass().getCanonicalName()); ListTagUtil.write(context, filterClass.render()); dataset = ListFilterHelper.filter(dataset, f, filterBy, filterValue); totalDataSetSize = dataset.size(); } /** * Builds a map of bog-standard pagination links complete with images * @return map (String, String[]) */ public Map getPaginationLinks() { Map links = new HashMap(); if (pageSize > 0 && dataset.size() > 0 && getTotalDataSetSize() > pageSize) { String pageLinkName = "list_" + uniqueName + "_page"; String[] data = new String[4]; if (!isFirstPage()) { data[0] = ICON_FIRST; data[1] = pageLinkName + "_first"; data[2] = "first"; data[3] = "First Page"; } else { data[0] = ICON_FIRST; data[1] = null; data[2] = null; data[3] = null; } links.put("allBackward", data); data = new String[4]; if (getPrevPageNumber() > -1) { data[0] = ICON_PREV; data[1] = pageLinkName + "_prev"; data[2] = String.valueOf(getPrevPageNumber()); data[3] = "Previous Page"; } else { data[0] = ICON_PREV; data[1] = null; data[2] = null; data[3] = null; } links.put("backward", data); data = new String[4]; if (getNextPageNumber() > -1) { data[0] = ICON_NEXT; data[1] = pageLinkName + "_next"; data[2] = String.valueOf(getNextPageNumber()); data[3] = "Next Page"; } else { data[0] = ICON_NEXT; data[1] = null; data[2] = null; data[3] = null; } links.put("forward", data); data = new String[4]; if (!isLastPage()) { data[0] = ICON_LAST; data[1] = pageLinkName + "_last"; data[2] = "last"; data[3] = "Last Page"; } else { data[0] = ICON_LAST; } links.put("allForward", data); } return links; } /** * Is the list empty? * @return boolean */ public boolean isListEmpty() { return dataset == null || dataset.size() == 0; } /** * Gets the set of characters that will be active on the alpha bar * @return the set of characters that are active */ public Set<Character> getAlphaBarIndex() { Set<Character> chars = new HashSet<Character>(); int i = 0; for (Object inputRow : dataset) { String value = getAlphaValue(inputRow); if (!StringUtils.isBlank(value)) { // Make sure that the alpha inputs are converted // to uppercase char val = value.charAt(0); val = Character.toUpperCase(val); if (!chars.contains(val)) { // add the character to the set chars.add(val); } } i++; } return chars; } private String getAlphaValue(Object inputRow) { String value; if (inputRow instanceof Map) { value = (String) ((Map) inputRow).get(alphaCol); } else { value = (String)MethodUtil.callMethod(inputRow, StringUtil.beanify("get " + alphaCol), new Object[0]); } return value; } /** * setter for the column that will be sorted when the alpha bar is used * @param col the column to set */ public void setAlphaColumn(String col) { alphaCol = col; } /** * Finds the first instance of an entry in DataSet that starts with the letter * "alphaPosition" * @return int the position within the DataSet of that entry */ public int findAlphaPosition() { AlphaBarHelper helper = AlphaBarHelper.getInstance(); if (helper.isSelected(uniqueName, request)) { if (alphaPosition > -1) { return alphaPosition; } char alpha = Character.toUpperCase(helper. getAlphaValue(uniqueName, request).charAt(0)); int i = 0; for (Object inputRow : dataset) { String value = getAlphaValue(inputRow); if (!StringUtils.isBlank(value)) { char val = value.charAt(0); val = Character.toUpperCase(val); if (val == alpha) { alphaPosition = i; return i; } } i++; } } return -1; } /** * * @return the defualt sort attribute */ public String getDefaultSortAttribute() { return defaultSortAttribute; } /** * * @param sortAttr the default sort attribute */ public void setDefaultSortAttribute(String sortAttr) { defaultSortAttribute = sortAttr; } /** * * @param asc the sort order */ public void setDefaultAscending(boolean asc) { ascending = asc; } /** * @return Returns the unfilteredDataSize. */ public int getUnfilteredDataSize() { return unfilteredDataSize; } }