/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Copyright (c) 2013, MPL CodeInside http://codeinside.ru
*/
package ru.codeinside.gses.lazyquerycontainer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Property.ValueChangeNotifier;
public final class LazyQueryView implements QueryView, ValueChangeListener {
/** Java serialization UID. */
private static final long serialVersionUID = 1L;
/** Query count debug property ID. */
public static final String DEBUG_PROPERTY_ID_QUERY_INDEX = "DEBUG_PROPERTY_ID_QUERY_COUT";
/** Batch index debug property ID. */
public static final String DEBUG_PROPERTY_ID_BATCH_INDEX = "DEBUG_PROPERTY_ID_BATCH_INDEX";
/** Batch query time debug property ID. */
public static final String DEBUG_PROPERTY_ID_BATCH_QUERY_TIME = "DEBUG_PROPERTY_ID_ACCESS_COUNT";
/** Item status property ID. */
public static final String PROPERTY_ID_ITEM_STATUS = "PROPERTY_ID_ITEM_STATUS";
/** Initial maximum cache size. */
private static final int DEFAULT_MAX_CACHE_SIZE = 100;
/** Maximum items in cache before old ones are evicted. */
private int maxCacheSize = DEFAULT_MAX_CACHE_SIZE;
/** Number of query executions. */
private int queryCount = 0;
/** Number of batches read. */
private int batchCount = 0;
/** QueryDefinition containing query properties and batch size. */
private QueryDefinition queryDefinition;
/** QueryFactory for constructing new queries when sort state changes. */
private QueryFactory queryFactory;
/** Currenct query used by view. */
private Query query;
/** Property IDs participating in sort. */
private Object[] sortPropertyIds;
/**
* Sort state of the properties participating in sort. If true then ascending else descending.
*/
private boolean[] ascendingStates;
/** List of item indexes in cache in order of access. */
private final LinkedList<Integer> itemCacheAccessLog = new LinkedList<Integer>();
/** Map of items in cache. */
private final Map<Integer, Item> itemCache = new HashMap<Integer, Item>();
/** Map from properties to items for items which are in cache. */
private Map<Property, Item> propertyItemMapCache = new HashMap<Property, Item>();
/** List of added items since last commit/rollback. */
private final List<Item> addedItems = new ArrayList<Item>();
/** List of modified items since last commit/rollback. */
private final List<Item> modifiedItems = new ArrayList<Item>();
/** List of deleted items since last commit/rollback. */
private final List<Item> removedItems = new ArrayList<Item>();
/**
* Constructs LazyQueryView with DefaultQueryDefinition and the given QueryFactory.
*
* @param queryFactory
* The QueryFactory to be used.
* @param compositeItems
* True if native items should be wrapped to CompositeItems.
* @param batchSize
* The batch size to be used when loading data.
*/
public LazyQueryView(final QueryFactory queryFactory, final boolean compositeItems, final int batchSize) {
initialize(new LazyQueryDefinition(compositeItems, batchSize), queryFactory);
}
/**
* Constructs LazyQueryView with given QueryDefinition and QueryFactory. The role of this constructor is to enable
* use of custom QueryDefinition implementations.
*
* @param queryDefinition
* The QueryDefinition to be used.
* @param queryFactory
* The QueryFactory to be used.
*/
public LazyQueryView(final QueryDefinition queryDefinition, final QueryFactory queryFactory) {
initialize(queryDefinition, queryFactory);
}
/**
* Initializes the LazyQueryView.
*
* @param queryDefinition
* The QueryDefinition to be used.
* @param queryFactory
* The QueryFactory to be used.
*/
private void initialize(final QueryDefinition queryDefinition, final QueryFactory queryFactory) {
this.queryDefinition = queryDefinition;
this.queryFactory = queryFactory;
this.queryFactory.setQueryDefinition(queryDefinition);
this.sortPropertyIds = new Object[0];
this.ascendingStates = new boolean[0];
}
/**
* Gets the QueryDefinition.
*
* @return the QueryDefinition
*/
@Override
public QueryDefinition getQueryDefinition() {
return queryDefinition;
}
/**
* Sets new sort state and refreshes view.
*
* @param sortPropertyIds
* The IDs of the properties participating in sort.
* @param ascendingStates
* The sort state of the properties participating in sort. True means ascending.
*/
@Override
public void sort(final Object[] sortPropertyIds, final boolean[] ascendingStates) {
this.sortPropertyIds = sortPropertyIds;
this.ascendingStates = ascendingStates;
refresh();
}
/**
* Refreshes the view by clearing cache, discarding buffered changes and current query instance. New query is
* created on demand.
*/
@Override
public void refresh() {
for (final Property property : propertyItemMapCache.keySet()) {
if (property instanceof ValueChangeNotifier) {
final ValueChangeNotifier notifier = (ValueChangeNotifier) property;
notifier.removeListener(this);
}
}
query = null;
batchCount = 0;
itemCache.clear();
itemCacheAccessLog.clear();
propertyItemMapCache.clear();
discard();
}
/**
* Returns the total size of query and added items since last commit.
*
* @return total number of items in the view.
*/
@Override
public int size() {
return getQuery().size() + addedItems.size();
}
/**
* Gets the batch size i.e. how many items is fetched at a time from storage.
*
* @return the batch size.
*/
public int getBatchSize() {
return queryDefinition.getBatchSize();
}
/**
* @return the maxCacheSize
*/
@Override
public int getMaxCacheSize() {
return maxCacheSize;
}
/**
* @param maxCacheSize
* the maxCacheSize to set
*/
@Override
public void setMaxCacheSize(final int maxCacheSize) {
this.maxCacheSize = maxCacheSize;
}
/**
* Gets item at given index from addedItems, cache and loads new batch on demand if required.
*
* @param index
* The item index.
* @return the item at given index.
*/
@Override
public Item getItem(final int index) {
final int addedItemCount = addedItems.size();
if (index < addedItemCount) {
// an item from the addedItems was requested
return addedItems.get(index);
}
if (!itemCache.containsKey(index - addedItemCount)) {
// item is not in our cache, ask the query for more items
queryItem(index - addedItemCount);
} else {
// item is already in our cache
// refresh cache access log.
itemCacheAccessLog.remove(new Integer(index));
itemCacheAccessLog.addLast(new Integer(index));
}
return itemCache.get(index - addedItemCount);
}
/**
* Query item and the surrounding batch of items.
*
* @param index
* The index of item requested to be queried.
*/
private void queryItem(final int index) {
final int batchSize = getBatchSize();
final int startIndex = index - index % batchSize;
final int count = Math.min(batchSize, getQuery().size() - startIndex);
final long queryStartTime = System.currentTimeMillis();
// load more items
final List<Item> items = getQuery().loadItems(startIndex, count);
final long queryEndTime = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
final int itemIndex = startIndex + i;
final Item item;
if (i >= items.size()) {
item = query.constructItem();
} else {
item = items.get(i);
}
itemCache.put(itemIndex, item);
if (i >= items.size()) {
removeItem(itemIndex);
}
if (itemCacheAccessLog.contains(itemIndex)) {
itemCacheAccessLog.remove((Object) itemIndex);
}
itemCacheAccessLog.addLast(itemIndex);
}
for (int i = 0; i < count; i++) {
final int itemIndex = startIndex + i;
final Item item = itemCache.get(itemIndex);
if (item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_INDEX) != null) {
item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_INDEX).setReadOnly(false);
item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_INDEX).setValue(batchCount);
item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_INDEX).setReadOnly(true);
}
if (item.getItemProperty(DEBUG_PROPERTY_ID_QUERY_INDEX) != null) {
item.getItemProperty(DEBUG_PROPERTY_ID_QUERY_INDEX).setReadOnly(false);
item.getItemProperty(DEBUG_PROPERTY_ID_QUERY_INDEX).setValue(queryCount);
item.getItemProperty(DEBUG_PROPERTY_ID_QUERY_INDEX).setReadOnly(true);
}
if (item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_QUERY_TIME) != null) {
item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_QUERY_TIME).setReadOnly(false);
item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_QUERY_TIME).setValue(queryEndTime - queryStartTime);
item.getItemProperty(DEBUG_PROPERTY_ID_BATCH_QUERY_TIME).setReadOnly(true);
}
for (final Object propertyId : item.getItemPropertyIds()) {
final Property property = item.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
final ValueChangeNotifier notifier = (ValueChangeNotifier) property;
notifier.addListener(this);
propertyItemMapCache.put(property, item);
}
}
}
// Increase batch count.
batchCount++;
// Evict items from cache if cache size exceeds max cache size
int counter = 0;
while (itemCache.size() > maxCacheSize) {
final int firstIndex = itemCacheAccessLog.getFirst();
final Item firstItem = itemCache.get(firstIndex);
// Remove oldest item in cache access log if it is not modified or
// removed.
if (!modifiedItems.contains(firstItem) && !removedItems.contains(firstItem)) {
itemCacheAccessLog.removeFirst();
itemCache.remove(firstIndex);
for (final Object propertyId : firstItem.getItemPropertyIds()) {
final Property property = firstItem.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
final ValueChangeNotifier notifier = (ValueChangeNotifier) property;
notifier.removeListener(this);
propertyItemMapCache.remove(property);
}
}
} else {
itemCacheAccessLog.removeFirst();
itemCacheAccessLog.addLast(firstIndex);
}
// Break from loop if entire cache has been iterated (all items are
// modified).
counter++;
if (counter > itemCache.size()) {
break;
}
}
}
/**
* Gets current query or constructs one on demand.
*
* @return The current query.
*/
private Query getQuery() {
if (query == null) {
query = queryFactory.constructQuery(sortPropertyIds, ascendingStates);
queryCount++;
}
return query;
}
/**
* Constructs and adds item to added items and returns index. Change can be committed or discarded with respective
* methods.
*
* @return index of the new item.
*/
@Override
public int addItem() {
final Item item = getQuery().constructItem();
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.Added);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
addedItems.add(0, item);
return 0;
}
/**
* Event handler for value change events. Adds the item to modified list if value was actually changed. Change can
* be committed or discarded with respective methods.
*
* @param event
* the ValueChangeEvent
*/
@Override
public void valueChange(final ValueChangeEvent event) {
final Property property = event.getProperty();
final Item item = propertyItemMapCache.get(property);
if (property == item.getItemProperty(PROPERTY_ID_ITEM_STATUS)) {
return;
}
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null
&& ((QueryItemStatus) item.getItemProperty(PROPERTY_ID_ITEM_STATUS).getValue()) != QueryItemStatus.Modified) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.Modified);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
if (!addedItems.contains(item) && !modifiedItems.contains(item)) {
modifiedItems.add(item);
}
}
/**
* Removes item at given index by adding it to the removed list. Change can be committed or discarded with
* respective methods.
*
* @param index
* of the item to be removed.
*/
@Override
public void removeItem(final int index) {
final Item item = getItem(index);
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.Removed);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
for (final Object propertyId : item.getItemPropertyIds()) {
final Property property = item.getItemProperty(propertyId);
property.setReadOnly(true);
}
removedItems.add(item);
}
/**
* Removes all items in the view. This method is immediately commited to the storage.
*/
@Override
public void removeAllItems() {
getQuery().deleteAllItems();
}
/**
* Checks whether view has been modified.
*
* @return True if view has been modified.
*/
@Override
public boolean isModified() {
return addedItems.size() != 0 || modifiedItems.size() != 0 || removedItems.size() != 0;
}
/**
* Commits changes in the view.
*/
@Override
public void commit() {
for (final Item item : addedItems) {
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.None);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
}
for (final Item item : modifiedItems) {
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.None);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
}
for (final Item item : removedItems) {
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.None);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
}
// Reverse added items so that they are saved in order of addition.
final List<Item> addedItemReversed = new ArrayList<Item>(addedItems);
Collections.reverse(addedItemReversed);
getQuery().saveItems(addedItemReversed, modifiedItems, removedItems);
addedItems.clear();
modifiedItems.clear();
removedItems.clear();
}
/**
* Discards changes in the view.
*/
@Override
public void discard() {
for (final Item item : addedItems) {
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.None);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
}
for (final Item item : modifiedItems) {
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.None);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
}
for (final Item item : removedItems) {
if (item.getItemProperty(PROPERTY_ID_ITEM_STATUS) != null) {
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(false);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setValue(QueryItemStatus.None);
item.getItemProperty(PROPERTY_ID_ITEM_STATUS).setReadOnly(true);
}
}
addedItems.clear();
modifiedItems.clear();
removedItems.clear();
}
/**
* {@inheritDoc}
*/
@Override
public List<Item> getAddedItems() {
return Collections.<Item> unmodifiableList(addedItems);
}
/**
* {@inheritDoc}
*/
@Override
public List<Item> getModifiedItems() {
return Collections.<Item> unmodifiableList(modifiedItems);
}
/**
* {@inheritDoc}
*/
@Override
public List<Item> getRemovedItems() {
return Collections.<Item> unmodifiableList(removedItems);
}
/**
* Used to set implementation property item cache map.
*
* @param propertyItemCacheMap
* the propertyItemMapCache to set
*/
public void setPropertyItemCacheMap(final Map<Property, Item> propertyItemCacheMap) {
this.propertyItemMapCache = propertyItemCacheMap;
}
}