zul/src/main/java/org/zkoss/zul/Column.java

Summary

Maintainability
F
1 wk
Test Coverage
/* Column.java

    Purpose:
        
    Description:
        
    History:
        Tue Oct 25 16:02:36     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.zkoss.lang.Classes;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.mesg.Messages;
import org.zkoss.zk.au.AuRequests;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SortEvent;
import org.zkoss.zk.ui.ext.Scopes;
import org.zkoss.zul.ext.GroupsSortableModel;
import org.zkoss.zul.ext.Sortable;
import org.zkoss.zul.impl.HeaderElement;
import org.zkoss.zul.mesg.MZul;

/**
 * A single column in a {@link Columns} element.
 * Each child of the {@link Column} element is placed in each successive
 * cell of the grid.
 * The column with the most child elements determines the number of rows
 * in each column.
 *
 * <p>The use of column is mainly to define attributes for each cell
 * in the grid.
 * 
 * <p>Default {@link #getZclass}: z-column. (since 3.5.0)
 *
 * @author tomyeh
 */
public class Column extends HeaderElement {
    private String _sortDir = "natural";
    private transient Comparator<?> _sortAsc, _sortDsc;
    private String _sortAscNm = "none";
    private String _sortDscNm = "none";
    private Object _value;
    private boolean _ignoreSort = false;
    private boolean _isCustomAscComparator = false;
    private boolean _isCustomDscComparator = false;

    static {
        addClientEvent(Column.class, Events.ON_SORT, CE_DUPLICATE_IGNORE);
        addClientEvent(Column.class, Events.ON_GROUP, CE_DUPLICATE_IGNORE);
        addClientEvent(Column.class, Events.ON_UNGROUP, CE_DUPLICATE_IGNORE);
    }

    public Column() {
    }

    public Column(String label) {
        super(label);
    }

    /* Constructs a grid header with label and image.
     *
     * @param lable the label. No label if null or empty.
     * @param src the URI of the image, or null to ignore.
     */
    public Column(String label, String src) {
        super(label, src);
    }

    /* Constructs a grid header with label, image and width.
     *
     * @param lable the label. No label if null or empty.
     * @param src the URI of the image. Ignored if null or empty.
     * @param width the width of the column. Ignored if null or empty.
     * @since 3.0.4
     */
    public Column(String label, String src, String width) {
        super(label, src);
        setWidth(width);
    }

    /** Returns the grid that contains this column. */
    public Grid getGrid() {
        final Component parent = getParent();
        return parent != null ? (Grid) parent.getParent() : null;
    }

    /** Returns the sort direction.
     * <p>Default: "natural".
     */
    public String getSortDirection() {
        return _sortDir;
    }

    /** Sets the sort direction. This does not sort the data, it only serves
     * as an indicator as to how the grid is sorted. (unless the grid has "autosort" attribute)
     *
     * <p>If you use {@link #sort(boolean)} to sort rows ({@link Row}),
     * the sort direction is maintained automatically.
     * If you want to sort it in customized way, you have to set the
     * sort direction manually.
     *
     * @param sortDir one of "ascending", "descending" and "natural"
     */
    public void setSortDirection(String sortDir) throws WrongValueException {
        if (sortDir == null
                || (!"ascending".equals(sortDir) && !"descending".equals(sortDir) && !"natural".equals(sortDir)))
            throw new WrongValueException("Unknown sort direction: " + sortDir);
        if (!Objects.equals(_sortDir, sortDir)) {
            _sortDir = sortDir;
            if (!"natural".equals(sortDir) && !_ignoreSort) {
                Grid grid = getGrid();
                if (grid != null && grid.isAutosort()) {
                    doSort("ascending".equals(sortDir));
                }
            }
            smartUpdate("sortDirection", _sortDir);
        }
    }

    /** Sets the type of the sorter.
     * You might specify either "auto", "auto(FIELD_NAME1[,FIELD_NAME2] ...)"(since 3.5.3),
     * "auto(<i>number</i>)" (since 5.0.6) or "none".
     *
     * <p>If "client" or "client(number)" is specified,
     * the sort functionality will be done by Javascript at client without notifying
     * to server, that is, the order of the component in the row is out of sync.
     * <ul>
     * <li> "client" : it is treated by a string</li>
     * <li> "client(number)" : it is treated by a number</li>
     * </ul>
     * <p>Note: client sorting cannot work in model case. (since 5.0.0)
     * 
     * <p>If "auto" is specified,
     * {@link #setSortAscending} and/or {@link #setSortDescending} 
     * are called with {@link RowComparator}, if
     * {@link #getSortDescending} and/or {@link #getSortAscending} are null.
     * If you assigned a comparator to them, it won't be affected.
     * The auto created comparator is case-insensitive.
     *
     * <p>If "auto(FIELD_NAME1, FIELD_NAME2, ...)" is specified,
     * {@link #setSortAscending} and/or {@link #setSortDescending} 
     * are called with {@link FieldComparator}, if
     * {@link #getSortDescending} and/or {@link #getSortAscending} are null.
     * If you assigned a comparator to them, it won't be affected.
     * The auto created comparator is case-sensitive.
     * 
     * <p>If "auto(LOWER(FIELD_NAME))" or "auto(UPPER(FIELD_NAME))" is specified,
     * {@link #setSortAscending} and/or {@link #setSortDescending} 
     * are called with {@link FieldComparator}, if
     * {@link #getSortDescending} and/or {@link #getSortAscending} are null.
     * If you assigned a comparator to them, it won't be affected.
     * The auto created comparator is case-insensitive.
     *
     * <p>If "auto(<i>number</i>)" is specified, 
     * {@link #setSortAscending} and/or {@link #setSortDescending} 
     * are called with {@link ArrayComparator}. Notice that the data must
     * be an array and the number-th element must be comparable ({@link Comparable}).
     *
     * <p>If "none" is specified, both {@link #setSortAscending} and
     * {@link #setSortDescending} are called with null.
     * Therefore, no more sorting is available to users for this column.
     * @throws IllegalAccessException 
     * @throws InstantiationException 
     * @throws ClassNotFoundException 
     * @since 3.5.3
     */
    public void setSort(String type) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        if (type == null)
            return;
        if (type.startsWith("client")) {
            setSortAscending(type);
            setSortDescending(type);
        } else if ("auto".equals(type)) {
            if (getSortAscending() == null)
                setSortAscending(new RowComparator(this, true, false, false));
            if (getSortDescending() == null)
                setSortDescending(new RowComparator(this, false, false, false));
        } else if (type.startsWith("auto")) {
            final int j = type.indexOf('(');
            final int k = type.lastIndexOf(')');
            if (j >= 0 && k >= 0) {
                final String name = type.substring(j + 1, k);
                char cc;
                int index = -1;
                if (name.length() > 0 && (cc = name.charAt(0)) >= '0' && cc <= '9')
                    if ((index = Integer.parseInt(name)) < 0)
                        throw new IllegalArgumentException("Nonnegative number is required: " + name);
                if (getSortAscending() == null || !_isCustomAscComparator) {
                    if (index < 0)
                        setSortAscending(new FieldComparator(name, true));
                    else
                        setSortAscending(new ArrayComparator(index, true));
                    _isCustomAscComparator = false;
                }
                if (getSortDescending() == null || !_isCustomDscComparator) {
                    if (index < 0)
                        setSortDescending(new FieldComparator(name, false));
                    else
                        setSortDescending(new ArrayComparator(index, false));
                    _isCustomDscComparator = false;
                }
            } else {
                throw new UiException("Unknown sort type: " + type);
            }
        } else if ("none".equals(type)) {
            setSortAscending((Comparator) null);
            setSortDescending((Comparator) null);
            setSortDirection("natural");
        }
    }

    /** Returns the ascending sorter, or null if not available.
     */
    public Comparator<?> getSortAscending() {
        return _sortAsc;
    }

    /** Sets the ascending sorter, or null for no sorter for
     * the ascending order.
     *
     * @param sorter the comparator used to sort the ascending order.
     * If you are using the group feature, you can pass an instance of
     * {@link GroupComparator} to have a better control.
     * If an instance of {@link GroupComparator} is passed,
     * {@link GroupComparator#compareGroup} is used to group elements,
     * and {@link GroupComparator#compare} is used to sort elements
     * with a group.
     * Otherwise, {@link Comparator#compare} is used to group elements
     * and sort elements within a group.
     */
    public void setSortAscending(Comparator<?> sorter) {
        if (!Objects.equals(_sortAsc, sorter)) {
            _sortAsc = sorter;
            _isCustomAscComparator = _sortAsc != null;
            String nm = _isCustomAscComparator ? "fromServer" : "none";
            if (!_sortAscNm.equals(nm)) {
                _sortAscNm = nm;
                smartUpdate("sortAscending", _sortAscNm);
            }
        }
    }

    /** Sets the ascending sorter with the class name, or null for
     * no sorter for the ascending order.
     */
    public void setSortAscending(String clsnm)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        if (!Strings.isBlank(clsnm) && clsnm.startsWith("client") && !_sortAscNm.equals(clsnm)) {
            _sortAscNm = clsnm;
            smartUpdate("sortAscending", clsnm);
        } else
            setSortAscending(toComparator(clsnm));
    }

    /** Returns the descending sorter, or null if not available.
     */
    public Comparator<?> getSortDescending() {
        return _sortDsc;
    }

    /** Sets the descending sorter, or null for no sorter for the
     * descending order.
     *
     * @param sorter the comparator used to sort the descending order.
     * If you are using the group feature, you can pass an instance of
     * {@link GroupComparator} to have a better control.
     * If an instance of {@link GroupComparator} is passed,
     * {@link GroupComparator#compareGroup} is used to group elements,
     * and {@link GroupComparator#compare} is used to sort elements
     * with a group.
     * Otherwise, {@link Comparator#compare} is used to group elements
     * and sort elements within a group.
     */
    public void setSortDescending(Comparator<?> sorter) {
        if (!Objects.equals(_sortDsc, sorter)) {
            _sortDsc = sorter;
            _isCustomDscComparator = _sortDsc != null;
            String nm = _isCustomDscComparator ? "fromServer" : "none";
            if (!_sortDscNm.equals(nm)) {
                _sortDscNm = nm;
                smartUpdate("sortDescending", _sortDscNm);
            }
        }
    }

    /** Sets the descending sorter with the class name, or null for
     * no sorter for the descending order.
     */
    public void setSortDescending(String clsnm)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        if (!Strings.isBlank(clsnm) && clsnm.startsWith("client") && !_sortDscNm.equals(clsnm)) {
            _sortDscNm = clsnm;
            smartUpdate("sortDescending", clsnm);
        } else
            setSortDescending(toComparator(clsnm));
    }

    private Comparator<?> toComparator(String clsnm)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        if (clsnm == null || clsnm.length() == 0)
            return null;

        final Page page = getPage();
        final Class cls = page != null ? page.resolveClass(clsnm) : Classes.forNameByThread(clsnm);
        if (cls == null)
            throw new ClassNotFoundException(clsnm);
        if (!Comparator.class.isAssignableFrom(cls))
            throw new UiException("Comparator must be implemented: " + clsnm);
        return (Comparator<?>) cls.newInstance();
    }

    /** Sorts the rows ({@link Row}) based on {@link #getSortAscending}
     * and {@link #getSortDescending}, if {@link #getSortDirection} doesn't
     * matches the ascending argument.
     *
     * <p>It checks {@link #getSortDirection} to see whether sorting
     * is required, and update {@link #setSortDirection} after sorted.
     * For example, if {@link #getSortDirection} returns "ascending" and
     * the ascending argument is false, nothing happens.
     * To enforce the sorting, you can invoke {@link #setSortDirection}
     * with "natural" before invoking this method.
     * Alternatively, you can invoke {@link #sort(boolean, boolean)} instead.
     *
     * <p>It sorts the rows by use of {@link Components#sort}, if not live
     * data (i.e., {@link Grid#getModel} is null).
     *
     * <p>On the other hand, it invokes {@link Sortable#sort} to sort
     * the rows, if live data (i.e., {@link Grid#getModel} is not null).
     * In other words, if you use the live data, you have to implement
     * {@link Sortable} to sort the live data explicitly.
     *
     * @param ascending whether to use {@link #getSortAscending}.
     * If the corresponding comparator is not set, it returns false
     * and does nothing.
     * @return whether the rows are sorted.
     * @exception UiException if {@link Grid#getModel} is not
     * null but {@link Sortable} is not implemented.
     */
    public boolean sort(boolean ascending) {
        final String dir = getSortDirection();
        if (ascending) {
            if ("ascending".equals(dir))
                return false;
        } else {
            if ("descending".equals(dir))
                return false;
        }

        return doSort(ascending);
    }
    
    /** Sorts the rows ({@link Row}) based on {@link #getSortAscending}
     * and {@link #getSortDescending}.
     *
     * @param ascending whether to use {@link #getSortAscending}.
     * If the corresponding comparator is not set, it returns false
     * and does nothing.
     * @param force whether to enforce the sorting no matter what the sort
     * direction ({@link #getSortDirection}) is.
     * If false, this method is the same as {@link #sort(boolean)}.
     * @return whether the rows are sorted.
     */
    public boolean sort(boolean ascending, boolean force) {
        if (force)
            setSortDirection("natural");
        return sort(ascending);
    }

    /*package*/ boolean doSort(boolean ascending) {
        final Comparator<?> cmpr = ascending ? _sortAsc : _sortDsc;
        if (cmpr == null)
            return false;

        final Grid grid = getGrid();
        if (grid == null)
            return false;
        final Rows rows = grid.getRows();
        if (rows == null)
            return false;

        //comparator might be zscript
        Scopes.beforeInterpret(this);
        try {
            final ListModel<?> model = grid.getModel();
            boolean isPagingMold = grid.inPagingMold();
            int activePg = isPagingMold ? grid.getPaginal().getActivePage() : 0;
            if (model != null) { //live data
                if (model instanceof GroupsSortableModel) {
                    sortGroupsModel(grid, (GroupsSortableModel) model, cmpr, ascending);
                } else {
                    if (!(model instanceof Sortable))
                        throw new UiException("Sortable must be implemented in " + model.getClass());
                    sortListModel((Sortable) model, cmpr, ascending);
                }
            } else { //not live data
                sort0(grid, cmpr);
            }
            if (isPagingMold)
                grid.getPaginal().setActivePage(activePg);
            // Because of maintaining the number of the visible item, we cause
            // the wrong active page when dynamically add/remove the item (i.e. sorting).
            // Therefore, we have to reset the correct active page.
        } finally {
            Scopes.afterInterpret();
        }
        fixDirection(grid, ascending);

        // sometimes the items at client side are out of date
        grid.getRows().invalidate();
        return true;
    }

    @SuppressWarnings("unchecked")
    private void sortGroupsModel(Grid grid, GroupsSortableModel model, Comparator cmpr, boolean ascending) {
        model.sort(cmpr, ascending, grid.getColumns().getChildren().indexOf(this));
    }

    @SuppressWarnings("unchecked")
    private void sortListModel(Sortable model, Comparator cmpr, boolean ascending) {
        model.sort(cmpr, ascending);
    }

    /** Sorts the rows. If with group, each group is sorted independently.
     */
    @SuppressWarnings("unchecked")
    private static void sort0(Grid grid, Comparator cmpr) {
        final Rows rows = grid.getRows();
        if (rows.hasGroup())
            for (Group g : rows.getGroups()) {
                int index = g.getIndex() + 1;
                Components.sort(rows.getChildren(), index, index + g.getItemCount(), cmpr);
            }
        else
            Components.sort(rows.getChildren(), cmpr);
    }

    private void fixDirection(Grid grid, boolean ascending) {
        _ignoreSort = true;
        //maintain
        for (Iterator it = grid.getColumns().getChildren().iterator(); it.hasNext();) {
            final Column hd = (Column) it.next();
            hd.setSortDirection(hd != this ? "natural" : ascending ? "ascending" : "descending");
        }
        _ignoreSort = false;
    }

    /**
     * Groups and sorts the rows ({@link Row}) based on
     * {@link #getSortAscending}.
     * If the corresponding comparator is not set, it returns false
     * and does nothing.
     * 
     * @param ascending whether to use {@link #getSortAscending}.
     * If the corresponding comparator is not set, it returns false
     * and does nothing.
     * @return whether the rows are grouped.
     * @since 3.5.0
     */
    public boolean group(boolean ascending) {
        final String dir = getSortDirection();
        if (ascending) {
            if ("ascending".equals(dir))
                return false;
        } else {
            if ("descending".equals(dir))
                return false;
        }
        final Comparator<?> cmpr = ascending ? _sortAsc : _sortDsc;
        if (cmpr == null)
            return false;

        final Grid grid = getGrid();
        if (grid == null)
            return false;

        //comparator might be zscript
        Scopes.beforeInterpret(this);
        try {
            final ListModel model = grid.getModel();
            int index = grid.getColumns().getChildren().indexOf(this);
            if (model != null) { //live data
                if (!(model instanceof GroupsSortableModel))
                    throw new UiException(
                            GroupsSortableModel.class + " must be implemented in " + model.getClass().getName());
                groupGroupsModel((GroupsSortableModel) model, cmpr, ascending, index);
            } else { // not live data
                final Rows rows = grid.getRows();
                if (rows == null)
                    return false; //Avoid grid with null group        
                if (rows.hasGroup()) {
                    for (Group group : new ArrayList<Group>(rows.getGroups()))
                        group.detach(); // Groupfoot is removed automatically, if any.
                }

                Comparator<?> cmprx;
                if (cmpr instanceof GroupComparator) {
                    cmprx = new GroupToComparator((GroupComparator) cmpr);
                } else {
                    cmprx = cmpr;
                }

                final List<Component> children = new LinkedList<Component>(rows.getChildren());
                rows.getChildren().clear();
                sortCollection(children, cmprx);

                Row previous = null;
                for (Iterator<Component> it = children.iterator(); it.hasNext();) {
                    final Row row = (Row) it.next();
                    it.remove();
                    if (previous == null || compare(cmprx, previous, row) != 0) {
                        //new group
                        final List<Component> cells = row.getChildren();
                        if (cells.size() < index)
                            throw new IndexOutOfBoundsException("Index: " + index + " but size: " + cells.size());
                        Group group;
                        Component cell = cells.get(index);
                        if (cell instanceof Label) {
                            String val = ((Label) cell).getValue();
                            group = new Group(val);
                        } else {
                            Component cc = cell.getFirstChild();
                            if (cc instanceof Label) {
                                String val = ((Label) cc).getValue();
                                group = new Group(val);
                            } else {
                                group = new Group(Messages.get(MZul.GRID_OTHER));
                            }
                        }
                        rows.appendChild(group);
                    }
                    rows.appendChild(row);
                    previous = row;
                }

                if (cmprx != cmpr)
                    sort0(grid, cmpr); //need to sort each group
            }
        } finally {
            Scopes.afterInterpret();
        }

        fixDirection(grid, ascending);

        // sometimes the items at client side are out of date
        grid.getRows().invalidate();
        return true;
    }

    @SuppressWarnings("unchecked")
    private void groupGroupsModel(GroupsSortableModel model, Comparator cmpr, boolean ascending, int index) {
        model.group(cmpr, ascending, index);
    }

    @SuppressWarnings("unchecked")
    private static void sortCollection(List<Component> comps, Comparator cmpr) {
        Collections.sort(comps, cmpr);
    }

    @SuppressWarnings("unchecked")
    private static int compare(Comparator cmpr, Object a, Object b) {
        return cmpr.compare(a, b);
    }

    // super
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);

        if (!"none".equals(_sortDscNm))
            render(renderer, "sortDescending", _sortDscNm);

        if (!"none".equals(_sortAscNm))
            render(renderer, "sortAscending", _sortAscNm);

        if (!"natural".equals(_sortDir))
            render(renderer, "sortDirection", _sortDir);

        org.zkoss.zul.impl.Utils.renderCrawlableText(getLabel());
    }

    /** Returns the value.
     * <p>Default: null.
     * <p>Note: the value is application dependent, you can place
     * whatever value you want.
     * @since 3.6.0
     */
    @SuppressWarnings("unchecked")
    public <T> T getValue() {
        return (T) _value;
    }

    /** Sets the value.
     * @param value the value.
     * <p>Note: the value is application dependent, you can place
     * whatever value you want.
     * @since 3.6.0
     */
    public <T> void setValue(T value) {
        _value = value;
    }

    //-- event listener --//
    /**
     * Invokes a sorting action based on a {@link SortEvent} and maintains
     * {@link #getSortDirection}.
     * @since 5.0.8
     */
    public void onSort(SortEvent event) {
        sort(event.isAscending());
    }

    /**
     * Internal use only.
     * @since 6.5.0
     */
    public void onGroupLater(SortEvent event) {
        group(event.isAscending());
    }

    /**
     * Ungroups and sorts the rows ({@link Row}) based on the ascending.
     * If the corresponding comparator is not set, it returns false
     * and does nothing.
     * 
     * @param ascending whether to use {@link #getSortAscending}.
     * If the corresponding comparator is not set, it returns false
     * and does nothing.
     * @since 6.5.0
     */
    public void ungroup(boolean ascending) {
        final Comparator<?> cmpr = ascending ? _sortAsc : _sortDsc;
        if (cmpr != null) {

            final Grid grid = getGrid();
            if (grid.getModel() == null) {

                // comparator might be zscript
                Scopes.beforeInterpret(this);
                try {
                    final Rows rows = grid.getRows();
                    if (rows != null) {
                        if (rows.hasGroup()) {
                            for (Group group : new ArrayList<Group>(rows.getGroups()))
                                group.detach(); // Groupfoot is removed
                                                // automatically, if any.
                        }

                        Comparator<?> cmprx;
                        if (cmpr instanceof GroupComparator) {
                            cmprx = new GroupToComparator((GroupComparator) cmpr);
                        } else {
                            cmprx = cmpr;
                        }

                        final List<Component> children = new LinkedList<Component>(rows.getChildren());
                        rows.getChildren().clear();
                        sortCollection(children, cmprx);
                        for (Component c : children)
                            rows.appendChild(c);
                    }
                } finally {
                    Scopes.afterInterpret();
                }
            }
            fixDirection(grid, ascending);

            // sometimes the items at client side are out of date
            grid.getRows().invalidate();
        }
    }

    public String getZclass() {
        return _zclass == null ? "z-column" : _zclass;
    }

    //-- Component --//
    public void beforeParentChanged(Component parent) {
        if (parent != null && !(parent instanceof Columns))
            throw new UiException("Unsupported parent for column: " + parent);
        super.beforeParentChanged(parent);
    }

    /** Processes an AU request.
     * <p>Default: in addition to what are handled by its superclass, it also 
     * handles onSort.
     * @since 5.0.8
     */
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_SORT)) {
            SortEvent evt = SortEvent.getSortEvent(request);
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_GROUP)) {
            final Map<String, Object> data = request.getData();
            final boolean ascending = AuRequests.getBoolean(data, "");
            Events.postEvent(new SortEvent(cmd, this, ascending));

            // internal use, and it should be invoked after onGroup event.
            Events.postEvent(-1000, new SortEvent("onGroupLater", this, ascending));
        } else if (cmd.equals(Events.ON_UNGROUP)) {
            final Map<String, Object> data = request.getData();
            final boolean ascending = AuRequests.getBoolean(data, "");
            ungroup(ascending);
            Events.postEvent(new SortEvent(cmd, request.getComponent(), ascending));
        } else
            super.service(request, everError);
    }

    //Cloneable//
    public Object clone() {
        final Column clone = (Column) super.clone();
        clone.fixClone();
        return clone;
    }

    private void fixClone() {
        if (_sortAsc instanceof RowComparator) {
            final RowComparator c = (RowComparator) _sortAsc;
            if (c.getColumn() == this && c.isAscending())
                _sortAsc = new RowComparator(this, true, c.shallIgnoreCase(), false);
        }
        if (_sortDsc instanceof RowComparator) {
            final RowComparator c = (RowComparator) _sortDsc;
            if (c.getColumn() == this && !c.isAscending())
                _sortDsc = new RowComparator(this, false, c.shallIgnoreCase(), false);
        }
    }

    //Serializable//
    //NOTE: they must be declared as private
    private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.defaultWriteObject();

        boolean written = false;
        if (_sortAsc instanceof RowComparator) {
            final RowComparator c = (RowComparator) _sortAsc;
            if (c.getColumn() == this && c.isAscending()) {
                s.writeBoolean(true);
                s.writeBoolean(c.shallIgnoreCase());
                written = true;
            }
        }
        if (!written) {
            s.writeBoolean(false);
            if (_sortAsc instanceof Serializable) {
                s.writeObject(_sortAsc);
            } else if (_sortAsc != null) {
                throw new java.io.NotSerializableException(_sortAsc.getClass().getName());
            } else {
                s.writeObject(null);
            }
        }

        written = false;
        if (_sortDsc instanceof RowComparator) {
            final RowComparator c = (RowComparator) _sortDsc;
            if (c.getColumn() == this && !c.isAscending()) {
                s.writeBoolean(true);
                s.writeBoolean(c.shallIgnoreCase());
                written = true;
            }
        }
        if (!written) {
            s.writeBoolean(false);
            if (_sortDsc instanceof Serializable) {
                s.writeObject(_sortDsc);
            } else if (_sortDsc != null) {
                throw new java.io.NotSerializableException(_sortDsc.getClass().getName());
            } else {
                s.writeObject(null);
            }
        }
    }

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        boolean b = s.readBoolean();
        if (b) {
            final boolean igcs = s.readBoolean();
            _sortAsc = new RowComparator(this, true, igcs, false);
        } else {
            //bug #2830325 FieldComparator not castable to ListItemComparator
            _sortAsc = (Comparator<?>) s.readObject();
        }

        b = s.readBoolean();
        if (b) {
            final boolean igcs = s.readBoolean();
            _sortDsc = new RowComparator(this, false, igcs, false);
        } else {
            //bug #2830325 FieldComparator not castable to ListItemComparator
            _sortDsc = (Comparator<?>) s.readObject();
        }
    }

    private static class GroupToComparator implements Comparator {
        private final GroupComparator _gcmpr;

        private GroupToComparator(GroupComparator gcmpr) {
            _gcmpr = gcmpr;
        }

        @SuppressWarnings("unchecked")
        public int compare(Object o1, Object o2) {
            return _gcmpr.compareGroup(o1, o2);
        }
    }
}