/* AbstractListModel.java
Purpose:
Description:
History:
Thu Aug 18 15:19:43 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.util.ArrayList;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.io.Serializables;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.ext.Pageable;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;
/**
* A skeletal implementation for {@link ListModel} and {@link Selectable}
*
* @author tomyeh
*/
public abstract class AbstractListModel<E>
implements ListModel<E>, Selectable<E>, java.io.Serializable, Pageable, PagingEventPublisher {
private static final Logger log = LoggerFactory.getLogger(AbstractListModel.class);
private transient List<ListDataListener> _listeners = new ArrayList<ListDataListener>();
private transient List<PagingListener> _pagingListeners = new ArrayList<PagingListener>();
/** The current selection. */
protected transient Set<E> _selection;
private SelectionControl<E> _ctrl;
private boolean _multiple;
protected AbstractListModel() {
_selection = newEmptySelection();
_ctrl = new DefaultSelectionControl<E>(this);
}
/**
* Fires a {@link ListDataEvent} for all registered listener (thru
* {@link #addListDataListener}.
*
* <p>
* Note: you can invoke this method only in an event listener.
*/
protected void fireEvent(int type, int index0, int index1) {
final ListDataEvent evt = new ListDataEvent(this, type, index0, index1);
for (ListDataListener l : _listeners)
l.onChange(evt);
}
// -- ListModel --//
/** {@inheritDoc} */
public void addListDataListener(ListDataListener l) {
if (l == null)
throw new NullPointerException();
_listeners.add(l);
}
/** {@inheritDoc} */
public void removeListDataListener(ListDataListener l) {
_listeners.remove(l);
}
//Selectable//
/** {@inheritDoc} */
public Set<E> getSelection() {
return Collections.unmodifiableSet(_selection);
}
/** {@inheritDoc} */
public void setSelection(Collection<? extends E> selection) {
if (isSelectionChanged(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
fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
}
}
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;
}
/** {@inheritDoc} */
public boolean isSelected(Object obj) {
return !isSelectionEmpty() && (_selection.size() == 1 ? Objects.equals(_selection.iterator().next(), obj)
: _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(ListDataEvent.SELECTION_CHANGED, -1, -1);
return true;
}
return false;
}
/** {@inheritDoc} */
public void clearSelection() {
if (!_selection.isEmpty()) {
_selection.clear();
fireEvent(ListDataEvent.SELECTION_CHANGED, -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)} 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(ListDataEvent.SELECTION_CHANGED, -1, -1);
}
/**Removes the selection of the given collection.
*/
protected void removeAllSelection(Collection<?> c) {
// B60-ZK-1126
// Notify selection has been changed
if (_selection.removeAll(c)) {
fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
}
}
/**Removes the selection that doesn't belong to the given collection.
*/
protected void retainAllSelection(Collection<?> c) {
// B60-ZK-1126
// Notify selection has been changed
if (_selection.retainAll(c)) {
fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
}
}
/** {@inheritDoc} */
public boolean isMultiple() {
return _multiple;
}
/** {@inheritDoc} */
public void setMultiple(boolean multiple) {
if (_multiple != multiple) {
_multiple = multiple;
fireEvent(ListDataEvent.MULTIPLE_CHANGED, -1, -1);
if (!multiple && _selection.size() > 1) {
E v = _selection.iterator().next();
_selection.clear();
_selection.add(v);
fireEvent(ListDataEvent.SELECTION_CHANGED, -1, -1);
}
}
}
public void setSelectionControl(SelectionControl ctrl) {
_ctrl = ctrl;
}
public SelectionControl getSelectionControl() {
return _ctrl;
}
/**
* A default selection control implementation for {@link AbstractListModel},
* 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 AbstractListModel model;
public DefaultSelectionControl(AbstractListModel model) {
this.model = model;
}
public boolean isSelectable(E e) {
return true;
}
public void setSelectAll(boolean selectAll) {
if (selectAll) {
List all = new LinkedList();
for (int i = 0, j = model.getSize(); i < j; i++) {
E o = (E) model.getElementAt(i);
if (isSelectable(o)) // check whether it can be selectable or not
all.add(o);
}
// avoid scroll into view at client side.
model.fireEvent(ListDataEvent.DISABLE_CLIENT_UPDATE, -1, -1);
if (model instanceof AbstractListModel)
try {
((Selectable) model).setSelection(all);
} finally {
model.fireEvent(ListDataEvent.ENABLE_CLIENT_UPDATE, -1, -1);
}
} else {
((Selectable) model).clearSelection();
}
}
public boolean isSelectAll() {
for (int i = 0, j = model.getSize(); i < j; i++) {
E o = (E) model.getElementAt(i);
if (isSelectable(o) && !((Selectable) model).isSelected(o))
return false;
}
return true;
}
}
/** Instantiation an empty set of the section.
* It is used to initialize {@link #_selection}.
* <p>By default, it instantiates an instance of LinkedHashSet.
* 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();
}
// Serializable//
private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
writeSelection(s);
Serializables.smartWrite(s, _listeners);
Serializables.smartWrite(s, _pagingListeners);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
readSelection(s);
_listeners = new ArrayList<ListDataListener>();
Serializables.smartRead(s, _listeners);
_pagingListeners = new ArrayList<PagingListener>();
Serializables.smartRead(s, _pagingListeners);
}
@SuppressWarnings("unchecked")
public Object clone() {
final AbstractListModel clone;
try {
clone = (AbstractListModel) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
clone._listeners = new ArrayList<ListDataListener>();
clone._pagingListeners = new ArrayList<PagingListener>();
clone._selection = clone.newEmptySelection();
clone._selection.addAll(_selection);
return clone;
}
// Pageable //
private int _pageSize = -1;
private int _activePage = -1;
// Pageable // ZK-1696
public int getPageSize() {
return _pageSize;
}
public void setPageSize(int size) throws WrongValueException {
if (size < 0) {
throw new WrongValueException("page size should >= 0");
}
if (_pageSize == size) return; //no change
int oldPageSize = _pageSize;
_pageSize = size;
//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) {
int maxPageIndex = getPageCount() - 1;
if (_activePage > maxPageIndex) {
_activePage = maxPageIndex;
}
}
for (PagingListener p : _pagingListeners) {
try {
p.onEvent(new PagingEvent(PagingEventPublisher.INTERNAL_EVENT, null, this, _activePage));
} catch (Exception e) {
log.warn("Failed to publish internal paging event", e);
}
}
}
public int getPageCount() {
int size = getSize();
if (size > 0 && _pageSize > 0) { //_pageSize default value is -1
int pageCount = size / _pageSize;
if (size % _pageSize == 0) {
return pageCount;
} else {
return pageCount + 1;
}
} else {
return 1;
}
}
public int getActivePage() {
return _activePage;
}
public void setActivePage(int pg) throws WrongValueException {
if (pg < 0) {
throw new WrongValueException("active page index should >= 0");
}
if (pg >= getPageCount()) {
pg = getPageCount() - 1; //set to last valid page
}
if (_activePage == pg) return; //no change
_activePage = pg;
for (PagingListener p : _pagingListeners) {
try {
p.onEvent(new PagingEvent(PagingEventPublisher.INTERNAL_EVENT, null, this, pg));
} catch (Exception e) {
log.warn("Failed to publish internal paging event", e);
}
}
}
public void addPagingEventListener(PagingListener l) {
if (l == null)
throw new NullPointerException();
_pagingListeners.add(l);
}
public void removePagingEventListener(PagingListener l) {
_pagingListeners.remove(l);
}
}