/* * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ package com.codename1.cloud; import com.codename1.io.CacheMap; import com.codename1.io.Log; import com.codename1.io.Util; import com.codename1.ui.events.DataChangedListener; import com.codename1.ui.events.SelectionListener; import com.codename1.ui.list.ListModel; import com.codename1.ui.util.EventDispatcher; import java.util.Hashtable; import java.util.Vector; /** * Seamlessly creates a cloud based searchable list model * * @author Shai Almog * @deprecated the cloud storage API is no longer supported, we recommend switching to a solution such as parse4cn1 */ public class CloudListModel implements ListModel { private String type; private int visibilityScope; private int batchSize = 20; private int keyBatchSize = 1000; private int sortProperty; private boolean ascending; private Object[] keys; private EventDispatcher modelListener = new EventDispatcher(); private EventDispatcher selectionListener = new EventDispatcher(); private int selectedIndex = 0; private Hashtable loadingPlaceholder; private CacheMap cache; private int index; private String queryValue; /** * Refreshes items in the current view every 30 seconds when repainted */ private int refreshRateMillis = 30000; private long[] lastRefresh; /** * Creates a list model that shows all the cloud elements that have the given key matching * the given value at the visibility scope listed below. This model can be further narrowed * down by using the filter functionality below.<br> * This is effectively equivalent to issuing a queryEquals method, however it polls in batches * and caches data as needed. * * @param type the type of object shown on the list * @param visibilityScope the scope of the list (CloudObject.ACCESS_* values) * @param sortProperty the index by which we sort the entries, 0 for unsorted * @param ascending whether the sort is ascending or descending */ public CloudListModel(String type, int visibilityScope, int sortProperty, boolean ascending) { this.type = type; this.visibilityScope = visibilityScope; this.sortProperty = sortProperty; this.ascending = ascending; init(); } private void init() { cache = new CacheMap(type + visibilityScope + sortProperty); cache.setCacheSize(30); cache.setStorageCacheSize(100); cache.setAlwaysStore(true); loadingPlaceholder = new Hashtable(); loadingPlaceholder.put("Line1", "Loading..."); loadingPlaceholder.put("Placeholder", Boolean.TRUE); // remove loading placeholders that might have gotten stuck in cache Vector v = cache.getKeysInCache(); int cacheSize = v.size(); for(int iter = 0 ; iter < cacheSize ; iter++) { Object k = v.elementAt(iter); Object e = cache.get(k); if(e instanceof Hashtable) { Hashtable h = (Hashtable)e; if(h.containsKey("Placeholder")) { cache.delete(k); } } } } /** * Creates a list model that shows all the cloud elements that have the given key matching * the given value at the visibility scope listed below. This model can be further narrowed * down by using the filter functionality below.<br> * This is effectively equivalent to issuing a queryEquals method, however it polls in batches * and caches data as needed. * * @param type the type of object shown on the list * @param visibilityScope the scope of the list (CloudObject.ACCESS_* values) * @param index the index by which we limit the entries * @param queryValue the queryValue for the given index * @param ascending whether the sort is ascending or descending */ public CloudListModel(String type, int visibilityScope, int index, String queryValue, boolean ascending) { this.type = type; this.visibilityScope = visibilityScope; this.index = index; this.queryValue = queryValue; this.ascending = ascending; init(); } /** * Refreshes the list from the server, this method blocks the EDT until * completion. */ public void refresh() { Vector vec = cache.getKeysInCache(); int s = vec.size(); Vector cld = new Vector(); for(int iter = 0 ; iter < s ; iter++) { Object key = vec.elementAt(iter); Object val = cache.get(key); if(val != null && val instanceof CloudObject) { cld.addElement((CloudObject)val); } } if(cld.size() > 0) { CloudObject[] obj = new CloudObject[cld.size()]; cld.toArray(obj); int response = CloudStorage.getInstance().refresh(obj); if(response != CloudStorage.RETURN_CODE_SUCCESS) { onError(new CloudException(response)); } else { // persist the object to the cache and fire the list model change for(int iter = 0 ; iter < obj.length ; iter++) { cache.put(obj[iter].getCloudId(), obj[iter]); } modelListener.fireDataChangeEvent(0, getSize()); } } } /** * Flushes the cache which might be essential when adding new elements */ public void clearCache() { cache.clearAllCache(); } /** * Sets the size of the local cache * @param elements elements to store in cache */ public void setCacheSize(int elements) { cache.setCacheSize(elements); } /** * Returns the elements cached within the implementation * @return the cache size */ public int getCacheSize() { return cache.getCacheSize(); } /** * Sets the size of the cache in persistent storage * @param elements elements to store in cache */ public void setStorageCacheSize(int elements) { cache.setStorageCacheSize(elements); } /** * Returns the elements cached within the persistent storage * @return the cache size */ public int getStorageCacheSize() { return cache.getCacheSize(); } /** * When loading the element from the cloud a placehold is shown to indicate to the user that the content isn't fully * here * @param loadingPlaceholder shows blank content to the user */ public void setLoadingPlaceholder(Hashtable loadingPlaceholder) { this.loadingPlaceholder = loadingPlaceholder; this.loadingPlaceholder.put("Placeholder", Boolean.TRUE); } /** * When loading the element from the cloud a placehold is shown to indicate to the user that the content isn't fully * here * * @return the element shown when content isn't available for a specific entry */ public Hashtable getLoadingPlaceholder() { return loadingPlaceholder; } /** * Returns the batch size fetched from the server * @return the size */ public int getBatchSize() { return batchSize; } /** * Updates the number of elements fetched from the server in a single batch * @param batchSize the batch size */ public void setBatchSize(int batchSize) { this.batchSize = batchSize; } /** * {@inheritDoc} */ public Object getItemAt(int index) { if(keys != null && index < keys.length && index > -1){ Object value = cache.get(keys[index]); if(value == null) { value = getLoadingPlaceholder(); fillUpList(index); } else { if(value instanceof CloudObject) { long time = System.currentTimeMillis(); if(lastRefresh[index] + refreshRateMillis < time) { CloudObject cld = (CloudObject)value; if(cld.getLastModified() > lastRefresh[index]) { lastRefresh[index] = cld.getLastModified(); } else { if(cld.getStatus() == CloudObject.STATUS_COMMITTED) { CloudStorage.getInstance().refreshAsync(cld); } } } } } return value; } return null; } private void fillUpList(final int startIndex) { final int len = Math.min(batchSize, keys.length - startIndex); Vector<String> request = new Vector<String>(); for(int iter = startIndex ; iter < startIndex + len ; iter++) { if(cache.get(keys[iter]) == null) { cache.put(keys[iter], loadingPlaceholder); request.addElement((String)keys[iter]); } } CloudResponse<CloudObject[]> resp = new CloudResponse<CloudObject[]>() { public void onSuccess(CloudObject[] returnValue) { for(int iter = 0 ; iter < returnValue.length ; iter++) { cache.put(returnValue[iter].getCloudId(), returnValue[iter]); } modelListener.fireDataChangeEvent(startIndex, len); } public void onError(CloudException err) { CloudListModel.this.onError(err); } }; String[] arr = new String[request.size()]; request.toArray(arr); CloudStorage.getInstance().fetch(arr, resp); } private void newRefreshRate() { lastRefresh = new long[keys.length]; long t = System.currentTimeMillis(); for(int iter = 0 ; iter < lastRefresh.length ; iter++) { lastRefresh[iter] = t; } } /** * {@inheritDoc} */ public int getSize() { if(keys == null) { keys = (Object[])cache.get("keyIndex"); if(keys == null) { keys = new Object[0]; } else { newRefreshRate(); } // refresh the key list even if we have them in cache since this might have changed CloudResponse<String[]> resp = new CloudResponse<String[]>() { private int responseOffset; public void onSuccess(String[] returnValue) { if(responseOffset == 0) { keys = returnValue; } else { String[] k = new String[keys.length + returnValue.length]; Util.mergeArrays(keys, returnValue, k); keys = k; } newRefreshRate(); cache.put("keyIndex", keys); modelListener.fireDataChangeEvent(-1, DataChangedListener.ADDED); // we might have more data, send another request if(returnValue.length == keyBatchSize) { responseOffset = keys.length; if(index > 0) { CloudStorage.getInstance().queryEqualsKeys(type, index, queryValue, keys.length, keyBatchSize, visibilityScope, this); } else { CloudStorage.getInstance().querySortedKeys(type, sortProperty, ascending, keys.length, keyBatchSize, visibilityScope, this); } } } public void onError(CloudException err) { CloudListModel.this.onError(err); } }; if(index > 0) { CloudStorage.getInstance().queryEqualsKeys(type, index, queryValue, 0, keyBatchSize, visibilityScope, resp); } else { CloudStorage.getInstance().querySortedKeys(type, sortProperty, ascending, 0, keyBatchSize, visibilityScope, resp); } } return keys.length; } /** * Invoked when a cloud error occurs * @param err the exception representing the error in cloud communications */ protected void onError(CloudException err) { Log.e(err); } /** * {@inheritDoc} */ public int getSelectedIndex() { return selectedIndex; } /** * {@inheritDoc} */ public void setSelectedIndex(int index) { int oldIndex = selectedIndex; this.selectedIndex = index; selectionListener.fireSelectionEvent(oldIndex, selectedIndex); } /** * {@inheritDoc} */ public void addDataChangedListener(DataChangedListener l) { modelListener.addListener(l); } /** * {@inheritDoc} */ public void removeDataChangedListener(DataChangedListener l) { modelListener.removeListener(l); } /** * {@inheritDoc} */ public void addSelectionListener(SelectionListener l) { selectionListener.addListener(l); } /** * {@inheritDoc} */ public void removeSelectionListener(SelectionListener l) { selectionListener.removeListener(l); } /** * <b>Notice</b> this method does NOT commit the data, after committing the data * the cache MUST be cleared! * {@inheritDoc} */ public void addItem(Object item) { CloudObject cld = (CloudObject)item; if(cld.getType() == null) { cld.setType(type); } CloudStorage.getInstance().save(cld); } /** * <b>Notice</b> this method does NOT commit the data, after committing the data * the cache MUST be cleared! * {@inheritDoc} */ public void removeItem(int index) { Object o = getItemAt(index); if(o instanceof CloudObject) { CloudObject cld = (CloudObject)o; CloudStorage.getInstance().delete(cld); } } /** * Indicates the rate in milliseconds in which to poll the server for the current data * of elements that are visible at the moment. * @return the refreshRateMillis */ public int getRefreshRateMillis() { return refreshRateMillis; } /** * Indicates the rate in milliseconds in which to poll the server for the current data * of elements that are visible at the moment. * @param refreshRateMillis the refreshRateMillis to set */ public void setRefreshRateMillis(int refreshRateMillis) { this.refreshRateMillis = refreshRateMillis; } }