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

Summary

Maintainability
D
2 days
Test Coverage
/* AbstractGroupsModel.java

    Purpose:
        
    Description:
        
    History:
        Tue Sep  2 08:45:01     2008, Created by tomyeh

Copyright (C) 2008 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.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.zkoss.io.Serializables;
import org.zkoss.zul.event.GroupsDataEvent;
import org.zkoss.zul.event.GroupsDataListener;
import org.zkoss.zul.ext.GroupsSelectableModel;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;

/**
 * A skeletal implementation for {@link GroupsModel}.
 * <p> Implements {@link Selectable} interface to handle the selection status.
 * (Since 6.0.0)
 * <p>Generics:
 * <dl>
 * <dt>D</dt><dd>The class of each data</dd>
 * <dt>H</dt><dd>The class of each group header</dd>
 * <dt>F</dt><dd>The class of each group footer</dd>
 * <dt>E</dt><dd>The class of each selection. It is the common base class
 * of D, H, F. In other words, D, H and F must extend from E.</dd>
 * </dl>
 * @author tomyeh
 * @since 3.5.0
 * @see Selectable
 */
public abstract class AbstractGroupsModel<D, H, F, E>
        implements GroupsModel<D, H, F>, GroupsSelectableModel<E>, java.io.Serializable {
    private transient List<GroupsDataListener> _listeners = new LinkedList<GroupsDataListener>();

    /** The current selection. */
    protected transient Set<E> _selection;
    private boolean _multiple;
    private SelectionControl<E> _ctrl;
    private boolean _groupSelectable;

    protected AbstractGroupsModel() {
        _selection = newEmptySelection();
        _ctrl = new DefaultSelectionControl<>(this);
    }

    /** Fires a {@link GroupsDataEvent} for all registered listener
     * (thru {@link #addGroupsDataListener}.
     *
     * <p>Note: you can invoke this method only in an event listener.
     */
    protected void fireEvent(int type, int groupIndex, int index0, int index1) {
        final GroupsDataEvent evt = new GroupsDataEvent(this, type, groupIndex, index0, index1);
        for (GroupsDataListener l : _listeners)
            l.onChange(evt);
    }

    public void setSelectionControl(SelectionControl ctrl) {
        _ctrl = ctrl;
    }

    public SelectionControl getSelectionControl() {
        return _ctrl;
    }

    //-- GroupsModel --//
    public void addGroupsDataListener(GroupsDataListener l) {
        if (l == null)
            throw new NullPointerException();
        _listeners.add(l);
    }

    public void removeGroupsDataListener(GroupsDataListener l) {
        _listeners.remove(l);
    }

    //Selectable//
    /** {@inheritDoc} */
    public Set<E> getSelection() {
        return Collections.unmodifiableSet(_selection);
    }

    /** {@inheritDoc} */
    public void setSelection(Collection<? extends E> selection) {
        if (!_selection.equals(selection)) {
            if (!_multiple && _selection.size() > 1)
                throw new IllegalArgumentException("Only one selection is allowed, not " + selection);
            _selection.clear();
            _selection.addAll(selection);
            if (selection.isEmpty()) {
                fireSelectionEvent(null);
            } else
                fireSelectionEvent(selection.iterator().next());
        }
    }

    /** {@inheritDoc} */
    public boolean isSelected(Object obj) {
        return _selection.contains(obj);
    }

    /** {@inheritDoc} */
    public boolean isSelectionEmpty() {
        return _selection.isEmpty();
    }

    /** {@inheritDoc} */
    public boolean addToSelection(E obj) {
        if (_selection.add(obj)) {
            if (!_multiple) {
                _selection.clear();
                _selection.add(obj);
            }
            fireSelectionEvent(obj);
            return true;
        }
        return false;
    }

    /** {@inheritDoc} */
    public boolean removeFromSelection(Object obj) {
        if (_selection.remove(obj)) {
            fireEvent(GroupsDataEvent.SELECTION_CHANGED, -1, -1, -1);
            return true;
        }
        return false;
    }

    /** {@inheritDoc} */
    public void clearSelection() {
        if (!_selection.isEmpty()) {
            _selection.clear();
            fireEvent(GroupsDataEvent.SELECTION_CHANGED, -1, -1, -1);
        }
    }

    /**
     * Selectable's implementor use only.
     * <p> Fires a selection event for component to scroll into view. The override
     * subclass must put the index0 of {@link #fireEvent(int, int, int, int)} as 
     * the view index to scroll. By default, the value -1 is assumed which means
     * no scroll into view.
     * <p> The method is invoked when both methods are invoked. {@link #addToSelection(Object)}
     * and {@link #setSelection(Collection)}.
     * @param e selected object.
     */
    protected void fireSelectionEvent(E e) {
        fireEvent(GroupsDataEvent.SELECTION_CHANGED, -1, -1, -1);
    }

    /**Removes the selection of the given collection.
     */
    protected void removeAllSelection(Collection<?> c) {
        _selection.removeAll(c);
    }

    /**Removes the selection that doesn't belong to the given collection.
     */
    protected void retainAllSelection(Collection<?> c) {
        _selection.retainAll(c);
    }

    /** {@inheritDoc} */
    public boolean isMultiple() {
        return _multiple;
    }

    /** {@inheritDoc} */
    public void setMultiple(boolean multiple) {
        if (_multiple != multiple) {
            _multiple = multiple;
            fireEvent(GroupsDataEvent.MULTIPLE_CHANGED, -1, -1, -1);

            if (!multiple && _selection.size() > 1) {
                E v = _selection.iterator().next();
                _selection.clear();
                _selection.add(v);
                fireEvent(GroupsDataEvent.SELECTION_CHANGED, -1, -1, -1);
            }
        }
    }

    public boolean isGroupSelectable() {
        return _groupSelectable;
    }

    public void setGroupSelectable(boolean groupSelectable) {
        _groupSelectable = groupSelectable;
    }

    /** Instantiation an empty set of the section.
     * It is used to initialize {@link #_selection}.
     * <p>By default, it instantiates an instance of LinkedHashMap.
     * The deriving class might override to instantiate a different class.
     */
    protected Set<E> newEmptySelection() {
        return new LinkedHashSet<E>();
    }

    /** Writes {@link #_selection}.
     * <p>Default: write it directly. Override it if E is not serializable.
     */
    protected void writeSelection(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.writeObject(_selection);
    }

    /** Reads back {@link #_selection}.
     * <p>Default: write it directly. Override it if E is not serializable.
     */
    @SuppressWarnings("unchecked")
    protected void readSelection(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        _selection = (Set<E>) s.readObject();
    }

    /**
     * A default selection control implementation for {@link AbstractGroupsModel},
     * by default it assumes all elements are selectable.
     * <p>Note: the implementation is not used for a huge data model, if in this case,
     * please implement your own one to speed up.</p>
     * @since 8.0.0
     */
    public static class DefaultSelectionControl<E> implements SelectionControl<E> {
        private AbstractGroupsModel<?, ?, ?, E> model;

        public DefaultSelectionControl(AbstractGroupsModel<?, ?, ?, E> model) {
            this.model = model;
        }

        public boolean isSelectable(E e) {
            return true;
        }

        public void setSelectAll(boolean selectAll) {
            if (selectAll) {
                boolean isGroupSelectable = model.isGroupSelectable();
                List<E> all = new LinkedList<>();
                for (int i = 0, j = model.getGroupCount(); i < j; i++) {
                    if (isGroupSelectable) {
                        Object group = model.getGroup(i);
                        if (isSelectable((E) group)) {
                            all.add((E) group);
                        }
                        for (int childIndex = 0, childSize = model
                                .getChildCount(i); childIndex < childSize; childIndex++) {
                            Object child = model.getChild(i, childIndex);
                            if (isSelectable((E) child)) {
                                all.add((E) child);
                            }
                        }
                        if (model.hasGroupfoot(i)) {
                            group = model.getGroupfoot(i);
                            if (isSelectable((E) group)) {
                                all.add((E) group);
                            }
                        }
                    } else {
                        for (int childIndex = 0, childSize = model
                                .getChildCount(i); childIndex < childSize; childIndex++) {
                            Object child = model.getChild(i, childIndex);
                            if (isSelectable((E) child)) {
                                all.add((E) child);
                            }
                        }
                    }
                }

                // avoid scroll into view at client side.
                model.fireEvent(GroupsDataEvent.DISABLE_CLIENT_UPDATE, -1, -1, -1);
                try {
                    ((Selectable<E>) model).setSelection(all);
                } finally {
                    model.fireEvent(GroupsDataEvent.ENABLE_CLIENT_UPDATE, -1, -1, -1);
                }
            } else {
                ((Selectable) model).clearSelection();
            }
        }

        public boolean isSelectAll() {
            Selectable smodel = (Selectable) model;
            boolean isGroupSelectable = model.isGroupSelectable();
            for (int i = 0, j = model.getGroupCount(); i < j; i++) {
                if (isGroupSelectable) {
                    Object group = model.getGroup(i);
                    if (isSelectable((E) group) && !smodel.isSelected(group)) {
                        return false;
                    }
                    for (int childIndex = 0, childSize = model.getChildCount(i); childIndex < childSize; childIndex++) {
                        Object child = model.getChild(i, childIndex);
                        if (isSelectable((E) child) && !smodel.isSelected(child)) {
                            return false;
                        }
                    }
                    if (model.hasGroupfoot(i)) {
                        group = model.getGroupfoot(i);
                        if (isSelectable((E) group) && !smodel.isSelected(group)) {
                            return false;
                        }
                    }
                } else {
                    for (int childIndex = 0, childSize = model.getChildCount(i); childIndex < childSize; childIndex++) {
                        Object child = model.getChild(i, childIndex);
                        if (isSelectable((E) child) && !smodel.isSelected(child)) {
                            return false;
                        }
                    }
                }
            }
            return true;
        }
    }

    //Serializable//
    private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.defaultWriteObject();

        writeSelection(s);
        Serializables.smartWrite(s, _listeners);
    }

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

        readSelection(s);
        _listeners = new LinkedList<GroupsDataListener>();
        Serializables.smartRead(s, _listeners);
    }

    @SuppressWarnings("unchecked")
    public Object clone() {
        final AbstractGroupsModel clone;
        try {
            clone = (AbstractGroupsModel) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
        clone._listeners = new LinkedList<GroupsDataListener>();
        clone._selection = clone.newEmptySelection();
        clone._selection.addAll(_selection);
        return clone;
    }
}