You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BeanItemContainer.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. /*
  2. @ITMillApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.data.util;
  5. import java.beans.PropertyDescriptor;
  6. import java.io.IOException;
  7. import java.util.ArrayList;
  8. import java.util.Collection;
  9. import java.util.Collections;
  10. import java.util.HashMap;
  11. import java.util.HashSet;
  12. import java.util.Iterator;
  13. import java.util.LinkedHashMap;
  14. import java.util.LinkedList;
  15. import java.util.List;
  16. import java.util.Map;
  17. import java.util.Set;
  18. import com.vaadin.data.Container;
  19. import com.vaadin.data.Property;
  20. import com.vaadin.data.Container.Filterable;
  21. import com.vaadin.data.Container.Indexed;
  22. import com.vaadin.data.Container.ItemSetChangeNotifier;
  23. import com.vaadin.data.Container.Sortable;
  24. import com.vaadin.data.Property.ValueChangeEvent;
  25. import com.vaadin.data.Property.ValueChangeListener;
  26. import com.vaadin.data.Property.ValueChangeNotifier;
  27. /**
  28. * An {@link ArrayList} backed container for {@link BeanItem}s.
  29. * <p>
  30. * Bean objects act as identifiers. For this reason, they should implement
  31. * Object.equals(Object) and Object.hashCode().
  32. * </p>
  33. *
  34. * @param <BT>
  35. *
  36. * @since 5.4
  37. */
  38. @SuppressWarnings("serial")
  39. public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable,
  40. ItemSetChangeNotifier, ValueChangeListener {
  41. /**
  42. * The filteredItems variable contains the items that are visible outside
  43. * the container. If filters are enabled this contains a subset of allItems,
  44. * if no filters are set this contains the same items as allItems.
  45. */
  46. private ListSet<BT> filteredItems = new ListSet<BT>();
  47. /**
  48. * The allItems variable always contains all the items in the container.
  49. * Some or all of these are also in the filteredItems list.
  50. */
  51. private ListSet<BT> allItems = new ListSet<BT>();
  52. /**
  53. * Maps all pojos (item ids) in the container (including filtered) to their
  54. * corresponding BeanItem.
  55. */
  56. private final Map<BT, BeanItem<BT>> beanToItem = new HashMap<BT, BeanItem<BT>>();
  57. // internal data model to obtain property IDs etc.
  58. private final Class<? extends BT> type;
  59. private transient LinkedHashMap<String, PropertyDescriptor> model;
  60. private List<ItemSetChangeListener> itemSetChangeListeners;
  61. private Set<Filter> filters = new HashSet<Filter>();
  62. /**
  63. * The item sorter which is used for sorting the container.
  64. */
  65. private ItemSorter itemSorter = new DefaultItemSorter();
  66. /* Special serialization to handle method references */
  67. private void readObject(java.io.ObjectInputStream in) throws IOException,
  68. ClassNotFoundException {
  69. in.defaultReadObject();
  70. model = BeanItem.getPropertyDescriptors(type);
  71. }
  72. /**
  73. * Constructs BeanItemContainer for beans of a given type.
  74. *
  75. * @param type
  76. * the class of beans to be used with this containers.
  77. * @throws IllegalArgumentException
  78. * If the type is null
  79. */
  80. public BeanItemContainer(Class<? extends BT> type) {
  81. if (type == null) {
  82. throw new IllegalArgumentException(
  83. "The type passed to BeanItemContainer must not be null");
  84. }
  85. this.type = type;
  86. model = BeanItem.getPropertyDescriptors(type);
  87. }
  88. /**
  89. * Constructs BeanItemContainer with given collection of beans in it. The
  90. * collection must not be empty or an IllegalArgument is thrown.
  91. *
  92. * @param collection
  93. * non empty {@link Collection} of beans.
  94. * @throws IllegalArgumentException
  95. * If the collection is null or empty.
  96. */
  97. @SuppressWarnings("unchecked")
  98. public BeanItemContainer(Collection<BT> collection)
  99. throws IllegalArgumentException {
  100. if (collection == null || collection.isEmpty()) {
  101. throw new IllegalArgumentException(
  102. "The collection passed to BeanItemContainer must not be null or empty");
  103. }
  104. type = (Class<? extends BT>) collection.iterator().next().getClass();
  105. model = BeanItem.getPropertyDescriptors(type);
  106. addAll(collection);
  107. }
  108. private void addAll(Collection<BT> collection) {
  109. // Pre-allocate space for the collection
  110. allItems.ensureCapacity(allItems.size() + collection.size());
  111. int idx = size();
  112. for (BT bean : collection) {
  113. if (internalAddAt(idx, bean) != null) {
  114. idx++;
  115. }
  116. }
  117. // Filter the contents when all items have been added
  118. filterAll();
  119. }
  120. /**
  121. * Unsupported operation.
  122. *
  123. * @see com.vaadin.data.Container.Indexed#addItemAt(int)
  124. */
  125. public Object addItemAt(int index) throws UnsupportedOperationException {
  126. throw new UnsupportedOperationException();
  127. }
  128. /**
  129. * Adds new item at given index.
  130. *
  131. * The bean is used both as the item contents and as the item identifier.
  132. *
  133. * @see com.vaadin.data.Container.Indexed#addItemAt(int, Object)
  134. */
  135. public BeanItem<BT> addItemAt(int index, Object newItemId)
  136. throws UnsupportedOperationException {
  137. if (index < 0 || index > size()) {
  138. return null;
  139. } else if (index == 0) {
  140. // add before any item, visible or not
  141. return addItemAtInternalIndex(0, newItemId);
  142. } else {
  143. // if index==size(), adds immediately after last visible item
  144. return addItemAfter(getIdByIndex(index - 1), newItemId);
  145. }
  146. }
  147. /**
  148. * Adds new item at given index of the internal (unfiltered) list.
  149. * <p>
  150. * The item is also added in the visible part of the list if it passes the
  151. * filters.
  152. * </p>
  153. *
  154. * @param index
  155. * Internal index to add the new item.
  156. * @param newItemId
  157. * Id of the new item to be added.
  158. * @return Returns new item or null if the operation fails.
  159. */
  160. private BeanItem<BT> addItemAtInternalIndex(int index, Object newItemId) {
  161. BeanItem<BT> beanItem = internalAddAt(index, (BT) newItemId);
  162. if (beanItem != null) {
  163. filterAll();
  164. }
  165. return beanItem;
  166. }
  167. /**
  168. * Adds the bean to all internal data structures at the given position.
  169. * Fails if the bean is already in the container or is not assignable to the
  170. * correct type. Returns the new BeanItem if the bean was added
  171. * successfully.
  172. *
  173. * <p>
  174. * Caller should call {@link #filterAll()} after calling this method to
  175. * ensure the filtered list is updated.
  176. * </p>
  177. *
  178. * @param position
  179. * The position at which the bean should be inserted
  180. * @param bean
  181. * The bean to insert
  182. *
  183. * @return true if the bean was added successfully, false otherwise
  184. */
  185. private BeanItem<BT> internalAddAt(int position, BT bean) {
  186. // Make sure that the item has not been added previously
  187. if (allItems.contains(bean)) {
  188. return null;
  189. }
  190. if (!type.isAssignableFrom(bean.getClass())) {
  191. return null;
  192. }
  193. // "filteredList" will be updated in filterAll() which should be invoked
  194. // by the caller after calling this method.
  195. allItems.add(position, bean);
  196. BeanItem<BT> beanItem = new BeanItem<BT>(bean, model);
  197. beanToItem.put(bean, beanItem);
  198. // add listeners to be able to update filtering on property
  199. // changes
  200. for (Filter filter : filters) {
  201. // addValueChangeListener avoids adding duplicates
  202. addValueChangeListener(beanItem, filter.propertyId);
  203. }
  204. return beanItem;
  205. }
  206. @SuppressWarnings("unchecked")
  207. public BT getIdByIndex(int index) {
  208. return filteredItems.get(index);
  209. }
  210. public int indexOfId(Object itemId) {
  211. return filteredItems.indexOf(itemId);
  212. }
  213. /**
  214. * Unsupported operation.
  215. *
  216. * @see com.vaadin.data.Container.Ordered#addItemAfter(Object)
  217. */
  218. public Object addItemAfter(Object previousItemId)
  219. throws UnsupportedOperationException {
  220. throw new UnsupportedOperationException();
  221. }
  222. /**
  223. * Adds new item after the given item.
  224. *
  225. * The bean is used both as the item contents and as the item identifier.
  226. *
  227. * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
  228. */
  229. public BeanItem<BT> addItemAfter(Object previousItemId, Object newItemId)
  230. throws UnsupportedOperationException {
  231. // only add if the previous item is visible
  232. if (containsId(previousItemId)) {
  233. return addItemAtInternalIndex(allItems.indexOf(previousItemId) + 1,
  234. newItemId);
  235. } else {
  236. return null;
  237. }
  238. }
  239. public BT firstItemId() {
  240. if (size() > 0) {
  241. return getIdByIndex(0);
  242. } else {
  243. return null;
  244. }
  245. }
  246. public boolean isFirstId(Object itemId) {
  247. return firstItemId() == itemId;
  248. }
  249. public boolean isLastId(Object itemId) {
  250. return lastItemId() == itemId;
  251. }
  252. public BT lastItemId() {
  253. if (size() > 0) {
  254. return getIdByIndex(size() - 1);
  255. } else {
  256. return null;
  257. }
  258. }
  259. public BT nextItemId(Object itemId) {
  260. int index = indexOfId(itemId);
  261. if (index >= 0 && index < size() - 1) {
  262. return getIdByIndex(index + 1);
  263. } else {
  264. // out of bounds
  265. return null;
  266. }
  267. }
  268. public BT prevItemId(Object itemId) {
  269. int index = indexOfId(itemId);
  270. if (index > 0) {
  271. return getIdByIndex(index - 1);
  272. } else {
  273. // out of bounds
  274. return null;
  275. }
  276. }
  277. public boolean addContainerProperty(Object propertyId, Class<?> type,
  278. Object defaultValue) throws UnsupportedOperationException {
  279. throw new UnsupportedOperationException();
  280. }
  281. /**
  282. * Unsupported operation.
  283. *
  284. * @see com.vaadin.data.Container#addItem()
  285. */
  286. public Object addItem() throws UnsupportedOperationException {
  287. throw new UnsupportedOperationException();
  288. }
  289. /**
  290. * Creates a new Item with the bean into the Container.
  291. *
  292. * The bean is used both as the item contents and as the item identifier.
  293. *
  294. * @see com.vaadin.data.Container#addItem(Object)
  295. */
  296. public BeanItem<BT> addBean(BT bean) {
  297. return addItem(bean);
  298. }
  299. /**
  300. * Creates a new Item with the bean into the Container.
  301. *
  302. * The bean is used both as the item contents and as the item identifier.
  303. *
  304. * @see com.vaadin.data.Container#addItem(Object)
  305. */
  306. public BeanItem<BT> addItem(Object itemId)
  307. throws UnsupportedOperationException {
  308. if (size() > 0) {
  309. // add immediately after last visible item
  310. int lastIndex = allItems.indexOf(lastItemId());
  311. return addItemAtInternalIndex(lastIndex + 1, itemId);
  312. } else {
  313. return addItemAtInternalIndex(0, itemId);
  314. }
  315. }
  316. public boolean containsId(Object itemId) {
  317. // only look at visible items after filtering
  318. return filteredItems.contains(itemId);
  319. }
  320. public Property getContainerProperty(Object itemId, Object propertyId) {
  321. return getItem(itemId).getItemProperty(propertyId);
  322. }
  323. public Collection<String> getContainerPropertyIds() {
  324. return model.keySet();
  325. }
  326. public BeanItem<BT> getItem(Object itemId) {
  327. return beanToItem.get(itemId);
  328. }
  329. @SuppressWarnings("unchecked")
  330. public Collection<BT> getItemIds() {
  331. return (Collection<BT>) filteredItems.clone();
  332. }
  333. public Class<?> getType(Object propertyId) {
  334. return model.get(propertyId).getPropertyType();
  335. }
  336. public boolean removeAllItems() throws UnsupportedOperationException {
  337. allItems.clear();
  338. filteredItems.clear();
  339. // detach listeners from all BeanItems
  340. for (BeanItem<BT> item : beanToItem.values()) {
  341. removeAllValueChangeListeners(item);
  342. }
  343. beanToItem.clear();
  344. fireItemSetChange();
  345. return true;
  346. }
  347. public boolean removeContainerProperty(Object propertyId)
  348. throws UnsupportedOperationException {
  349. throw new UnsupportedOperationException();
  350. }
  351. public boolean removeItem(Object itemId)
  352. throws UnsupportedOperationException {
  353. if (!allItems.remove(itemId)) {
  354. return false;
  355. }
  356. // detach listeners from Item
  357. removeAllValueChangeListeners(getItem(itemId));
  358. // remove item
  359. beanToItem.remove(itemId);
  360. filteredItems.remove(itemId);
  361. fireItemSetChange();
  362. return true;
  363. }
  364. private void addValueChangeListener(BeanItem<BT> beanItem, Object propertyId) {
  365. Property property = beanItem.getItemProperty(propertyId);
  366. if (property instanceof ValueChangeNotifier) {
  367. // avoid multiple notifications for the same property if
  368. // multiple filters are in use
  369. ValueChangeNotifier notifier = (ValueChangeNotifier) property;
  370. notifier.removeListener(this);
  371. notifier.addListener(this);
  372. }
  373. }
  374. private void removeValueChangeListener(BeanItem<BT> item, Object propertyId) {
  375. Property property = item.getItemProperty(propertyId);
  376. if (property instanceof ValueChangeNotifier) {
  377. ((ValueChangeNotifier) property).removeListener(this);
  378. }
  379. }
  380. private void removeAllValueChangeListeners(BeanItem<BT> item) {
  381. for (Object propertyId : item.getItemPropertyIds()) {
  382. removeValueChangeListener(item, propertyId);
  383. }
  384. }
  385. public int size() {
  386. return filteredItems.size();
  387. }
  388. public Collection<Object> getSortableContainerPropertyIds() {
  389. LinkedList<Object> sortables = new LinkedList<Object>();
  390. for (Object propertyId : getContainerPropertyIds()) {
  391. Class<?> propertyType = getType(propertyId);
  392. if (Comparable.class.isAssignableFrom(propertyType)) {
  393. sortables.add(propertyId);
  394. }
  395. }
  396. return sortables;
  397. }
  398. /*
  399. * (non-Javadoc)
  400. *
  401. * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
  402. * boolean[])
  403. */
  404. public void sort(Object[] propertyId, boolean[] ascending) {
  405. itemSorter.setSortProperties(this, propertyId, ascending);
  406. doSort();
  407. // notifies if anything changes in the filtered list, including order
  408. filterAll();
  409. }
  410. /**
  411. * Perform the sorting of the data structures in the container. This is
  412. * invoked when the <code>itemSorter</code> has been prepared for the sort
  413. * operation. Typically this method calls
  414. * <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays
  415. * (containing item ids) that need to be sorted.
  416. *
  417. */
  418. protected void doSort() {
  419. Collections.sort(allItems, getItemSorter());
  420. }
  421. public void addListener(ItemSetChangeListener listener) {
  422. if (itemSetChangeListeners == null) {
  423. itemSetChangeListeners = new LinkedList<ItemSetChangeListener>();
  424. }
  425. itemSetChangeListeners.add(listener);
  426. }
  427. public void removeListener(ItemSetChangeListener listener) {
  428. if (itemSetChangeListeners != null) {
  429. itemSetChangeListeners.remove(listener);
  430. }
  431. }
  432. private void fireItemSetChange() {
  433. if (itemSetChangeListeners != null) {
  434. final Container.ItemSetChangeEvent event = new Container.ItemSetChangeEvent() {
  435. public Container getContainer() {
  436. return BeanItemContainer.this;
  437. }
  438. };
  439. for (ItemSetChangeListener listener : itemSetChangeListeners) {
  440. listener.containerItemSetChange(event);
  441. }
  442. }
  443. }
  444. public void addContainerFilter(Object propertyId, String filterString,
  445. boolean ignoreCase, boolean onlyMatchPrefix) {
  446. if (filters.isEmpty()) {
  447. filteredItems = (ListSet<BT>) allItems.clone();
  448. }
  449. // listen to change events to be able to update filtering
  450. for (BeanItem<BT> item : beanToItem.values()) {
  451. addValueChangeListener(item, propertyId);
  452. }
  453. Filter f = new Filter(propertyId, filterString, ignoreCase,
  454. onlyMatchPrefix);
  455. filter(f);
  456. filters.add(f);
  457. fireItemSetChange();
  458. }
  459. /**
  460. * Filter the view to recreate the visible item list from the unfiltered
  461. * items, and send a notification if the set of visible items changed in any
  462. * way.
  463. */
  464. protected void filterAll() {
  465. // avoid notification if the filtering had no effect
  466. List<BT> originalItems = filteredItems;
  467. // it is somewhat inefficient to do a (shallow) clone() every time
  468. filteredItems = (ListSet<BT>) allItems.clone();
  469. for (Filter f : filters) {
  470. filter(f);
  471. }
  472. // check if exactly the same items are there after filtering to avoid
  473. // unnecessary notifications
  474. // this may be slow in some cases as it uses BT.equals()
  475. if (!originalItems.equals(filteredItems)) {
  476. fireItemSetChange();
  477. }
  478. }
  479. protected void filter(Filter f) {
  480. Iterator<BT> iterator = filteredItems.iterator();
  481. while (iterator.hasNext()) {
  482. BT bean = iterator.next();
  483. if (!f.passesFilter(getItem(bean))) {
  484. iterator.remove();
  485. }
  486. }
  487. }
  488. public void removeAllContainerFilters() {
  489. if (!filters.isEmpty()) {
  490. filters = new HashSet<Filter>();
  491. // stop listening to change events for any property
  492. for (BeanItem<BT> item : beanToItem.values()) {
  493. removeAllValueChangeListeners(item);
  494. }
  495. filterAll();
  496. }
  497. }
  498. public void removeContainerFilters(Object propertyId) {
  499. if (!filters.isEmpty()) {
  500. for (Iterator<Filter> iterator = filters.iterator(); iterator
  501. .hasNext();) {
  502. Filter f = iterator.next();
  503. if (f.propertyId.equals(propertyId)) {
  504. iterator.remove();
  505. }
  506. }
  507. // stop listening to change events for the property
  508. for (BeanItem<BT> item : beanToItem.values()) {
  509. removeValueChangeListener(item, propertyId);
  510. }
  511. filterAll();
  512. }
  513. }
  514. public void valueChange(ValueChangeEvent event) {
  515. // if a property that is used in a filter is changed, refresh filtering
  516. filterAll();
  517. }
  518. public ItemSorter getItemSorter() {
  519. return itemSorter;
  520. }
  521. public void setItemSorter(ItemSorter itemSorter) {
  522. this.itemSorter = itemSorter;
  523. }
  524. }