/* * #%L * carewebframework * %% * Copyright (C) 2008 - 2016 Regenstrief Institute, Inc. * %% * Licensed under the Apache 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.apache.org/licenses/LICENSE-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. * * This Source Code Form is also subject to the terms of the Health-Related * Additional Disclaimer of Warranty and Limitation of Liability available at * * http://www.carewebframework.org/licensing/disclaimer. * * #L% */ package org.carewebframework.ui.zk; import java.io.Serializable; import java.lang.reflect.Method; import java.util.Comparator; import java.util.List; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.event.SortEvent; import org.zkoss.zul.Column; import org.zkoss.zul.Grid; import org.zkoss.zul.Listbox; import org.zkoss.zul.Listheader; import org.zkoss.zul.Tree; import org.zkoss.zul.Treecol; import org.zkoss.zul.impl.HeaderElement; /** * Serves as a generic comparator for list and grid displays. Includes a number of static methods * for auto-wiring comparators into sortable header elements. */ public class RowComparator implements Comparator<Object>, Serializable { /** * Convenience method for auto-wiring list box headers. * <p> * {@link #autowireColumnComparators(List)} * * @param listbox List box whose headers are to be auto-wired. */ public static void autowireColumnComparators(Listbox listbox) { autowireColumnComparators(listbox.getListhead()); } /** * Convenience method for auto-wiring grid headers. * <p> * {@link #autowireColumnComparators(List)} * * @param grid Grid whose columns are to be auto-wired. */ public static void autowireColumnComparators(Grid grid) { autowireColumnComparators(grid.getColumns()); } /** * Convenience method for auto-wiring tree columns. * <p> * {@link #autowireColumnComparators(List)} * * @param tree Tree whose columns are to be auto-wired. */ public static void autowireColumnComparators(Tree tree) { autowireColumnComparators(tree.getTreecols()); } /** * Auto-wire all children of the parent (may be null). * * @param parent The parent component. */ private static void autowireColumnComparators(Component parent) { if (parent != null) { autowireColumnComparators(parent.getChildren()); } } /** * Automatically wires column headers to generic comparators. * <p> * The getter method for each header element is derived either from a custom attribute named * "getter" or, absent that, from the element's id. This value may either be a property name or * the name of the getter method itself. This method is used when comparing values across rows * under that header. If getter is specified for a header, no comparator is generated for that * header. * <p> * Custom comparators may be specified in a custom attribute named "comparator" on the header * element. This attribute may be an object instance that implements the Comparator interface or * the name of a class that implements the Comparator interface. * <p> * Finally, if a header element has been marked with the custom attribute named "natural", that * header element will be used to establish the "natural" ordering of the model. The value of * the attribute may be either "ascending" or "descending" (default is "ascending" if any other * value is specified) to indicate the direction of the natural ordering. When this attribute is * present, the sort toggle becomes tri-state, toggling from "ascending" to "descending" to * "natural" in that order. * * @param headers List of headers (of type Column, Listheader, or Treecol). */ public static void autowireColumnComparators(List<Component> headers) { Component defaultSortHeader = getDefaultSortHeader(headers); SortListener sortListener = defaultSortHeader == null ? null : new SortListener(defaultSortHeader); for (Component cmp : headers) { if (cmp instanceof HeaderElement) { String getter = getGetter(cmp); if (getter == null) { continue; } Comparator<?> comparator = getCustomComparator(cmp); RowComparator asc = new RowComparator(true, getter, comparator); RowComparator dsc = new RowComparator(false, getter, comparator); if (sortListener != null) { cmp.addEventListener(Events.ON_SORT, sortListener); } if (cmp instanceof Column) { ((Column) cmp).setSortAscending(asc); ((Column) cmp).setSortDescending(dsc); } else if (cmp instanceof Listheader) { ((Listheader) cmp).setSortAscending(asc); ((Listheader) cmp).setSortDescending(dsc); } else if (cmp instanceof Treecol) { ((Treecol) cmp).setSortAscending(asc); ((Treecol) cmp).setSortDescending(dsc); } } else { throw new IllegalArgumentException("Component not a supported type: " + cmp.getClass().getName()); } } } /** * Sort event listener that intercepts sort events to implement tri-state sort toggling. */ private static class SortListener implements EventListener<SortEvent> { private final Component sortDefault; public SortListener(Component sortDefault) { this.sortDefault = sortDefault; } @Override public void onEvent(SortEvent event) throws Exception { Component cmp = event.getTarget(); if (cmp instanceof Column) { Column header = (Column) cmp; if ("descending".equals(header.getSortDirection())) { header.setSortDirection("natural"); header = (Column) sortDefault; header.sort(event.isAscending()); header.setSortDirection("natural"); event.stopPropagation(); } } else if (cmp instanceof Listheader) { Listheader header = (Listheader) cmp; if ("descending".equals(header.getSortDirection())) { header.setSortDirection("natural"); header = (Listheader) sortDefault; header.sort(event.isAscending()); header.setSortDirection("natural"); event.stopPropagation(); } } else if (cmp instanceof Treecol) { Treecol header = (Treecol) cmp; if ("descending".equals(header.getSortDirection())) { header.setSortDirection("natural"); header = (Treecol) sortDefault; header.sort(event.isAscending()); header.setSortDirection("natural"); event.stopPropagation(); } } } }; /** * If the component has a custom attribute named "getter", that value is returned. Otherwise, * the component's id is assumed to be either a property name or the getter method name. If the * former, assumes the getter method is getXxxx. * * @param component Component used to derive getter method. * @return Null if the component has no id and no getter attribute, or has a ZK-generated id. * Otherwise, if the id is prefixed with a standard getter prefix ("get", "has", "is"), * it is assumed to be the name of the getter method. Lacking such a prefix, a prefix of * "get" is prepended to the case-adjusted id to obtain the getter method. */ private static String getGetter(Component component) { if (component.hasAttribute("getter")) { return (String) component.getAttribute("getter"); } String id = component.getId(); if (id == null || id.isEmpty() || id.startsWith("z")) { return null; } String lc = id.toLowerCase(); if (lc.startsWith("is") || lc.startsWith("has") || lc.startsWith("get")) { return StringUtils.uncapitalize(id); // is the getter name } return "get".concat(StringUtils.capitalize(id)); // infer getter name } /** * Returns an optional custom comparator from a component. A custom comparator is specific in * the custom attribute named "comparator" and may be an instance of a Comparator or the name of * a class implementing Comparator. * * @param component Component whose custom comparator is sought. * @return A custom comparator, if any. */ private static Comparator<?> getCustomComparator(Component component) { Object cmpr = component.getAttribute("comparator"); if (cmpr instanceof String) { try { Class<?> clazz = ClassUtils.getClass((String) cmpr); if (Comparator.class.isAssignableFrom(clazz)) { cmpr = clazz.newInstance(); } } catch (Exception e) { cmpr = null; } } return cmpr instanceof Comparator ? (Comparator<?>) cmpr : null; } /** * Gets the header element marked as governing the "natural" sort order of the model. * * @param headers The list of headers to search. * @return The header that has been designated for default sorting, or null if none. */ private static Component getDefaultSortHeader(List<Component> headers) { for (Component header : headers) { if (header.hasAttribute("natural")) { return header; } } return null; } private static final Log log = LogFactory.getLog(RowComparator.class); private static final long serialVersionUID = 1L; private final boolean _asc; private final String _getter; private final Comparator<Object> _customComparator; /** * Constructs a row comparator. * * @param asc If true, an ascending comparator is created. If false, a descending comparator is * created. * @param beanProperty This is the name of the getter method that will be used to retrieve a * value from the underlying model object for comparison. */ public RowComparator(boolean asc, String beanProperty) { this(asc, beanProperty, null); } /** * Constructs a row comparator. * * @param asc If true, an ascending comparator is created. If false, a descending comparator is * created. * @param getter This is the name of the getter method that will be used to retrieve a value * from the underlying model object for comparison. * @param customComparator Optional custom comparator. */ @SuppressWarnings("unchecked") public RowComparator(boolean asc, String getter, Comparator<?> customComparator) { _asc = asc; _getter = getter; _customComparator = (Comparator<Object>) customComparator; } /** * Performs a comparison between two objects. If the objects implement the Comparable interface, * that method is used. Otherwise, the objects are converted to their string representations and * that method is used. Null values are handled. */ @SuppressWarnings("unchecked") @Override public int compare(Object o1, Object o2) { Object v1 = getValue(o1), v2 = getValue(o2); int result; if (v1 == null && v2 == null) { result = 0; } else if (v2 == null) { result = -1; } else if (v1 == null) { result = 1; } else if (_customComparator != null) { result = _customComparator.compare(v1, v2); } else if (v1 instanceof Comparable && !(v1 instanceof String)) { result = ((Comparable<Object>) v1).compareTo(v2); } else { result = v1.toString().compareToIgnoreCase(v2.toString()); } return _asc ? result : -result; } /** * Gets a value from the model object using the getter method. * * @param o The model object. * @return Value returned by the getter method. */ private Object getValue(Object o) { // Special case that returns the model object itself, not a property of the object. if ("this".equals(_getter)) { return o; } // Otherwise, use getter to retrieve a property value from the model object. try { Object[] params = null; Method method = o.getClass().getMethod(_getter, (Class<?>[]) params); return method.invoke(o, params); } catch (Exception e) { log.error(e.getMessage(), e); return null; } } }