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

Summary

Maintainability
F
6 days
Test Coverage
/* AbstractTreeModel.java

    Purpose:
        
    Description:
        
    History:
        Aug 10 2007, Created by Jeff Liu

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.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.io.Serializables;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.event.TreeDataEvent;
import org.zkoss.zul.event.TreeDataListener;
import org.zkoss.zul.ext.Openable;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;
import org.zkoss.zul.ext.TreeOpenableModel;
import org.zkoss.zul.ext.TreeSelectableModel;

/**
 * A skeletal implementation for {@link TreeModel}.
 * 
 * <p>{@link AbstractTreeModel} implements both {@link TreeSelectableModel}
 * and {@link TreeOpenableModel}. In other words, it stores the selection
 * and open states, such that {@link Tree} and other UI can interact with.
 *
 * <p>In additions, {@link AbstractTreeModel} also implements
 * {@link Selectable} and {@link Openable} to simplify the access
 * (and provides backward compatibility to ZK 5 and earlier).
 * However, these two interfaces are optional and designed for application.
 * {@link Tree} and all ZK core don't access it at all.
 *
 * <p>For introduction, please refer to <a href=
 * "http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVC/Model/Tree_Model"
 * >ZK Developer's Reference: Tree Model</a>.
 * 
 * @author jumperchen
 * @author tomyeh
 * @since 3.0.0
 */
public abstract class AbstractTreeModel<E> implements TreeModel<E>, TreeSelectableModel, TreeOpenableModel,
        Selectable<E>, Openable<E>, java.io.Serializable, PageableModel {

    private static final Logger log = LoggerFactory.getLogger(AbstractTreeModel.class);

    /**
     * The root object to be return by method {@link #getRoot()}.
     */
    private E _root;
    private transient List<TreeDataListener> _listeners = new LinkedList<TreeDataListener>();
    private transient List<PagingListener> _pagingListeners = new ArrayList<PagingListener>();
    /** The selection. */
    protected Set<Path> _selection = new LinkedHashSet<Path>();
    /** The open information. */
    protected Set<Path> _opens = new LinkedHashSet<Path>();
    private boolean _multiple;

    private SelectionControl<E> _ctrl;

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

    public SelectionControl getSelectionControl() {
        return _ctrl;
    }

    /**
     * Creates a {@link AbstractTreeModel}.
     * 
     * @param root
     *            root of tree
     */
    public AbstractTreeModel(E root) {
        _root = root;
        //ZK-2611: only for TreeDataEvent.INTERVAL_REMOVED and INTERVAL_ADDED:
        addTreeDataListener(new TreeDataListener() {
            public void onChange(TreeDataEvent event) {
                updatePath(event);
                invalidatePageCount();
            }
        });
        _ctrl = createDefaultSelectionControl();
    }

    public List<TreeDataListener> getTreeDataListeners() {
        return _listeners;
    }

    private SelectionControl<E> createDefaultSelectionControl() {
        String clznm = Library.getProperty("org.zkoss.zul.AbstractTreeModel.DefaultSelectionControl.class");
        if (clznm != null) {
            try {
                return (SelectionControl<E>) Classes.newInstanceByThread(clznm, new Class[] { AbstractTreeModel.class },
                        new Object[] { this });
            } catch (Exception e) {
                UiException.Aide.wrap(e, "Can't initialize SelectionControl");
            }
        }
        return new DefaultSelectionControl<>(this);
    }

    private void updatePath(TreeDataEvent event) {
        final int type = event.getType();
        final int[] affectedPath = event.getAffectedPath();
        if (affectedPath == null || affectedPath.length < 1)
            return;
        switch (type) {
        case TreeDataEvent.INTERVAL_REMOVED:
            internalDataChange(_opens, affectedPath, true);
            internalDataChange(_selection, affectedPath, true);
            break;
        case TreeDataEvent.INTERVAL_ADDED:
            internalDataChange(_opens, affectedPath, false);
            internalDataChange(_selection, affectedPath, false);
            break;
        }
    }

    private void internalDataChange(Set<Path> openOrSelect, int[] affectedPath, boolean isRemoved) {
        List<Path> list = new LinkedList<Path>(openOrSelect);
        int update = isRemoved ? -1 : 1;
        for (Iterator<Path> it = list.iterator(); it.hasNext();) {
            Path p = it.next();
            // p.verifyPrefix should be execute anyway
            if (p.verifyPrefix(affectedPath, update) && isRemoved) {
                it.remove();
            }
        }
        openOrSelect.clear();
        openOrSelect.addAll(list);
    }

    /**
     * Return the root of the tree model.
     */
    public E getRoot() {
        return _root;
    }

    /** Sets the root of the tree model.
     */
    /*package*/ void setRootDirectly(E root) {
        _root = root;
    }

    /**
     * Fires a {@link TreeDataEvent} for all registered listener
     * 
     * <p>
     * Note: you can invoke this method only in an event listener.
     * @since 6.0.0
     */
    public void fireEvent(int evtType, int[] path, int indexFrom, int indexTo) {
        final TreeDataEvent evt = new TreeDataEvent(this, evtType, path, indexFrom, indexTo);
        for (TreeDataListener l : _listeners)
            l.onChange(evt);
    }

    /**
     * Has the same functionality with {@link #fireEvent(int, int[], int, int)},
     * while this is used for node removal only
     *
     * @since 7.0.5
     */
    public void fireEvent(int evtType, int[] path, int indexFrom, int indexTo, int[] affectedPath) {
        final TreeDataEvent evt = new TreeDataEvent(this, evtType, path, indexFrom, indexTo, affectedPath);
        for (TreeDataListener l : _listeners)
            l.onChange(evt);
    }

    /**
     * Has the same functionality with {@link #fireEvent(int, int[], int, int)},
     * while this is used for node removal only
     *
     * @since 10.0.0
     */
    public void fireEvent(int evtType, int[] path, int indexFrom, int indexTo, int[][] affectedPaths) {
        final TreeDataEvent evt = new TreeDataEvent(this, evtType, path, indexFrom, indexTo, affectedPaths);
        for (TreeDataListener l : _listeners)
            l.onChange(evt);
    }

    /**
     * Fires a {@link TreeDataEvent} for all registered listener when selection
     * status has changed.
     * 
     * @since 6.0.0
     */
    protected void fireSelectionChanged(int[] path) {
        final TreeDataEvent evt = new TreeDataEvent(this, TreeDataEvent.SELECTION_CHANGED, path, 0, 1);
        for (TreeDataListener l : _listeners)
            l.onChange(evt);
    }

    /**
     * Fires a {@link TreeDataEvent} for all registered listener when open
     * status has changed.
     * 
     * @since 6.0.0
     */
    protected void fireOpenChanged(int[] path) {
        final TreeDataEvent evt = new TreeDataEvent(this, TreeDataEvent.OPEN_CHANGED, path, 0, 1);
        for (TreeDataListener l : _listeners)
            l.onChange(evt);
    }

    /**
     * Returns the index of child in parent. If either parent or child is null,
     * returns -1. If either parent or child don't belong to this tree model,
     * returns -1.
     * <p>
     * The default implementation iterates through all children of
     * <code>parent</code> by invoking, and check if <code>child</code> is part
     * of them. You could override it if you have a better algorithm.
     * {@link #getChild}
     * 
     * @param parent
     *            a node in the tree, obtained from this data source
     * @param child
     *            the node we are interested in
     * @return the index of the child in the parent, or -1 if either child or
     *         parent are null or don't belong to this tree model
     * @since 5.0.6
     */
    public int getIndexOfChild(E parent, E child) {
        final int cnt = getChildCountOptimized(parent);
        for (int j = 0; j < cnt; ++j)
            if (Objects.equals(child, getChild(parent, j)))
                return j;
        return -1;
    }

    public E getChild(int[] path) {
        E node = getRoot();
        for (int childCount = 0, i = 0; i < path.length && node != null; i++) {
            if (path[i] < 0 || path[i] > (childCount = getChildCountOptimized(node)))
                return null;
            node = getChild(node, path[i]);
        }
        return node;
    }

    //Retrieves the child count. Note: it won't call getChildCount if isLeaf
    private int getChildCountOptimized(E parent) {
        return isLeaf(parent) ? 0 : getChildCount(parent);
    }

    /**
     * Returns the path from the specified child.
     * This implementation looks for the child by traversing every possible
     * child (deep-first).
     * It is suggested to override this method for better performance,
     * if there is a better algorithm.
     * @since 6.0.0 
     */
    public int[] getPath(E child) {
        final List<Integer> path = new ArrayList<Integer>();
        dfSearch(path, getRoot(), child);

        final int[] ipath = new int[path.size()];
        for (int j = 0; j < ipath.length; j++)
            ipath[j] = path.get(j);
        return ipath;
    }

    private boolean dfSearch(List<Integer> path, E node, E target) {
        if (node.equals(target))
            return true;

        for (int i = 0, size = getChildCountOptimized(node); i < size; i++)
            if (dfSearch(path, getChild(node, i), target)) {
                path.add(0, new Integer(i));
                return true;
            }
        return false;
    }

    // -- TreeModel --//

    public void addTreeDataListener(TreeDataListener l) {
        _listeners.add(l);
    }

    public void removeTreeDataListener(TreeDataListener l) {
        _listeners.remove(l);
    }

    //TreeSelectableModel//

    public void setMultiple(boolean multiple) {
        if (_multiple != multiple) {
            _multiple = multiple;
            fireEvent(TreeDataEvent.MULTIPLE_CHANGED, null, -1, -1);

            if (!multiple && _selection.size() > 1) {
                final List<Path> sels = new LinkedList<Path>();
                sels.addAll(_selection);
                final Path sel = sels.remove(0);
                _selection.clear();
                _selection.add(sel);

                for (final Path path : sels)
                    fireSelectionChanged(path.path);
            }
        }
    }

    public boolean isMultiple() {
        return _multiple;
    }

    // TreeSelectableModel

    public boolean addSelectionPath(int[] path) {
        if (path != null && path.length > 0) {
            final int[][] paths = new int[1][path.length];
            paths[0] = path;
            return addSelectionPaths(paths);
        }
        return false;
    }

    public boolean addSelectionPaths(int[][] paths) {
        boolean added = false;
        final int len = paths != null ? paths.length : 0;
        final boolean multiple = isMultiple();
        for (int j = 0; j < len; ++j)
            if (paths[j] != null) {
                final Path path = new Path(paths[j]);
                if (multiple) {
                    if (_selection.add(path)) {
                        added = true;
                        fireSelectionChanged(path.path);
                    }
                } else {
                    if (!_selection.contains(path)) {
                        added = true;
                        clearSelection();
                        _selection.add(path);
                        fireSelectionChanged(path.path);
                    }
                    break; //done
                }
            }
        return added;
    }

    public boolean removeSelectionPath(int[] path) {
        if (path != null && path.length > 0) {
            final int[][] paths = new int[1][path.length];
            paths[0] = path;
            return removeSelectionPaths(paths);
        }
        return false;
    }

    public boolean removeSelectionPaths(int[][] paths) {
        boolean found = false;
        final int len = paths != null ? paths.length : 0;
        for (int j = 0; j < len && !_selection.isEmpty(); ++j) {
            final Path path = new Path(paths[j]);
            if (_selection.remove(path)) {
                found = true;
                fireSelectionChanged(path.path);
            }
            if (!isMultiple())
                break;
        }
        return found;
    }

    public boolean isPathSelected(int[] path) {
        return path != null && _selection.contains(new Path(path));
    }

    public int[] getSelectionPath() {
        return _selection.isEmpty() ? null : _selection.iterator().next().path;
    }

    public int[][] getSelectionPaths() {
        if (_selection.isEmpty())
            return null;

        final int[][] paths = new int[_selection.size()][];
        int j = 0;
        for (final Path path : _selection)
            paths[j++] = path.path;
        return paths;
    }

    public int getSelectionCount() {
        return _selection.size();
    }

    public boolean isSelectionEmpty() {
        return _selection.isEmpty();
    }

    public void clearSelection() {
        if (!_selection.isEmpty()) {
            final int[][] paths = getSelectionPaths();
            if (paths != null) {
                _selection.clear();
                for (int j = 0; j < paths.length; ++j)
                    fireSelectionChanged(paths[j]);
            }
        }
    }

    // TreeOpenableModel //

    public boolean addOpenPath(int[] path) {
        if (path != null && path.length > 0) {
            final int[][] paths = new int[1][path.length];
            paths[0] = path;
            return addOpenPaths(paths);
        }
        return false;
    }

    public boolean addOpenPaths(int[][] paths) {
        boolean added = false;
        final int len = paths != null ? paths.length : 0;
        for (int j = 0; j < len; ++j) {
            if (paths[j] != null) {
                final Path path = new Path(paths[j]);
                if (_opens.add(path)) {
                    added = true;
                    fireOpenChanged(path.path);
                }
            }
        }
        return added;
    }

    public boolean removeOpenPath(int[] path) {
        if (path != null && path.length > 0) {
            final int[][] paths = new int[1][path.length];
            paths[0] = path;
            return removeOpenPaths(paths);
        }
        return false;
    }

    public boolean removeOpenPaths(int[][] paths) {
        boolean found = false;
        final int len = paths != null ? paths.length : 0;
        for (int j = 0; j < len && !_opens.isEmpty(); ++j) {
            final Path path = new Path(paths[j]);
            if (_opens.remove(path)) {
                found = true;
                fireOpenChanged(path.path);
            }
        }
        return found;
    }

    public boolean isPathOpened(int[] path) {
        return path != null && _opens.contains(new Path(path));
    }

    public int[] getOpenPath() {
        return _opens.isEmpty() ? null : _opens.iterator().next().path;
    }

    public int[][] getOpenPaths() {
        if (_opens.isEmpty())
            return null;

        final int[][] paths = new int[_opens.size()][];
        int j = 0;
        for (final Path path : _opens)
            paths[j++] = path.path;
        return paths;
    }

    public int getOpenCount() {
        return _opens.size();
    }

    public boolean isOpenEmpty() {
        return _opens.isEmpty();
    }

    public void clearOpen() {
        if (!_opens.isEmpty()) {
            final int[][] paths = getOpenPaths();
            if (paths != null) {
                _opens.clear();
                for (int j = 0; j < paths.length; ++j)
                    fireOpenChanged(paths[j]);
            }
        }
    }

    /** A utility that the deriving class can call to save the states
     * before sorting the model.
     * <p>Default: saves the selection and open states.
     * <p>For example, {@link DefaultTreeModel#sort} invokes it to preserve
     * the selection to the same objects (rather than the same paths).
     * @since 6.0.0
     */
    protected Object beforeSort() {
        final States<E> states = new States<E>();
        for (final Path path : _selection)
            states.selection.add(getChild(path.path));
        for (final Path path : _opens)
            states.opens.add(getChild(path.path));
        return states;
    }

    /** A utility that the deriving class can call to restore the states
     * saved by {@link #beforeSort}
     * @since 6.0.0
     */
    protected void afterSort(Object ctx) {
        if (ctx instanceof States) {
            @SuppressWarnings("unchecked")
            final States<E> states = (States) ctx;
            _selection.clear();
            for (final E node : states.selection)
                _selection.add(new Path(getPath(node)));
            _opens.clear();
            for (final E node : states.opens)
                _opens.add(new Path(getPath(node)));
        }
    }

    //Selectable//

    public Set<E> getSelection() {
        final Set<E> selected = new LinkedHashSet<E>();
        int[][] paths = getSelectionPaths();
        if (paths != null)
            for (int i = 0; i < paths.length; i++)
                selected.add(getChild(paths[i]));
        return selected;
    }

    public void setSelection(Collection<? extends E> selection) {
        if (isSelectionChanged(selection)) {
            clearSelection();
            for (final E node : selection)
                addToSelection(node);
        }
    }

    private boolean isSelectionChanged(Collection<? extends E> selection) {
        if (_selection.size() != selection.size())
            return true;

        for (final E e : selection)
            if (!_selection.contains(e))
                return true;
        return false;
    }

    @SuppressWarnings("unchecked")

    public boolean isSelected(Object child) {
        final int[] path = getPath((E) child);
        if (path != null && path.length > 0)
            return isPathSelected(path);
        return false;
    }

    public boolean addToSelection(E child) {
        final int[] path = getPath(child);
        if (path != null && path.length > 0)
            return addSelectionPath(path);
        return false;
    }

    @SuppressWarnings("unchecked")

    public boolean removeFromSelection(Object child) {
        final int[] path = getPath((E) child);
        if (path != null && path.length > 0)
            return removeSelectionPath(path);
        return false;
    }

    //Openable//

    public Set<E> getOpenObjects() {
        final Set<E> opened = new LinkedHashSet<E>();
        int[][] paths = getOpenPaths();
        if (paths != null)
            for (int i = 0; i < paths.length; i++)
                opened.add(getChild(paths[i]));
        return opened;
    }

    public void setOpenObjects(Collection<? extends E> opened) {
        clearOpen();
        for (final E node : opened)
            addOpenObject(node);
    }

    @SuppressWarnings("unchecked")

    public boolean isObjectOpened(Object child) {
        final int[] path = getPath((E) child);
        if (path != null && path.length > 0)
            return isPathOpened(path);
        return false;
    }

    public boolean addOpenObject(E child) {
        final int[] path = getPath(child);
        if (path != null && path.length > 0)
            return addOpenPath(path);
        return false;
    }

    @SuppressWarnings("unchecked")

    public boolean removeOpenObject(Object child) {
        final int[] path = getPath((E) child);
        if (path != null && path.length > 0)
            return removeOpenPath(path);
        return false;
    }

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

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

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

        _listeners = new LinkedList<TreeDataListener>();
        _pagingListeners = new ArrayList<PagingListener>();
        Serializables.smartRead(s, _listeners);
        Serializables.smartRead(s, _pagingListeners);
    }

    @SuppressWarnings("unchecked")

    public Object clone() {
        final AbstractTreeModel clone;
        try {
            clone = (AbstractTreeModel) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }

        clone._listeners = new LinkedList<TreeDataListener>();
        clone._pagingListeners = new ArrayList<PagingListener>();
        clone._opens = new LinkedHashSet<Path>(_opens);
        clone._selection = new LinkedHashSet<Path>(_selection);
        return clone;
    }

    /** Represents a tree path.
     * @since 6.0.0
     */
    protected static class Path implements java.io.Serializable, Comparable {
        public final int[] path;

        protected Path(int[] path) {
            this.path = path;
        }

        protected Path(Path p) {
            int length = p.path.length;
            this.path = new int[length];
            for (int i = 0; i < length; i++) {
                this.path[i] = p.path[i];
            }
        }

        public int hashCode() {
            return Objects.hashCode(path);
        }

        public boolean equals(Object o) {
            return o instanceof Path && Objects.equals(path, ((Path) o).path);
        }

        public int compareTo(Object o) {
            if (!(o instanceof Path))
                throw new WrongValueException(o + " is not Path object");
            int length = path.length;
            Path toCompared = (Path) o;
            int[] toPath = toCompared.path;
            int toLength = toCompared.path.length;

            int smaller = (length < toLength ? length : toLength);
            for (int i = 0; i < smaller; i++) {
                if (path[i] != toPath[i])
                    return path[i] - toPath[i];
            }
            return length - toLength;
        }

        private boolean verifyPrefix(int[] path) {
            return verifyPrefix(path, 0);
        }

        /**
         * Test if this object's path match the given path when removal or addition,
         * and there are three situations:
         * 1. if this.path is fully matched, then it should be removed or updated
         * 2. if this.path match the given path except the last digit, they are
         * sibling, and we have to update the value by "update"
         * value of update:
         * 0 -> not update
         * 1 -> add one if behind prefix path (when model added)
         * -1 -> remove one if behind prefix path (when model removed
         * 3. if it failed matching before last digit, they are not sibling,
         * and just ignore this.path
         * 
         * Note: Because it will update this.path, be careful to use this function.
         * @param path the path to be matched
         * @param update 
         * @return boolean
         * @since 7.0.5
         */
        private boolean verifyPrefix(int[] path, int update) {
            if (path.length > this.path.length)
                return false;
            int i = 0;
            for (; i < path.length; i++) {
                if (path[i] != this.path[i])
                    break;
            }

            if (i == path.length) { //fully match prefix, should be removed
                if (update == 1) //only for add case
                    this.path[i - 1] += update;
                return true;
            } else if (i == path.length - 1) { //different on last digit, sibling
                if (this.path[i] > path[i]) //this.path is later
                    this.path[i] += update;
                return false;
            } else {
                return false;
            }
        }

        public String toString() {
            String result = "[";
            for (int i = 0; i < path.length; i++) {
                result += path[i] + ", ";
            }
            result = result.substring(0, result.length() - 2) + "]";
            return result;
        }
    }

    private static class States<E> {
        private final Set<E> selection = new LinkedHashSet<E>();
        private final Set<E> opens = new LinkedHashSet<E>();
    }

    // Pageable //
    private int _pageSize = -1;
    private int _activePage = -1;
    private int _pageCount = -1; // pending calculation

    // Pageable //
    public int getPageSize() {
        return _pageSize;
    }

    public void setPageSize(int size) throws WrongValueException {
        if (size < 0) {
            throw new WrongValueException("expecting positive non zero value, got: " + size);
        }
        
        if (_pageSize == size) return; //no change
        
        int oldPageSize = _pageSize;
        _pageSize = size;
        invalidatePageCount(); //need to invalidate cached value first since we're calling getPageCount() below
        
        //ZK-3173: increased page size might causes active page to exceed page count
        //need to correct the active page value before sending out paging event
        if (size > oldPageSize) {
            if (_activePage > 0) {
                int maxPageIndex = getPageCount() - 1;
                if (_activePage > maxPageIndex) {
                    _activePage = maxPageIndex;
                }
            }
        }
        
        for (PagingListener p : _pagingListeners) {
            try {
                p.onEvent(new PagingEvent(PageableModel.INTERNAL_EVENT, null, this, _activePage));
            } catch (Exception e) {
                log.warn("Failed to publish internal paging event", e);
            }
        }
    }

    /**
     * {@inheritDoc}
     * <br><br>
     * Note: the entire tree will be traversed once to count the number of tree nodes,
     * although the result will be cached until the model has changed (open/add/remove/etc.),
     * it is still a VERY EXPENSIVE operation, please @Override to provide your
     * own implementation for better performance
     *
     * @return number of total size, or 0 if the model is empty
     * @since 10.0.0
     */
    public int getTotalSize() {
        return getChildNodeCount(_root);
    }

    /**
     * {@inheritDoc}
     * <br><br>
     * Note: the entire tree will be traversed once to count the number of tree nodes, 
     * although the result will be cached until the model has changed (open/add/remove/etc.), 
     * it is still a VERY EXPENSIVE operation, please @Override to provide your 
     * own implementation for better performance
     * 
     * @return number of pages, or 1 if the model is empty
     * @since 8.0.0 
     */
    public int getPageCount() {
        if (_pageCount < 1) { // dirty/invalid value, re-calculation required
            if (_root != null) {
                int count = getTotalSize();
                // no child or _pageSize is still default value -1, return one page anyway
                if (count <= 0 || _pageSize < 0) {
                    _pageCount = 1;
                } else {
                    int pageCount = count / _pageSize;
                    if (count % _pageSize == 0) {
                        _pageCount = pageCount;
                    } else {
                        _pageCount = pageCount + 1;
                    }
                }
            } else {
                _pageCount = 1; // will always have at least one page
            }
        }
        return _pageCount;
    }

    // count the number of child nodes, will traverse into any opened child 
    // nodes, depth-first style
    private int getChildNodeCount(E node) {
        int c = getChildCount(node);
        int count = c;
        for (int i = 0; i < c; i++) {
            E child = getChild(node, i);
            if (isPathOpened(getPath(child))) {
                count += getChildNodeCount(child);
            }
        }
        return count;
    }

    //ZK-1696: set to a invalid value to trigger re-calculation when getPageCount() is called
    private void invalidatePageCount() {
        _pageCount = -1;
    }

    public int getActivePage() {
        return _activePage;
    }

    public void setActivePage(int pg) throws WrongValueException {
        if (pg < 0) {
            throw new WrongValueException("expecting positive non zero value, got: " + pg);
        }
        if (pg > 0) {
            int pc = _pageCount == -1 ? getPageCount() : _pageCount;
            if (pg >= pc) {
                pg = pc - 1; //set to last valid page
            }
        }
        if (_activePage == pg) return; //no change
        
        _activePage = pg;
        for (PagingListener p : _pagingListeners) {
            try {
                p.onEvent(new PagingEvent(PageableModel.INTERNAL_EVENT, null, this, pg));
            } catch (Exception e) {
                log.warn("Failed to publish internal paging event", e);
            }
        }
    }

    private List<E> getAllNodes() {
        E root = getRoot();
        List all = new LinkedList();
        if (root != null) {
            getChildNodes(all, root);
        }
        return all;
    }

    private void getChildNodes(List<E> all, E parent) {
        for (int i = 0, j = getChildCount(parent); i < j; i++) {
            E child = getChild(parent, i);
            if (child != null) {
                all.add(child);
                getChildNodes(all, child);
            }
        }
    }

    /**
     * A default selection control implementation for {@link AbstractTreeModel},
     * 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 AbstractTreeModel<E> model;

        public DefaultSelectionControl(AbstractTreeModel<E> model) {
            this.model = model;
        }

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

        public void setSelectAll(boolean selectAll) {
            if (selectAll) {
                List<E> all = new LinkedList<>();
                List<E> allNodes = model.getAllNodes();
                for (E o : allNodes) {
                    if (isSelectable(o)) // check whether it can be selectable or not
                        all.add(o);
                }
                // avoid scroll into view at client side.
                model.fireEvent(TreeDataEvent.DISABLE_CLIENT_UPDATE, null, -1, -1);
                try {
                    ((Selectable<E>) model).setSelection(all);
                } finally {
                    model.fireEvent(TreeDataEvent.ENABLE_CLIENT_UPDATE, null, -1, -1);
                }
            } else {
                ((Selectable) model).clearSelection();
            }
        }

        public boolean isSelectAll() {
            List<E> allNodes = model.getAllNodes();
            for (E o : allNodes) {
                if (isSelectable(o) && !((Selectable) model).isSelected(o))
                    return false;
            }
            return true;
        }
    }

    public void addPagingEventListener(PagingListener listener) {
        if (listener == null)
            throw new NullPointerException();
        _pagingListeners.add(listener);
    }

    public void removePagingEventListener(PagingListener listener) {
        _pagingListeners.remove(listener);
    }
}