zul/src/main/java/org/zkoss/zul/ListModelMap.java
/* ListModelMap.java
{{IS_NOTE
Purpose:
Description:
History:
Fri Dec 01 11:15:23 2006, Created by Henri Chen
}}IS_NOTE
Copyright (C) 2006 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.zul;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.ext.Sortable;
/**
* <p>This is the {@link ListModel} as a {@link java.util.Map} to be used with {@link Listbox}.
* Add or remove the contents of this model as a List would cause the associated Listbox to change accordingly.</p>
*
* @author Henri Chen
* @see ListModel
* @see ListModelList
* @see ListModelMap
*/
public class ListModelMap<K, V> extends AbstractListModel<Map.Entry<K, V>>
implements Sortable<Map.Entry<K, V>>, Map<K, V>, java.io.Serializable {
private static final long serialVersionUID = 20120206122736L;
protected Map<K, V> _map;
private Comparator<Map.Entry<K, V>> _sorting;
private boolean _sortDir;
/**
* Constructor.
*
* @param map the map to represent
* @param live whether to have a 'live' {@link ListModel} on top of
* the specified map.
* If false, the content of the specified map is copied.
* If true, this object is a 'facade' of the specified map,
* i.e., when you add or remove items from this {@link ListModelMap},
* the inner "live" map would be changed accordingly.
*
* However, it is not a good idea to modify <code>map</code>
* if it is passed to this method with live is true,
* since {@link Listbox} is not smart enough to handle it.
* Instead, modify it thru this object.
* @since 2.4.0
*/
public ListModelMap(Map<K, V> map, boolean live) {
_map = live ? map : new LinkedHashMap<K, V>(map);
}
/**
* Constructor.
*/
public ListModelMap() {
_map = new LinkedHashMap<K, V>();
}
/**
* Constructor.
* It makes a copy of the specified map (i.e., not live).
*
* <p>Notice that if the data is static or not shared, it is better to
* use <code>ListModelMap(map, true)</code> instead, since
* making a copy is slower.
*/
public ListModelMap(Map<? extends K, ? extends V> map) {
_map = new LinkedHashMap<K, V>(map);
}
/**
* Constructor.
* @param initialCapacity the initial capacity for this ListModelMap.
*/
public ListModelMap(int initialCapacity) {
_map = new LinkedHashMap<K, V>(initialCapacity);
}
/**
* Constructor.
* @param initialCapacity the initial capacity for this ListModelMap.
* @param loadFactor the loadFactor to increase capacity of this ListModelMap.
*/
public ListModelMap(int initialCapacity, float loadFactor) {
_map = new LinkedHashMap<K, V>(initialCapacity, loadFactor);
}
/**
* Get the inner real Map.
*/
public Map<K, V> getInnerMap() {
return _map;
}
//-- ListModel --//
public int getSize() {
return _map.size();
}
/**
* Returns the entry (Map.Entry) at the specified index.
*/
@SuppressWarnings("unchecked")
public Map.Entry<K, V> getElementAt(int j) {
if (j < 0 || j >= _map.size())
throw new IndexOutOfBoundsException("" + j);
for (Iterator<Map.Entry<K, V>> it = _map.entrySet().iterator();;) {
final Map.Entry<K, V> o = it.next();
if (--j < 0)
return new Entry0(o);
}
}
private static class Entry0<K, V> implements Map.Entry<K, V>, Serializable {
private K _key;
private V _value;
private transient Map.Entry<K, V> _entry;
Entry0(Map.Entry<K, V> entry) {
_entry = entry;
}
public K getKey() {
return _entry != null ? _entry.getKey() : _key;
}
public V getValue() {
return _entry != null ? _entry.getValue() : _value;
}
public V setValue(V value) {
V oldValue = _value;
if (_entry != null) {
oldValue = _entry.getValue();
_entry.setValue(value);
} else
_value = value;
return oldValue;
}
public final boolean equals(Object o) {
if (_entry != null)
return _entry.equals(o);
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry) o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
if (_entry != null)
return _entry.hashCode();
return (_key == null ? 0 : _key.hashCode()) ^ (_value == null ? 0 : _value.hashCode());
}
public final String toString() {
if (_entry != null)
return _entry.toString();
return getKey() + "=" + getValue();
}
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
if (_entry != null) {
s.writeObject(_entry.getKey());
s.writeObject(_entry.getValue());
} else {
s.writeObject(_key);
s.writeObject(_value);
}
}
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
_key = (K) s.readObject();
_value = (V) s.readObject();
}
}
//-- Map --//
public void clear() {
int i2 = _map.size() - 1;
if (i2 < 0) {
return;
}
clearSelection();
_map.clear();
fireEvent(ListDataEvent.INTERVAL_REMOVED, 0, i2);
}
public boolean containsKey(Object key) {
return _map.containsKey(key);
}
public boolean containsValue(Object value) {
return _map.containsValue(value);
}
public Set<Map.Entry<K, V>> entrySet() {
return new Entries(_map.entrySet());
}
public boolean equals(Object o) {
if (this == o)
return true;
return _map.equals(o instanceof ListModelMap ? ((ListModelMap<?, ?>) o)._map : o);
}
public String toString() {
return _map.toString();
}
public V get(Object key) {
return _map.get(key);
}
public int hashCode() {
return _map.hashCode();
}
public boolean isEmpty() {
return _map.isEmpty();
}
public Set<K> keySet() {
return new Keys(_map.keySet());
}
public V put(K key, V o) {
final V ret;
if (_map.containsKey(key)) {
if (Objects.equals(o, _map.get(key))) {
return o; //nothing changed
}
int index = indexOfKey(key);
ret = _map.put(key, o);
fireEvent(ListDataEvent.CONTENTS_CHANGED, index, index);
} else {
ret = _map.put(key, o);
//After put, the position can change if not LinkedHashMap
//bug #1819318 Problem while using SortedSet with Databinding
//bug #1839634 Problem while using HashSet with Databinding
if (_map instanceof LinkedHashMap) {
//bug 1869614 Problem when switching from ListModelList to ListModelMap,
//java.lang.IndexOutOfBoundsException: 1, interval added index should be _map.size() - 1
final int i1 = _map.size() - 1;
fireEvent(ListDataEvent.INTERVAL_ADDED, i1, i1);
} else if (_map instanceof SortedMap) {
final int i1 = indexOfKey(key);
fireEvent(ListDataEvent.INTERVAL_ADDED, i1, i1);
} else { //bug #1839634, HashMap, not sure the iteration sequence
//of the HashMap, must resync
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
}
return ret;
}
/** Returns the index of the specified object based on the key.
*
* @param o the key to look for
*/
public int indexOfKey(Object o) {
int j = 0;
for (Iterator<K> it = _map.keySet().iterator(); it.hasNext(); ++j) {
if (Objects.equals(o, it.next()))
return j;
}
return -1;
}
/** Returns the index of the specified object based on the entry
* (Map.Entry).
*
* @param o the object to look for. It must be an instance of Map.Entry.
*/
public int indexOf(Object o) {
int j = 0;
for (Iterator<?> it = _map.entrySet().iterator(); it.hasNext(); ++j) {
if (Objects.equals(o, it.next()))
return j;
}
return -1;
}
public void putAll(Map<? extends K, ? extends V> c) {
if (c == _map) //special case
return;
if (_map instanceof LinkedHashMap) {
int sz = c.size();
if (sz <= 0) {
return;
}
List<Map.Entry<? extends K, ? extends V>> added = new ArrayList<Map.Entry<? extends K, ? extends V>>(
c.size());
for (Map.Entry<? extends K, ? extends V> me : c.entrySet()) {
K key = me.getKey();
if (_map.containsKey(key)) {
put(key, me.getValue());
} else {
added.add(me);
}
}
for (Map.Entry<? extends K, ? extends V> me : added) {
_map.put(me.getKey(), me.getValue());
}
int len = added.size();
if (len > 0) {
fireEvent(ListDataEvent.INTERVAL_ADDED, sz, sz + len - 1);
}
} else { //bug #1819318 Problem while using SortedSet with Databinding
//bug #1839634 Problem while using HashSet with Databinding
_map.putAll(c);
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
}
public V remove(Object key) {
if (_map.containsKey(key)) {
//bug #1819318 Problem while using SortedSet with Databinding
V ret = null;
removeSelectionByKey(key);
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
int index = indexOfKey(key);
ret = _map.remove(key);
fireEvent(ListDataEvent.INTERVAL_REMOVED, index, index);
} else { //bug #1839634 Problem while using HashSet with Databinding
ret = _map.remove(key);
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
return ret;
}
return null;
}
private void removeSelectionByKey(Object key) {
for (Map.Entry<K, V> entry : getSelection()) {
if (Objects.equals(key, entry.getKey())) {
removeFromSelection(entry);
return;
}
}
}
private void removeSelectionByValue(Object value) {
for (Map.Entry<K, V> entry : getSelection()) {
if (Objects.equals(value, entry.getValue())) {
removeFromSelection(entry);
return;
}
}
}
public int size() {
return _map.size();
}
public Collection<V> values() {
return new Values(_map.values());
}
//-- Sortable --//
/** Sorts the data.
*
* @param cmpr the comparator.
* @param ascending whether to sort in the ascending order.
* It is ignored since this implementation uses cmpr to compare.
*/
public void sort(Comparator<Map.Entry<K, V>> cmpr, final boolean ascending) {
final List<Map.Entry<K, V>> copy = new ArrayList<Map.Entry<K, V>>(_map.entrySet());
_sorting = cmpr;
_sortDir = ascending;
try {
Collections.sort(copy, cmpr);
} catch (ClassCastException e) {
throw new UiException("Unable to sort, maybe you should use FieldComparator, sort=\"auto(key)\" or sort=\"auto(value)\"", e);
}
_map.clear();
for (Map.Entry<K, V> me : copy) {
_map.put(me.getKey(), me.getValue());
}
fireEvent(ListDataEvent.STRUCTURE_CHANGED, -1, -1);
}
public void sort() {
if (_sorting == null)
throw new UiException("The sorting comparator is not assigned, please use sort(Comparator cmpr, final boolean ascending)");
sort(_sorting, _sortDir);
}
public String getSortDirection(Comparator<Map.Entry<K, V>> cmpr) {
if (Objects.equals(_sorting, cmpr))
return _sortDir ? "ascending" : "descending";
return "natural";
}
@SuppressWarnings("unchecked")
private boolean removePartial(Collection<?> master, Collection<?> c, boolean isRemove, boolean byKey,
boolean byValue) {
int sz = c.size();
int removed = 0;
int retained = 0;
int index = 0;
int begin = -1;
// B60-ZK-1126.zul
// Remember the selections to be cleared
List selected = new ArrayList();
for (final Iterator<?> it = master.iterator(); it.hasNext() && (!isRemove || removed < sz)
&& (isRemove || retained < sz); ++index) {
Object item = it.next();
if (c.contains(item) == isRemove) {
if (begin < 0) {
begin = index;
}
++removed;
it.remove();
// B60-ZK-1126.zul
// Clear the selection later
selected.add(item);
} else {
++retained;
if (begin >= 0) {
fireEvent(ListDataEvent.INTERVAL_REMOVED, begin, index - 1);
index = begin; //this range removed, the index is reset to begin
begin = -1;
}
}
}
// B60-ZK-1126.zul
// Clear the selected items that were removed
if (!selected.isEmpty()) {
if (byKey) {
for (Object item : selected) {
removeSelectionByKey(item);
}
} else if (byValue) {
for (Object item : selected) {
removeSelectionByValue(item);
}
} else {
for (Object item : selected) {
removeFromSelection(item);
}
}
}
if (begin >= 0) {
fireEvent(ListDataEvent.INTERVAL_REMOVED, begin, index - 1);
}
return removed > 0;
}
private class MyIterator<E> implements Iterator<E> {
private Iterator<E> _it;
private E _current;
private int _nextIndex;
public MyIterator(Iterator<E> inner) {
_it = inner;
}
public boolean hasNext() {
return _it.hasNext();
}
public E next() {
_current = _it.next();
++_nextIndex;
return _current;
}
public void remove() {
_it.remove();
//bug #1819318 Problem while using SortedSet with Databinding
removeFromSelection(_current);
--_nextIndex;
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
fireEvent(ListDataEvent.INTERVAL_REMOVED, _nextIndex, _nextIndex);
} else {
//bug #1839634 Problem while using HashSet with Databinding
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
}
}
private abstract class MySet<E> implements Set<E> {
private final Set<E> _set;
private final boolean _keyset;
public MySet(Set<E> inner, boolean keyset) {
_set = inner;
_keyset = keyset;
}
public void clear() {
int i2 = _set.size() - 1;
if (i2 < 0) {
return;
}
clearSelection();
_set.clear();
fireEvent(ListDataEvent.INTERVAL_REMOVED, 0, i2);
}
public boolean remove(Object o) {
boolean ret = false;
if (_set.contains(o)) {
//bug #1819318 Problem while using SortedSet with Databinding
removeFromSelection(o);
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
final int index = indexOf(o);
ret = _set.remove(o);
fireEvent(ListDataEvent.INTERVAL_REMOVED, index, index);
} else { //bug #1839634 Problem while using HashSet with Databinding
ret = _set.remove(o);
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
}
return ret;
}
protected abstract int indexOf(Object o);
public boolean removeAll(Collection<?> c) {
if (_set == c || this == c) { //special case
clear();
return true;
}
//bug #1819318 Problem while using SortedSet with Databinding
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
return removePartial(_set, c, true, _keyset, false);
} else { //bug #1839634 Problem while using HashSet with Databinding
removeAllSelection(c);
final boolean ret = _set.removeAll(c);
if (ret) {
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
return ret;
}
}
public boolean retainAll(Collection<?> c) {
if (_set == c || this == c) { //special case
return false;
}
//bug #1819318 Problem while using SortedSet with Databinding
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
return removePartial(_set, c, false, _keyset, false);
} else { //bug #1839634 Problem while using HashSet with Databinding
retainAllSelection(c);
final boolean ret = _set.retainAll(c);
if (ret) {
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
return ret;
}
}
public Iterator<E> iterator() {
return new MyIterator<E>(_set.iterator());
}
public boolean add(E o) {
throw new UnsupportedOperationException("add()");
}
public boolean addAll(Collection<? extends E> col) {
throw new UnsupportedOperationException("addAll()");
}
public boolean contains(Object o) {
return _set == null ? false : _set.contains(o);
}
public boolean containsAll(Collection<?> c) {
return _set == null ? false : _set.containsAll(c);
}
@SuppressWarnings("unchecked")
public boolean equals(Object o) {
if (this == o)
return true;
if (o instanceof MySet) {
return Objects.equals(((MySet<?>) o)._set, _set);
} else {
return Objects.equals(_set, o);
}
}
public int hashCode() {
return Objects.hashCode(_set);
}
public boolean isEmpty() {
return _set == null || _set.isEmpty();
}
public int size() {
return _set == null ? 0 : _set.size();
}
public Object[] toArray() {
return _set == null ? new Object[0] : _set.toArray();
}
public <T> T[] toArray(T[] a) {
return _set == null ? a : _set.toArray(a);
}
}
/** Represents the key set.
*/
private class Keys extends MySet<K> {
public Keys(Set<K> inner) {
super(inner, true);
}
protected int indexOf(Object o) {
return indexOfKey(o);
}
}
private class Entries extends MySet<Map.Entry<K, V>> {
private Entries(Set<Map.Entry<K, V>> inner) {
super(inner, false);
}
protected int indexOf(Object o) {
return ListModelMap.this.indexOf(o);
}
}
private abstract class MyCol<E> implements Collection<E> {
protected Collection<E> _col;
public MyCol(Collection<E> col) {
_col = col;
}
public Iterator<E> iterator() {
return new MyIterator<E>(_col.iterator());
}
public boolean add(E o) {
throw new UnsupportedOperationException("add()");
}
public boolean addAll(Collection<? extends E> col) {
throw new UnsupportedOperationException("addAll()");
}
public boolean contains(Object o) {
return _col == null ? false : _col.contains(o);
}
public boolean containsAll(Collection<?> c) {
return _col == null ? false : _col.containsAll(c);
}
public int hashCode() {
return _col == null ? 0 : _col.hashCode();
}
@SuppressWarnings("unchecked")
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof MyCol) {
return Objects.equals(((MyCol<?>) o)._col, _col);
} else {
return Objects.equals(_col, o);
}
}
public boolean isEmpty() {
return _col == null ? true : _col.isEmpty();
}
public int size() {
return _col == null ? 0 : _col.size();
}
public Object[] toArray() {
return _col == null ? new Object[0] : _col.toArray();
}
public <T> T[] toArray(T[] a) {
return _col == null ? a : _col.toArray(a);
}
}
private class Values extends MyCol<V> {
private Collection<V> _col;
public Values(Collection<V> col) {
super(col);
}
public void clear() {
int i2 = _col.size() - 1;
if (i2 < 0) {
return;
}
clearSelection();
_col.clear();
fireEvent(ListDataEvent.INTERVAL_REMOVED, 0, i2);
}
private int indexOfAndRemove(Object o) {
int j = 0;
for (Iterator<?> it = _col.iterator(); it.hasNext(); ++j) {
final Object val = it.next();
if (Objects.equals(val, o)) {
removeFromSelection(o);
it.remove();
return j;
}
}
return -1;
}
public boolean remove(Object o) {
//bug #1819318 Problem while using SortedSet with Databinding
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
int index = indexOfAndRemove(o);
if (index < 0) {
return false;
}
fireEvent(ListDataEvent.INTERVAL_REMOVED, index, index);
return true;
} else {
removeFromSelection(o);
final boolean ret = _col.remove(o);
if (ret) {
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
return ret;
}
}
public boolean removeAll(Collection<?> c) {
if (_col == c || this == c) { //special case
clearSelection();
clear();
return true;
}
//bug #1819318 Problem while using SortedSet with Databinding
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
return removePartial(_col, c, true, false, true);
} else { //bug #1839634 Problem while using HashSet with Databinding
removeAllSelection(c);
final boolean ret = _col.removeAll(c);
if (ret) {
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
return ret;
}
}
public boolean retainAll(Collection<?> c) {
if (_col == c || this == c) { //special case
return false;
}
//bug #1819318 Problem while using SortedSet with Databinding
if (_map instanceof LinkedHashMap || _map instanceof SortedMap) {
return removePartial(_col, c, false, false, true);
} else { //bug #1839634 Problem while using HashSet with Databinding
retainAllSelection(c);
final boolean ret = _col.retainAll(c);
if (ret) {
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
}
return ret;
}
}
}
@SuppressWarnings("unchecked")
public Object clone() {
ListModelMap<K, V> clone = (ListModelMap<K, V>) super.clone();
if (_map != null)
clone._map = new LinkedHashMap<K, V>(_map);
return clone;
}
protected void fireSelectionEvent(Entry<K, V> e) {
fireEvent(ListDataEvent.SELECTION_CHANGED, indexOf(e), -1);
}
protected void writeSelection(java.io.ObjectOutputStream s) throws java.io.IOException {
s.writeInt(_selection.size());
for (final Map.Entry<K, V> sel : _selection)
s.writeObject(sel.getKey());
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void readSelection(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
_selection = newEmptySelection();
int size = s.readInt();
while (--size >= 0) {
((Set) _selection).add(s.readObject());
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
//the performance is bad, but no better algorithm
@SuppressWarnings("rawtypes")
Set selection = _selection;
_selection = newEmptySelection();
for (final Object key : selection) {
if (_map.containsKey(key))
for (Map.Entry<K, V> entry : _map.entrySet())
if (Objects.equals(key, entry.getKey()))
_selection.add(entry);
}
}
}