/* GRANITE DATA SERVICES Copyright (C) 2012 GRANITE DATA SERVICES S.A.S. This file is part of Granite Data Services. Granite Data Services is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Granite Data Services is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, see <http://www.gnu.org/licenses/>. */ package org.granite.client.tide.collections; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Set; import javax.annotation.PreDestroy; import org.granite.client.tide.data.EntityManager.UpdateKind; import org.granite.client.tide.events.TideEvent; import org.granite.client.tide.events.TideEventObserver; import org.granite.client.tide.server.TideFaultEvent; import org.granite.client.tide.server.TideResponder; import org.granite.client.tide.server.TideResultEvent; import org.granite.client.tide.server.TideRpcEvent; import org.granite.logging.Logger; import org.granite.tide.data.model.Page; /** * @author William DRAI */ public abstract class AbstractPagedCollection<E> implements List<E>, TideEventObserver { private static final Logger log = Logger.getLogger(AbstractPagedCollection.class); protected boolean initializing = false; private boolean initSent = false; protected int first; protected int last; // Current last index of local data protected int max; // Page size protected int count; // Result count private E[] localIndex = null; protected boolean fullRefresh = false; protected boolean filterRefresh = false; public AbstractPagedCollection() { super(); log.debug("create collection"); first = 0; last = 0; count = 0; initializing = true; } /** * Get total number of elements * * @return collection total size */ @Override public int size() { if (initialFind()) return 0; else if (localIndex == null) return 0; return count; } /** * Set the page size. The collection will store in memory twice this page size, and each server call * will return at most the page size. * * @param max maximum number of requested elements */ public void setMaxResults(int max) { this.max = max; } private Class<? extends E> elementClass; private String elementName; private Set<String> entityNames = new HashSet<String>(); public void setElementClass(Class<? extends E> elementClass) { this.elementClass = elementClass; if (this.elementName != null) entityNames.remove(elementName); elementName = elementClass != null ? elementClass.getSimpleName() : null; if (this.elementName != null) entityNames.add(this.elementName); } @Override public void handleEvent(TideEvent event) { if (event.getType().startsWith(UpdateKind.REFRESH.eventName() + ".")) { String entityName = event.getType().substring(UpdateKind.REFRESH.eventName().length()+1); if (entityNames.contains(entityName)) fullRefresh(); } } /** * Clear collection content */ @Override @PreDestroy public void clear() { initializing = true; initSent = false; getInternalWrappedList().clear(); clearLocalIndex(); first = 0; last = first+max; fullRefresh = false; filterRefresh = false; } private List<Integer[]> pendingRanges = new ArrayList<Integer[]>(); /** * Abstract method: trigger a results query for the current filter * @param first : index of first required result * @param last : index of last required result */ protected void find(int first, int last) { log.debug("find from %d to %d", first, last); pendingRanges.add(new Integer[] { first, last }); } /** * Force refresh of collection when filter/sort have been changed * * @return always false */ public boolean fullRefresh() { this.fullRefresh = true; return refresh(); } /** * Refresh collection with new filter/sort parameters * * @return always false */ public boolean refresh() { // Recheck sort fields to listen for asc/desc change events pendingRanges.clear(); if (fullRefresh) { log.debug("full refresh"); clearLocalIndex(); fullRefresh = false; if (filterRefresh) { first = 0; last = first+max; filterRefresh = false; } } else log.debug("refresh"); if (!initialFind()) find(first, last); return true; } private boolean initialFind() { if (max > 0 && !initializing) return false; if (!initSent) { log.debug("initial find"); find(0, max); initSent = true; } return true; } private void clearLocalIndex() { localIndex = null; } /** * Build a result object from the result event * * @param event the result event * @param first first index requested * @param max max elements requested * * @return a Page object containing data from the collection * resultList : the retrieved data * resultCount : the total count of elements (non paged) * firstResult : the index of the first retrieved element * maxResults : the maximum count of retrieved elements */ protected abstract Page<E> getResult(TideResultEvent<?> event, int first, int max); /** * Notify listeners of remote page result * * @param event the remote event (ResultEvent or FaultEvent) */ protected abstract void firePageChange(TideRpcEvent event); /** * Initialize collection after first find * * @param event the result event of the first find */ protected void initialize(TideResultEvent<?> event) { } /** * Event handler for results query * * @param event the result event * @param first first requested index * @param max max elements requested */ protected void findResult(TideResultEvent<?> event, int first, int max) { Page<E> page = getResult(event, first, max); handleResult(page, event, first, max); } /** * Event handler for results query * * @param page the result page * @param event the result event * @param first first requested index * @param max max elements requested */ @SuppressWarnings("unchecked") protected void handleResult(Page<E> page, TideResultEvent<?> event, int first, int max) { List<E> list = (List<E>)page.getResultList(); for (Iterator<Integer[]> ipr = pendingRanges.iterator(); ipr.hasNext(); ) { Integer[] pr = ipr.next(); if (pr[0] == first && pr[1] == first+max) { ipr.remove(); break; } } if (initializing && event != null) { if (this.max == 0 && page.getMaxResults() > 0) this.max = page.getMaxResults(); initialize(event); } int nextFirst = (Integer)page.getFirstResult(); int nextLast = nextFirst + (Integer)page.getMaxResults(); int pageNum = max > 0 ? nextFirst / max : 0; log.debug("handle result page %d (%d - %d)", pageNum, nextFirst, nextLast); if (!initializing) { log.debug("Adjusting from %d-%d to %d-%d size %d", AbstractPagedCollection.this.first, AbstractPagedCollection.this.last, nextFirst, nextLast, list.size()); // Adjust internal list to expected results without triggering events if (nextFirst > AbstractPagedCollection.this.first && nextFirst < AbstractPagedCollection.this.last) { getInternalWrappedList().subList(0, Math.min(getInternalWrappedList().size(), nextFirst - AbstractPagedCollection.this.first)).clear(); for (int i = 0; i < nextFirst - AbstractPagedCollection.this.first && AbstractPagedCollection.this.last - nextFirst + i < list.size(); i++) { E elt = list.get(AbstractPagedCollection.this.last - nextFirst + i); getInternalWrappedList().add(elt); } } else if (nextFirst == AbstractPagedCollection.this.first && nextLast > AbstractPagedCollection.this.last) { for (int i = 0; i < (nextLast-nextFirst)-(AbstractPagedCollection.this.last-AbstractPagedCollection.this.first) && AbstractPagedCollection.this.last + i < list.size(); i++) { E elt = list.get(AbstractPagedCollection.this.last + i); getInternalWrappedList().add(elt); } } else if (nextLast > AbstractPagedCollection.this.first && nextLast < AbstractPagedCollection.this.last) { if (nextLast-AbstractPagedCollection.this.first < getInternalWrappedList().size()) getInternalWrappedList().subList(nextLast-AbstractPagedCollection.this.first, getInternalWrappedList().size()).clear(); else getInternalWrappedList().clear(); for (int i = 0; i < AbstractPagedCollection.this.first - nextFirst && i < list.size(); i++) { E elt = list.get(i); getInternalWrappedList().add(i, elt); } } else if (nextFirst >= AbstractPagedCollection.this.last || nextLast <= AbstractPagedCollection.this.first) { getInternalWrappedList().clear(); for (int i = 0; i < list.size(); i++) { E elt = list.get(i); getInternalWrappedList().add(i, elt); } } } else getWrappedList().addAll(list); count = page.getResultCount(); initializing = false; if (localIndex != null) { List<String> entityNames = new ArrayList<String>(); for (int i = 0; i < localIndex.length; i++) { String entityName = localIndex[i].getClass().getSimpleName(); if (!entityName.equals(elementName)) entityNames.remove(entityName); } } for (Object o : list) { if (elementClass == null || (o != null && o.getClass().isAssignableFrom(elementClass))) elementClass = (Class<? extends E>)o.getClass(); } localIndex = (E[])Array.newInstance(elementClass, list.size()); localIndex = list.toArray(localIndex); if (localIndex != null) { for (int i = 0; i < localIndex.length; i++) { String entityName = localIndex[i].getClass().getSimpleName(); if (!entityName.equals(elementName)) entityNames.add(entityName); } } this.first = nextFirst; this.last = nextLast; pendingRanges.clear(); firePageChange(event); } /** * Event handler for results fault * * @param event the fault event * @param first first requested index * @param max max elements requested */ protected void findFault(TideFaultEvent event, int first, int max) { handleFault(event); } /** * Event handler for results query fault * * @param event the fault event */ protected void handleFault(TideFaultEvent event) { log.debug("findFault: %s", event); for (Iterator<Integer[]> ipr = pendingRanges.iterator(); ipr.hasNext(); ) { Integer[] pr = ipr.next(); if (pr[0] == first && pr[1] == first+max) { ipr.remove(); break; } } if (initializing) initSent = false; firePageChange(event); } protected abstract List<E> getInternalWrappedList(); protected abstract List<E> getWrappedList(); /** * Override of get() with lazy page loading * * @param index index of requested item * @return object at specified index */ @Override public E get(int index) { if (index < 0) return null; if (initialFind()) return null; if (localIndex != null && index >= first && index < last) { // Local data available for index int j = index-first; if (j >= 0 && j < localIndex.length) return localIndex[j]; // Index not in current loaded range, max is more than last page size return null; } // If already in a pending range, return null for (Integer[] pendingRange : pendingRanges) { if (index >= pendingRange[0] && index < pendingRange[1]) return null; } int page = index / max; // Trigger a results query for requested page int nfi = 0; int nla = 0; @SuppressWarnings("unused") int idx = page * max; if (index >= last && index < last + max) { nfi = first; nla = last + max; if (nla > nfi + 2*max) nfi = nla - 2*max; if (nfi < 0) nfi = 0; if (nla > count) nla = count; } else if (index < first && index >= first - max) { nfi = first - max; if (nfi < 0) nfi = 0; nla = last; if (nla > nfi + 2*max) nla = nfi + 2*max; if (nla > count) nla = count; } else { nfi = index - max; nla = nfi + 2 * max; if (nfi < 0) nfi = 0; if (nla > count) nla = count; } log.debug("request find for index " + index); find(nfi, nla); return null; } @Override public boolean isEmpty() { return size() == 0; } @Override public boolean contains(Object o) { if (o == null) return false; if (localIndex != null) { for (Object obj : localIndex) { if (o.equals(obj)) return true; } } return false; } @Override public boolean containsAll(Collection<?> c) { return false; } @Override public int indexOf(Object o) { if (o == null) return -1; if (localIndex != null) { for (int i = 0; i < localIndex.length; i++) { if (o.equals(localIndex[i])) return first+i;; } } return -1; } @Override public int lastIndexOf(Object o) { if (o == null) return -1; if (localIndex != null) { int index = -1; for (int i = 0; i < localIndex.length; i++) { if (o.equals(localIndex[i])) index = first+i;; } return index; } return -1; } @Override public Iterator<E> iterator() { return new PagedCollectionIterator(); } @Override public ListIterator<E> listIterator() { return new PagedCollectionIterator(); } @Override public ListIterator<E> listIterator(int index) { return new PagedCollectionIterator(); } @Override public boolean add(E e) { throw new UnsupportedOperationException(); } @Override public void add(int index, E element) { throw new UnsupportedOperationException(); } @Override public boolean addAll(Collection<? extends E> c) { throw new UnsupportedOperationException(); } @Override public boolean addAll(int index, Collection<? extends E> c) { throw new UnsupportedOperationException(); } @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } @Override public E remove(int index) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(Collection<?> c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException(); } @Override public E set(int index, E element) { throw new UnsupportedOperationException(); } @Override public List<E> subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } public class PagedCollectionIterator implements ListIterator<E> { private ListIterator<E> wrappedListIterator; public PagedCollectionIterator() { wrappedListIterator = getWrappedList().listIterator(); } public PagedCollectionIterator(int index) { wrappedListIterator = getWrappedList().listIterator(index); } @Override public boolean hasNext() { return wrappedListIterator.hasNext(); } @Override public E next() { return wrappedListIterator.next(); } @Override public boolean hasPrevious() { return wrappedListIterator.hasPrevious(); } @Override public E previous() { return wrappedListIterator.previous(); } @Override public int nextIndex() { return wrappedListIterator.nextIndex(); } @Override public int previousIndex() { return wrappedListIterator.previousIndex(); } @Override public void remove() { throw new UnsupportedOperationException(); } @Override public void set(E e) { throw new UnsupportedOperationException(); } @Override public void add(E e) { throw new UnsupportedOperationException(); } } public class PagedCollectionResponder implements TideResponder<Object> { private int first; private int max; public PagedCollectionResponder(int first, int max) { this.first = first; this.max = max; } @Override public void result(TideResultEvent<Object> event) { findResult(event, first, max); } public void fault(TideFaultEvent event) { findFault(event, first, max); } } }