zul/src/main/java/org/zkoss/zul/impl/ListboxDataLoader.java

Summary

Maintainability
F
1 wk
Test Coverage
/* ListboxDataLoader.java
{{IS_NOTE
    Purpose:
        
    Description:
        
    History:
        Nov 23, 2009 2:53:30 PM, Created by henrichen
}}IS_NOTE

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

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

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;

import org.zkoss.lang.Objects;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.au.DeferredValue;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.sys.UiEngine;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.ForEachStatus;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.Attributes;
import org.zkoss.zul.Frozen;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listfoot;
import org.zkoss.zul.Listgroup;
import org.zkoss.zul.ListgroupRendererExt;
import org.zkoss.zul.Listgroupfoot;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.ListitemRendererExt;
import org.zkoss.zul.Paging;
import org.zkoss.zul.event.GroupsDataEvent;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.ext.GroupingInfo;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.impl.GroupsListModel.GroupDataInfo;

/**
 * Generic {@link Listbox} data loader.
 * @author henrichen
 * @since 5.0.0
 */
public class ListboxDataLoader implements DataLoader, Cropper { //no need to serialize since Listbox assumes it
    private Listbox _listbox;

    //--DataLoader--//
    public void init(Component owner, int offset, int limit) {
        _listbox = (Listbox) owner;
    }

    public void reset() {
        //do nothing
    }

    public final Component getOwner() {
        return _listbox;
    }

    public int getOffset() {
        return 0;
    }

    public int getLimit() {
        return _listbox.getRows() > 0 ? _listbox.getRows() + 5 : 50;
    }

    public int getTotalSize() {
        final ListModel model = _listbox.getModel();
        return model != null ? model.getSize() : _listbox.getVisibleItemCount();
    }

    private int INVALIDATE_THRESHOLD = -1;

    /**
     * updates the status of the changed group.
     * @param event
     * @since 8.0.4
     */
    public void doGroupsDataChange(GroupsDataEvent event) {
        if (event.getType() == GroupsDataEvent.GROUPS_OPENED) {
            GroupsListModel groupsListModel = ((GroupsListModel) _listbox.getModel());
            int groupIndex = event.getGroupIndex();
            int offset = groupsListModel.getGroupOffset(groupIndex);
            ((Listgroup) _listbox.getItems().get(offset)).setOpen(event.getModel().isGroupOpened(groupIndex));
        }
    }

    public void doListDataChange(ListDataEvent event) {
        if (INVALIDATE_THRESHOLD == -1) {
            INVALIDATE_THRESHOLD = Utils.getIntAttribute(this.getOwner(), "org.zkoss.zul.invalidateThreshold", 10,
                    true);
        }
        //when this is called _model is never null
        final ListModel _model = _listbox.getModel();
        final int newsz = _model.getSize(), oldsz = _listbox.getItemCount();
        int min = event.getIndex0(), max = event.getIndex1(), cnt;

        switch (event.getType()) {
        case ListDataEvent.INTERVAL_ADDED:
            cnt = newsz - oldsz;
            if (cnt < 0)
                throw new UiException("Adding causes a smaller list?");
            if (cnt == 0) //no change, nothing to do here
                return;
            boolean isInvalidated = false;
            if ((oldsz <= 0 || cnt > INVALIDATE_THRESHOLD) && !inPagingMold()) {
                invalidateListitems();
                isInvalidated = true;
            }
            if (min < 0)
                if (max < 0)
                    min = 0;
                else
                    min = max - cnt + 1;
            if (min > oldsz)
                min = oldsz;

            ListitemRenderer renderer = null;
            final Component next = min < oldsz ? _listbox.getItemAtIndex(min) : null;
            while (--cnt >= 0) {
                if (renderer == null)
                    renderer = (ListitemRenderer) getRealRenderer();
                _listbox.insertBefore(newUnloadedItem(renderer, min++), next);
            }

            // Fix ZK-5468: the content of the subsequence item might be changed
            if (!isInvalidated && !_listbox.isInvalidated()) {
                syncModel(max, _listbox.getItemCount() - (max - min));
            }
            break;

        case ListDataEvent.INTERVAL_REMOVED:
            cnt = oldsz - newsz;
            if (cnt < 0)
                throw new UiException("Removal causes a larger list?");
            if (cnt == 0) //no change, nothing to do here
                return;
            if (min >= 0)
                max = min + cnt - 1;
            else if (max < 0)
                max = cnt - 1; //0 ~ cnt - 1            
            if (max > oldsz - 1)
                max = oldsz - 1;

            boolean isInvalidated0 = false;
            if ((newsz <= 0 || cnt > INVALIDATE_THRESHOLD) && !inPagingMold()) {
                _listbox.shallUpdateScrollPos(true);
                invalidateListitems();
                isInvalidated0 = true;
            }

            //detach from end (due to groupfoot issue)
            Component comp = _listbox.getItemAtIndex(max);
            while (--cnt >= 0) {
                Component p = comp.getPreviousSibling();
                comp.detach();
                comp = p;
            }

            // Fix ZK-5468: the content of the subsequence item might be changed
            if (!isInvalidated0 && !_listbox.isInvalidated()) {
                syncModel(max, _listbox.getItemCount() - (max - min));
            }
            break;

        default: //CONTENTS_CHANGED
            syncModel(min, max < 0 ? -1 : (max - min + 1));
            //TonyQ: B50-ZK-897 , listfoot disappear after clicking run button , 
            //                    sync logic with GridDataLoader 
        }
    }

    private void invalidateListitems() {
        //Bug 3147518: avoid memory leak
        //Also better performance (outer better than remove a lot)
        final Execution execution = Executions.getCurrent();
        final String uuid = _listbox.getUuid();
        final boolean isDeferInitModel = execution != null && execution.getAttribute("zkoss.Listbox.deferInitModel_" + uuid) != null;
        if (isDeferInitModel) return; // skip while defer loading a model

        final Page page = _listbox.getPage();
        if (execution != null && execution.isAsyncUpdate(page)) {
            final UiEngine engine = page != null
                ? ((WebAppCtrl) page.getDesktop().getWebApp()).getUiEngine()
                : null;
            if (engine != null) {
                engine.addSmartUpdate(_listbox, "itemsInvalid_", new DeferredRedraw(_listbox.getItems()), 10000);
                execution.setAttribute("zkoss.Listbox.invalidateListitems" + uuid, Boolean.TRUE);
            }
        }
    }

    /** Creates an new and unloaded listitem. */
    protected final Listitem newUnloadedItem(ListitemRenderer renderer, int index) {
        final ListModel model = _listbox.getModel();
        Listitem item = null;
        if (model instanceof GroupsListModel) {
            final GroupsListModel gmodel = (GroupsListModel) model;
            final GroupingInfo info = gmodel.getDataInfo(index);
            switch (info.getType()) {
            case GroupDataInfo.GROUP:
                item = newListgroup(renderer);
                ((Listgroup) item).setOpen(info.isOpen());
                break;
            case GroupDataInfo.GROUPFOOT:
                item = newListgroupfoot(renderer);
                break;
            default:
                item = newListitem(renderer);
            }
        } else {
            item = newListitem(renderer);
        }
        ((LoadStatus) item.getExtraCtrl()).setLoaded(false);
        ((LoadStatus) item.getExtraCtrl()).setIndex(index);

        newUnloadedCell(renderer, item);
        return item;
    }

    private Listitem newListitem(ListitemRenderer renderer) {
        Listitem item = null;
        if (renderer instanceof ListitemRendererExt)
            item = ((ListitemRendererExt) renderer).newListitem(_listbox);
        if (item == null) {
            item = new Listitem();
            item.applyProperties();
        }
        return item;
    }

    private Listgroup newListgroup(ListitemRenderer renderer) {
        Listgroup group = null;
        if (renderer instanceof ListgroupRendererExt)
            group = ((ListgroupRendererExt) renderer).newListgroup(_listbox);
        if (group == null) {
            group = new Listgroup();
            group.applyProperties();
        }
        return group;
    }

    private Listgroupfoot newListgroupfoot(ListitemRenderer renderer) {
        Listgroupfoot groupfoot = null;
        if (renderer instanceof ListgroupRendererExt)
            groupfoot = ((ListgroupRendererExt) renderer).newListgroupfoot(_listbox);
        if (groupfoot == null) {
            groupfoot = new Listgroupfoot();
            groupfoot.applyProperties();
        }
        return groupfoot;
    }

    private Listcell newUnloadedCell(ListitemRenderer renderer, Listitem item) {
        Listcell cell = null;
        if (renderer instanceof ListitemRendererExt)
            cell = ((ListitemRendererExt) renderer).newListcell(item);

        if (cell == null) {
            cell = new Listcell();
            cell.applyProperties();
        }
        cell.setParent(item);
        return cell;
    }

    public Object getRealRenderer() {
        final ListitemRenderer renderer = _listbox.getItemRenderer();
        return renderer != null ? renderer : _defRend;
    }

    private static final ListitemRenderer _defRend = new ListitemRenderer() {
        public void render(final Listitem item, final Object data, final int index) {
            final Listbox listbox = (Listbox) item.getParent();
            Template tm = listbox.getTemplate("model");
            GroupingInfo info = null;
            if (item instanceof Listgroup) {
                final Template tm2 = listbox.getTemplate("model:group");
                if (tm2 != null)
                    tm = tm2;
                if (listbox.getModel() instanceof GroupsListModel) {
                    final GroupsListModel gmodel = (GroupsListModel) listbox.getModel();
                    info = gmodel.getDataInfo(index);
                }
            } else if (item instanceof Listgroupfoot) {
                final Template tm2 = listbox.getTemplate("model:groupfoot");
                if (tm2 != null)
                    tm = tm2;
            }
            if (tm == null) {
                item.setLabel(Objects.toString(data));
                item.setValue(data);
            } else {
                final GroupingInfo groupingInfo = info;
                final Component[] items = ShadowElementsCtrl
                        .filterOutShadows(tm.create(listbox, item, new VariableResolver() {
                    public Object resolveVariable(String name) {
                        if ("each".equals(name)) {
                            return data;
                        } else if ("forEachStatus".equals(name)) {
                            return new ForEachStatus() {

                                public ForEachStatus getPrevious() {
                                    return null;
                                }

                                public Object getEach() {
                                    return getCurrent();
                                }

                                public int getIndex() {
                                    return index;
                                }

                                public Integer getBegin() {
                                    return 0;
                                }

                                public Integer getEnd() {
                                    return listbox.getModel().getSize();
                                }

                                public Object getCurrent() {
                                    return data;
                                }

                                public boolean isFirst() {
                                    return getCount() == 1;
                                }

                                public boolean isLast() {
                                    return getIndex() + 1 == getEnd();
                                }

                                public Integer getStep() {
                                    return null;
                                }

                                public int getCount() {
                                    return getIndex() + 1;
                                }
                            };
                        } else if ("groupingInfo".equals(name)) {
                            return groupingInfo;
                        } else {
                            return null;
                        }
                    }
                }, null));
                if (items.length != 1)
                    throw new UiException("The model template must have exactly one item, not " + items.length);

                final Listitem nli = (Listitem) items[0];

                //sync open state
                if (nli instanceof Listgroup && item instanceof Listgroup) {
                    ((Listgroup) nli).setOpen(((Listgroup) item).isOpen());
                }

                if (nli.getValue() == null) //template might set it
                    nli.setValue(data);
                item.setAttribute(Attributes.MODEL_RENDERAS, nli);
                //indicate a new item is created to replace the existent one
                item.detach();
            }
        }
    };

    public void syncModel(int offset, int limit) {
        _listbox.setAttribute(Listbox.SYNCING_MODEL, Boolean.TRUE);
        try {
            syncModel0(offset, limit);
        } finally {
            //Bug ZK-2789: do not use setAttribute when actually trying to removeAttribute
            _listbox.removeAttribute(Listbox.SYNCING_MODEL);
        }
    }

    private void syncModel0(int offset, int limit) {
        int min = offset;
        int max = offset + limit - 1;

        final ListModel _model = _listbox.getModel();
        final int newsz = _model.getSize();
        final int oldsz = _listbox.getItemCount();
        final Paginal _pgi = _listbox.getPaginal();
        final boolean inPaging = inPagingMold();
        final boolean shallInvalidated = //Bug 3147518: avoid memory leak
        (min < 0 || min == 0) && (max < 0 || max >= newsz || max >= oldsz);

        int newcnt = newsz - oldsz;
        int atg = _pgi != null ? _listbox.getActivePage() : 0;
        ListitemRenderer renderer = null;
        Component next = null;
        if (oldsz > 0) {
            if (min < 0)
                min = 0;
            else if (min > oldsz - 1)
                min = oldsz - 1;
            if (max < 0)
                max = oldsz - 1;
            else if (max > oldsz - 1)
                max = oldsz - 1;
            if (min > max) {
                int t = min;
                min = max;
                max = t;
            }

            int cnt = max - min + 1; //# of affected
            if (_model instanceof GroupsListModel) {
                //detach all from end to front since groupfoot
                //must be detached before group
                newcnt += cnt; //add affected later
                if ((shallInvalidated || newcnt > INVALIDATE_THRESHOLD) && !inPaging)
                    invalidateListitems();

                Component comp = _listbox.getItemAtIndex(max);
                next = comp.getNextSibling();
                while (--cnt >= 0) {
                    Component p = comp.getPreviousSibling();
                    comp.detach();
                    comp = p;
                }
            } else { //ListModel
                int addcnt = 0;
                Component item = _listbox.getItemAtIndex(min);
                while (--cnt >= 0) {
                    next = item.getNextSibling();

                    if (cnt < -newcnt) { //if shrink, -newcnt > 0
                        item.detach(); //remove extra
                    } else if (((Listitem) item).isLoaded()) {
                        if (renderer == null)
                            renderer = (ListitemRenderer) getRealRenderer();

                        // ZK-2450: cache selected Index and item, added them back after detach item
                        if (_pgi != null && ((Listitem) item).isSelected()) {
                            int index = ((Listitem) item).getIndex();
                            item.detach(); // always detach
                            Listitem newItem = newUnloadedItem(renderer, min);
                            _listbox.insertBefore(newItem, next);
                            _listbox.addItemToSelection(newItem);
                        } else {
                            item.detach(); //always detach
                            _listbox.insertBefore(newUnloadedItem(renderer, min), next);
                        }
                        ++addcnt;
                    }
                    ++min;
                    item = next; //B2100338.,next item could be Paging, don't use Listitem directly
                }

                if ((shallInvalidated || addcnt > INVALIDATE_THRESHOLD || addcnt + newcnt > INVALIDATE_THRESHOLD)
                        && !inPagingMold())
                    invalidateListitems();
            }
        } else {
            min = 0;
        }

        for (; --newcnt >= 0; ++min) {
            if (renderer == null)
                renderer = (ListitemRenderer) getRealRenderer();
            _listbox.insertBefore(newUnloadedItem(renderer, min), next);
        }
        if (_pgi != null) {
            if (atg >= _pgi.getPageCount())
                atg = _pgi.getPageCount() - 1;
            _pgi.setActivePage(atg);
            if (_pgi.getTotalSize() != newsz)
                _pgi.setTotalSize(newsz); //Bug ZK-1601: reset total size since model size may changed.
        }
    }

    protected boolean inPagingMold() {
        return "paging".equals(_listbox.getMold());
    }

    protected boolean inSelectMold() {
        return "select".equals(_listbox.getMold());
    }

    public void updateModelInfo() {
        // do nothing
    }

    public void setLoadAll(boolean b) {
        // do nothing
    }

    //--Cropper--//
    public boolean isCropper() {
        return _listbox != null && inPagingMold() && _listbox.getPageSize() <= getTotalSize();
        //Single page is considered as not a cropper.
        //isCropper is called after a component is removed, so
        //we have to test >= rather than >
    }

    /** Retrieves the children available at client.
     * <p>It can not be overridden. Rather, override {@link #getAvailableAtClient(boolean)} instead.
     */
    public final Set<? extends Component> getAvailableAtClient() {
        return getAvailableAtClient(false);
    }

    /** Retrieves the children available at client with more control.
     * <p>Derived class shall override this method rather than {@link #getAvailableAtClient()}.
     * @param itemOnly whether to return only {@link Listitem} and derives.
     * @since 5.0.10
     */
    protected Set<? extends Component> getAvailableAtClient(boolean itemOnly) {
        final boolean invalidateListitems = Executions.getCurrent()
                .removeAttribute("zkoss.Listbox.invalidateListitems" + _listbox.getUuid()) != null;
        if (invalidateListitems && !_listbox.isInvalidated())
            return getAvailableAtClient(0, 0, itemOnly);
        if (!isCropper())
            return null;

        final Paginal pgi = _listbox.getPaginal();
        int pgsz = pgi.getPageSize();
        int ofs = pgi.getActivePage() * pgsz;
        return getAvailableAtClient(ofs, pgsz, itemOnly);
    }

    /** Retrieves the children available at the client within the given range.
     * @param itemOnly whether to return only {@link Listitem} and derives.
     * @since 5.0.10
     */
    protected Set<? extends Component> getAvailableAtClient(int offset, int limit, boolean itemOnly) {
        final Set<Component> avail = new LinkedHashSet<Component>(32);
        if (!itemOnly) {
            avail.addAll(_listbox.getHeads());
            final Listfoot listfoot = _listbox.getListfoot();
            if (listfoot != null)
                avail.add(listfoot);
            final Paging paging = _listbox.getPagingChild();
            if (paging != null)
                avail.add(paging);
            final Frozen frozen = _listbox.getFrozen();
            if (frozen != null)
                avail.add(frozen);
        }

        int pgsz = limit;
        int ofs = offset;
        if (_listbox.getItemCount() > 0) {
            Component item = _listbox.getItems().get(0);
            while (item != null) {
                if (pgsz == 0)
                    break;
                if (item.isVisible() && item instanceof Listitem) {
                    if (--ofs < 0) {
                        --pgsz;
                        avail.add(item);
                    }
                }
                if (item instanceof Listgroup) {
                    final Listgroup g = (Listgroup) item;
                    if (!g.isOpen()) {
                        for (int j = 0, len = g.getItemCount(); j < len; j++)
                            item = item.getNextSibling();
                    }
                }
                if (item != null)
                    item = item.getNextSibling();
            }
        }
        return avail;
    }

    public Component getCropOwner() {
        return _listbox;
    }

    protected static class DeferredRedraw implements DeferredValue {
        private final Collection<? extends Component> _items;

        public DeferredRedraw(Collection<? extends Component> items) {
            _items = items;
        }

        public Object getValue() {
            return ComponentsCtrl.redraw(_items);
        }
    }
}