ch.gbrain.gwtstorage.manager.StorageManager.java Source code

Java tutorial

Introduction

Here is the source code for ch.gbrain.gwtstorage.manager.StorageManager.java

Source

package ch.gbrain.gwtstorage.manager;

/*
 * #%L
 * GwtStorage
 * %%
 * Copyright (C) 2016 gbrain.ch
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.fusesource.restygwt.client.JsonCallback;
import org.fusesource.restygwt.client.Method;
import org.fusesource.restygwt.client.Resource;

import ch.gbrain.gwtstorage.model.StorageInfo;
import ch.gbrain.gwtstorage.model.StorageItem;
import ch.gbrain.gwtstorage.model.StorageResource;

import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.i18n.shared.DateTimeFormat;
import com.google.gwt.i18n.shared.DateTimeFormat.PredefinedFormat;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.storage.client.Storage;
import com.googlecode.gwtphonegap.client.PhoneGap;
import com.googlecode.gwtphonegap.client.connection.Connection;
import com.googlecode.gwtphonegap.client.file.DirectoryEntry;
import com.googlecode.gwtphonegap.client.file.EntryBase;
import com.googlecode.gwtphonegap.client.file.FileCallback;
import com.googlecode.gwtphonegap.client.file.FileDownloadCallback;
import com.googlecode.gwtphonegap.client.file.FileEntry;
import com.googlecode.gwtphonegap.client.file.FileError;
import com.googlecode.gwtphonegap.client.file.FileReader;
import com.googlecode.gwtphonegap.client.file.FileSystem;
import com.googlecode.gwtphonegap.client.file.FileWriter;
import com.googlecode.gwtphonegap.client.file.Flags;
import com.googlecode.gwtphonegap.client.file.ReaderCallback;
import com.googlecode.gwtphonegap.client.file.WriterCallback;

/**
 * This class deals about writing and reading objects from Type StorageItem and
 * as well loading application resource files (eg. video files) from the
 * applications home base (server base). This is useful when running the App in
 * the Web but as well in a Phonegap container as we could store the files
 * locally (cache) when running in the phonegap container.
 */
public class StorageManager {

    /**
     * The remote base url of the application
     */
    private String remoteAppBaseUrl = "http://www.exmaple.com/myapp/";

    public String getRemoteAppBaseUrl() {
        return remoteAppBaseUrl;
    }

    public void setRemoteAppBaseUrl(String baseUrl) {
        this.remoteAppBaseUrl = baseUrl;
        logger.log(Level.INFO, "SetRemoteAppBaseUrl:" + baseUrl);
    }

    /**
     * The default storage url in the application. This is the relative location
     * from the applications base directory.
     */
    private String storageUrl = "storage/v1/";

    public String getStorageUrl() {
        return storageUrl;
    }

    private String getLocalStorageUrl() {
        return storageUrl;
    }

    private String getRemoteStorageUrl() {
        return getRemoteAppBaseUrl() + storageUrl;
    }

    public void setStorageUrl(String storageUrl) {
        this.storageUrl = storageUrl;
        logger.log(Level.INFO, "SetStorageUrl:" + storageUrl);
    }

    /**
     * The directory we are going to create locally on the mobile device as cache
     * directory when running in the phonegap container.
     */
    private String cacheDirectory = "myApp";

    public String getCacheDirectory() {
        return cacheDirectory;
    }

    public void setCacheDirectory(String cacheDirectory) {
        if (this.cacheDirectory.equals(cacheDirectory))
            return; // nothing to do it
                    // is the same as
                    // before
        this.cacheDirectory = cacheDirectory;
        this.cacheDirectoryEntry = null;
        logger.log(Level.INFO, "SetCacheDirectory:" + cacheDirectory);
    }

    /**
     * Needs to be enabled to perform any caching at all.
     */
    private boolean cacheEnabled = true;

    public void setCacheEnabled(boolean cacheEnabled) {
        this.cacheEnabled = cacheEnabled;
    }

    public boolean getCacheEnabled() {
        return this.cacheEnabled;
    }

    /**
     * If enabled, the download of the big resource files for local caching (eg.
     * videos) is only invoked if we are in a wlan network connected. Else it is
     * taken from backend url always by need.
     */
    private boolean wlanEnabled = true;

    public void setWlanEnabled(boolean wlanEnabled) {
        this.wlanEnabled = wlanEnabled;
    }

    public boolean getWlanEnabled() {
        return this.wlanEnabled;
    }

    private Boolean lastCachingState = null;

    private void logResourceCachingState(boolean state, String msg) {
        if (lastCachingState == null || lastCachingState != state) {
            logger.log(Level.INFO, msg);
        }
        lastCachingState = state;
    }

    /**
     * Evaluates if resource caching is currently enabled at all.
     * 
     * @return true if resouces shall be downloaded actually.
     */
    private boolean isResourceCachingEnabled() {
        if (!phonegap.isPhoneGapDevice())
            return false;
        if (!this.getCacheEnabled()) {
            logResourceCachingState(false, "ResourceCaching is disabled");
            return false;
        }
        if (this.getWlanEnabled()) {
            // check connection state
            if (phonegap.isPhoneGapDevice()
                    && phonegap.getConnection().getType().equalsIgnoreCase(Connection.WIFI)) {
                logResourceCachingState(true, "Wlan requested and available, ResourceCaching enabled");
                return true;
            }
            logResourceCachingState(false, "Wlan requested but not available, ResourceCaching disabled");
            return false;
        }
        logResourceCachingState(true, "ResourceCaching enabled");
        return true;
    }

    /**
     * Evaluates if Caching is possible at all on the given client device at the
     * moment of evaluation.
     * 
     * @return True if we have access to the local file system and therefore are able to provide a local resource caching.
     */
    public boolean isResourceCachingPossible() {
        if (!phonegap.isPhoneGapDevice())
            return false;
        return true;
    }

    private PhoneGap phonegap;

    public PhoneGap getPhonegap() {
        return phonegap;
    }

    private Logger logger;

    public Logger getLogger() {
        return logger;
    }

    private Storage localStorage = null;

    /**
     * Default constructor setting up the StorageManager all inclusive Logger and
     * Phonegap references are setup locally
     */
    public StorageManager() {
        this(null, null);
    }

    /**
     * Constructor allowing to inject the Phonegap reference and a Logger
     * 
     * @param phonegap Give the phonegap reference you have already. Give null if
     *          you want it to be treated here automatically.
     * @param logger Give a logger reference or null if you want it to be treated
     *          here locally.
     */
    public StorageManager(PhoneGap phonegap, Logger logger) {
        if (phonegap != null) {
            this.phonegap = phonegap;
        } else {
            this.phonegap = GWT.create(PhoneGap.class);
        }
        if (logger != null) {
            this.logger = logger;
        } else {
            this.logger = Logger.getLogger("StorageManager");
        }
        getLocalStorage();
        this.getFileSystem(null);
    }

    /**
     * Retrieve a reference to the local Storage (Browsers HTML5 key-value store)
     * 
     * @return The local storage or null if not supported
     */
    public Storage getLocalStorage() {
        if (localStorage == null) {
            localStorage = Storage.getLocalStorageIfSupported();
            if (localStorage == null) {
                logger.log(Level.SEVERE, "No LocalStorage available!!!!!!!!!!!!!");
            }
        }
        return localStorage;
    }

    /****************************************************************************************************************
     * Read / Write StorageItem to private local HTML5 storage
     ****************************************************************************************************************/

    /**
     * Write the given item to the local HTML5 storage.
     * 
     * @param item The object to be serialized and stored under the ID within the
     *          local storage
     * @return true if the write operation succeeded
     */
    public boolean writeStorageItemToLocalStorage(StorageItem item) {
        if (item == null)
            return false;
        try {
            JSONValue json = item.toJson();
            getLocalStorage().setItem(item.getStorageItemIdKey(), json.toString());
            writeStorageItemStorageTimeToLocalStorage(item);
            logger.log(Level.INFO, "Local StorageItem written" + item.getLogId());
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Failure local write" + item.getLogId(), ex);
        }
        return false;
    }

    /**
     * Read the item from the local html5 storage.
     * 
     * @param item The item to be read from the local storage with the given ID as
     *          key.
     * @return false if the read operation failed or nothing is found.
     */
    public boolean readStorageItemFromLocalStorage(StorageItem item) {
        return readStorageItemFromLocalStorage(item, 0, 0);
    }

    /**
     * Read the item from the local html5 storage and take the cacheTime in
     * account
     * 
     * @param item
     * @param expectedVersion The minimum item version, not checked if <=0
     * @param cacheTime If the item was stored longer than the cacheTime, it isn't
     *          accepted, not checked if <=0
     * @return false if the read operation failed or nothing is found, the version
     *         wasn't ok or the cacheTime elapsed already
     */
    private boolean readStorageItemFromLocalStorage(StorageItem item, int expectedVersion, int cacheTime) {
        if (item == null)
            return false;
        try {
            String val = getLocalStorage().getItem(item.getStorageItemIdKey());
            if (val != null) {
                logger.log(Level.INFO, "Local StorageItem found" + item.getLogId());
                item.fromJson(val);
                // check if the version is ok
                if (expectedVersion > 0) {
                    if (!checkStorageItemVersion(expectedVersion, item.getVersion())) {
                        logger.log(Level.INFO, "Local StorageItem version mismatch" + item.getLogId());
                        return false;
                    }
                }
                // check if cache is valid
                if (cacheTime > 0) {
                    Date storeTime = readStorageItemStorageTimeFromLocalStorage(item);
                    if (storeTime != null) { // there was a time available, so compare it
                        Date nowTime = new Date();
                        if (nowTime.getTime() - (cacheTime * 1000) > storeTime.getTime()) { // elapsed
                            logger.log(Level.INFO, "Local StorageItem time elapsed" + item.getLogId());
                            return false;
                        }
                    }
                }
                logger.log(Level.INFO, "Local readStorageItem complete" + item.getLogId());
                return true;
            } else {
                logger.log(Level.INFO, "Local readStorageItem not found" + item.getLogId());
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception local readStorageItem" + item.getLogId(), ex);
        }
        return false;
    }

    /**
     * Write the items Date/Time store value to html5 storage for later usage in
     * relation to the cache time
     * 
     * @param item The storage time for the given item (ID) is written to the
     *          key-value HTML5 storage.
     * @return false if the read operation failed or nothing is found.
     */
    private void writeStorageItemStorageTimeToLocalStorage(StorageItem item) {
        if (item == null || item.getStorageItemTimeKey() == null)
            return;
        try {
            String saveTime = DateTimeFormat.getFormat(PredefinedFormat.DATE_TIME_FULL).format(new Date());
            getLocalStorage().setItem(item.getStorageItemTimeKey(), saveTime);
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception local writeStorageItem time" + item.getLogId(), ex);
        }
    }

    /**
     * Read the items Date/Time store value from html5 storage.
     * 
     * @param item The time when a certain StorageItem was written to the HTML5
     *          key-value storage is read.
     * @return null if the read operation failed or nothing is found.
     */
    private Date readStorageItemStorageTimeFromLocalStorage(StorageItem item) {
        if (item == null || item.getStorageItemTimeKey() == null)
            return null;
        try {
            String val = getLocalStorage().getItem(item.getStorageItemTimeKey());
            if (val != null) {
                return DateTimeFormat.getFormat(PredefinedFormat.DATE_TIME_FULL).parse(val);
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception local readStorageItem time" + item.getLogId(), ex);
        }
        return null;
    }

    /**
     * Remove all StorageItem related keys from the LocalStorage, thus clear the
     * cached json objects and references etc.
     */
    public void clearStorageItems() {
        try {
            Storage storage = this.getLocalStorage();
            if (storage == null)
                return;
            Integer len = storage.getLength();
            int index = 0;
            for (int i = 0; i < len; i++) {
                String key = storage.key(index);
                if (StorageItem.isStorageItemKey(key)) {
                    logger.log(Level.INFO, "Remove cached StorageItem:" + key);
                    storage.removeItem(key);
                } else {
                    index++;
                }
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Execption clearing StorageItems", ex);
        }
    }

    /****************************************************************************************************************
     * Read / Write StorageItems to local Files
     ****************************************************************************************************************/

    /**
     * Write the item to a local file
     * 
     * @param item The StorageItem to be stored to the file system within the
     *          defined cache directory
     * @param callback Is called once the asynch action completed or failed.
     * @return false if the asynchronous action invocation failed.
     */
    public boolean writeStorageItemToLocalFile(final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        if (item == null)
            return false;
        try {
            logger.log(Level.INFO, "local writeStorageItem invoked " + item.toString());
            return getLocalFileReference(getCacheDirectory(), item.getJsonFileName(), true,
                    new FileCallback<FileEntry, StorageError>() {
                        @Override
                        public void onSuccess(FileEntry entry) {
                            logger.log(Level.INFO,
                                    "local writeStorageItem FileEntry successfully retrieved" + item.getLogId());
                            // store the file content
                            writeStorageItemToLocalFile(entry, item, callback);
                        }

                        @Override
                        public void onFailure(StorageError error) {
                            logger.log(Level.SEVERE, "Failure local writeStorageItem FileSystem creation"
                                    + item.getLogId() + " " + error.toString());
                        }
                    });
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception local writeStorageItem " + item.getLogId(), ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /**
     * Write the item to the local fileentry asynchronous
     * 
     * @param fileEntry
     * @param item
     * @param callback Is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    private boolean writeStorageItemToLocalFile(FileEntry fileEntry, final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        if (item == null)
            return false;
        try {
            logger.log(Level.INFO, "writeStorageItem to local file invoked" + item.getLogId());
            fileEntry.createWriter(new FileCallback<FileWriter, FileError>() {
                @Override
                public void onSuccess(FileWriter writer) {
                    writer.setOnWriteEndCallback(new WriterCallback<FileWriter>() {
                        @Override
                        public void onCallback(FileWriter result) {
                            // file written
                            logger.log(Level.INFO, "writeToLocalFile successfully written" + item.getLogId());
                            if (callback != null) {
                                callback.onSuccess(item);
                            }
                        }
                    });
                    writer.setOnErrorCallback(new WriterCallback<FileWriter>() {
                        @Override
                        public void onCallback(FileWriter result) {
                            // Error while writing file
                            logger.log(Level.SEVERE,
                                    "Failure file write StorageItem" + item.getLogId() + " : " + result.toString());
                            if (callback != null) {
                                callback.onFailure(new StorageError(result.getError()));
                            }
                        }
                    });
                    JSONValue json = item.toJson();
                    writer.write(json.toString());
                }

                @Override
                public void onFailure(FileError error) {
                    // can not create writer
                    logger.log(Level.SEVERE, "Failure file writer creation StorageItem" + item.getLogId() + " : "
                            + error.toString());
                    if (callback != null) {
                        callback.onFailure(new StorageError(error));
                    }
                }
            });
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception file write StorageItem" + item.toString(), ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /**
     * Read the item from the Local File storage
     * 
     * @param item The StorageItem (or inherited objects) to be read from the
     *          local cache file system location.
     * @param callback Is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    public boolean readStorageItemFromLocalFile(final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        if (item == null)
            return false;
        try {
            // get the file reference
            return getLocalFileReference(getCacheDirectory(), item.getJsonFileName(), false,
                    new FileCallback<FileEntry, StorageError>() {
                        @Override
                        public void onSuccess(FileEntry entry) {
                            logger.log(Level.INFO, "StorageItem File successfully retrieved" + item.getLogId());
                            readStorageItemFromLocalFile(entry, item, callback);
                        }

                        @Override
                        public void onFailure(StorageError error) {
                            logger.log(Level.SEVERE, "Failure LocalFileReference retrieval" + item.getLogId()
                                    + " : " + error.toString());
                            if (callback != null) {
                                callback.onFailure(error);
                            }
                        }
                    });
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception file write StorageItem" + item.getLogId(), ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /**
     * Read the StorageItem from the given FileEntry and refresh the given
     * CommonView
     * 
     * @param fileEntry
     * @param item
     * @param callback Is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    private boolean readStorageItemFromLocalFile(FileEntry fileEntry, final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        if (item == null)
            return false;
        try {
            // logger.log(Level.INFO,"readStorageItem from local file invoked" +
            // item.getLogId());
            FileReader reader = phonegap.getFile().createReader();
            reader.setOnloadCallback(new ReaderCallback<FileReader>() {
                @Override
                public void onCallback(FileReader result) {
                    String json = result.getResult();
                    // do something with the content
                    item.fromJson(json);
                    logger.log(Level.INFO,
                            "readStorageItem from local file load completed for item" + item.getLogId());
                    if (callback != null) {
                        callback.onSuccess(item);
                    }
                }
            });
            reader.setOnErrorCallback(new ReaderCallback<FileReader>() {
                @Override
                public void onCallback(FileReader result) {
                    // error while reading file...
                    logger.log(Level.SEVERE,
                            "Error StorageItem file writer reading" + item.getLogId() + " : " + result.toString());
                    if (callback != null) {
                        callback.onFailure(new StorageError(result.getError()));
                    }
                }
            });
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception file read StorageItem" + item.getLogId(), ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /**
     * Our reference to the file system
     */
    private FileSystem fileSystem = null;

    /**
     * Retrieve the FileEntry Reference on the local filesystem of the device if
     * running in a local container eg. Phonegap
     * 
     * @param directory
     * @param filename
     * @param callback is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    private boolean getFileSystem(final FileCallback<FileSystem, StorageError> callback) {
        try {
            if (!phonegap.isPhoneGapDevice())
                return false;
            if (fileSystem != null) {
                if (callback != null) {
                    callback.onSuccess(fileSystem);
                }
                return true;
            }
            logger.log(Level.INFO, "getFileReference - Request Local File System");
            phonegap.getFile().requestFileSystem(FileSystem.LocalFileSystem_PERSISTENT, 0,
                    new FileCallback<FileSystem, FileError>() {
                        @Override
                        public void onSuccess(FileSystem entry) {
                            logger.log(Level.INFO, "FileSystem retrieved");
                            fileSystem = entry;
                            if (callback != null) {
                                callback.onSuccess(fileSystem);
                            }
                        }

                        @Override
                        public void onFailure(FileError error) {
                            logger.log(Level.SEVERE, "Failure filesystem retrieval " + error.toString());
                            if (callback != null) {
                                callback.onFailure(new StorageError(error));
                            }
                        }
                    });
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "General failure FileSystem retrieval", ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /**
     * Retrieve the FileEntry Reference on the local filesystem of the device if
     * running in a local container eg. Phonegap
     * 
     * @param directory
     * @param filename
     * @param callback is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    public boolean getLocalFileReference(final String directory, final String filename, final boolean create,
            final FileCallback<FileEntry, StorageError> callback) {
        try {
            if (!phonegap.isPhoneGapDevice())
                return false;
            getLocalDirectoryEntry(directory, new FileCallback<DirectoryEntry, StorageError>() {
                @Override
                public void onSuccess(DirectoryEntry directoryEntry) {
                    directoryEntry.getFile(filename, new Flags(create, false),
                            new FileCallback<FileEntry, FileError>() {
                                @Override
                                public void onSuccess(FileEntry entry) {
                                    logger.log(Level.INFO, "getLocalFileReference - File retrieved : " + filename);
                                    if (callback != null) {
                                        callback.onSuccess(entry);
                                    }
                                }

                                @Override
                                public void onFailure(FileError error) {
                                    logger.log(Level.SEVERE,
                                            "Failure file retrieval " + filename + " " + error.toString());
                                    if (callback != null) {
                                        callback.onFailure(new StorageError(error));
                                    }
                                }
                            });
                }

                @Override
                public void onFailure(StorageError error) {
                    logger.log(Level.SEVERE, "Failure filesystem retrieval " + error.toString());
                    callback.onFailure(error);
                }
            });
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "General failure directory/file creator", ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    private String lastLocalDirectory = null;
    private DirectoryEntry lastLocalDirectoryEntry = null;

    /**
     * Retrieve the FileEntry Reference on the local filesystem of the device if
     * running in a local container eg. Phonegap
     * 
     * @param directory The directory which we want to get a reference for. It will be created if it doesn't exist yet. 
     *                  It is based on the Filesystem reference.
     * @param callback is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    private boolean getLocalDirectoryEntry(final String directory,
            final FileCallback<DirectoryEntry, StorageError> callback) {
        try {
            if (!phonegap.isPhoneGapDevice())
                return false;
            if (lastLocalDirectory != null && lastLocalDirectoryEntry != null) {
                if (directory.equals(lastLocalDirectory)) {
                    if (callback != null) {
                        callback.onSuccess(lastLocalDirectoryEntry);
                    }
                    return true;
                }
            }
            getFileSystem(new FileCallback<FileSystem, StorageError>() {
                @Override
                public void onSuccess(FileSystem entry) {
                    logger.log(Level.INFO, "getLocalDirectoryEntry - FileSystem retrieved");
                    final DirectoryEntry root = entry.getRoot();
                    root.getDirectory(directory, new Flags(true, false),
                            new FileCallback<DirectoryEntry, FileError>() {
                                @Override
                                public void onSuccess(final DirectoryEntry dirEntry) {
                                    logger.log(Level.INFO,
                                            "getLocalDirectoryEntry - Directory retrieved : " + directory);
                                    lastLocalDirectory = directory;
                                    lastLocalDirectoryEntry = dirEntry;
                                    if (callback != null) {
                                        callback.onSuccess(dirEntry);
                                    }
                                }

                                @Override
                                public void onFailure(FileError error) {
                                    logger.log(Level.SEVERE, "Failure directory retrieval " + directory + " : "
                                            + error.toString() + " : " + error.getErrorCode());
                                    if (callback != null) {
                                        callback.onFailure(new StorageError(error));
                                    }
                                }
                            });
                }

                @Override
                public void onFailure(StorageError error) {
                    logger.log(Level.SEVERE,
                            "Failure filesystem retrieval " + error.toString() + " : " + error.getErrorCode());
                    if (callback != null) {
                        callback.onFailure(error);
                    }
                }
            });
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception in getLocalDirectory : " + directory, ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /****************************************************************************************************************
     * Read StorageItem from URL path
     ****************************************************************************************************************/

    /**
     * Retrieve the item from the storage. First try local storage, if the version
     * and the validTime are valid, it is returned. Else it tries to retrieve it
     * from the remote backend. If found, it is cached locally for fast access.
     * 
     * @param item The item to be read by ID from 1. the cache, 2. localAppPath 3.
     *          remoteAppPath in this priority order
     * @param useCache If true, the system will first try to retrieve the value
     *          from the local cache before it reads the same from the
     *          applications path
     * @param validTime The maximum age in seconds of the cache to be accepted as
     *          a valid item value, if elapsed it will try to read from the
     *          applications path / If <= 0 don't care
     * @param expectedVersion The versionNumber which must be available in the
     *          cache to be a valid cache item. If <=0 don't care.
     */
    public boolean readStorageItem(final StorageItem item, boolean useCache, int expectedVersion, int validTime,
            final Callback<StorageItem, StorageError> callback) {
        try {
            logger.log(Level.INFO, "readStorageItem" + item.getLogId());
            if (useCache && this.getCacheEnabled()) { // retrieve the item first from local storage cache
                if (this.readStorageItemFromLocalStorage(item, expectedVersion, validTime)) { // found it valid in the cache
                    callback.onSuccess(item);
                    return true;
                }
            }
            // didn't found a matching item in the cache yet or version mismatch or
            // cache time elapsed
            if (phonegap.isPhoneGapDevice()) {
                // we run in a locally installed app and want to retrieve now the value
                // from the given backend
                return this.readStorageItemFromRemoteApplication(item, callback);
            } else { // in the case of web app, load it from the applications relative base path
                     // this is automatically from the backend server where the app was
                     // loaded from
                return this.readStorageItemFromLocalApplication(item, callback);
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception readStorageItem" + item.getLogId(), ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /**
     * Read the item asynch from the applications own source server with the given
     * credentials and base path eg.
     * /storage/v1/ch.gbrain.testapp.model.items-1.json Note: We just give the
     * local relative url which means: - If running as local application (eg.
     * started locally in Browser) it will load from local base - If running as
     * web application it will load from the servers base - If running as Phonegap
     * app it will load from the local installed base
     * 
     * @param item
     * @param callback is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    public boolean readStorageItemFromLocalApplication(final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        // we run in the web directly and therefore we read it directly from the
        // application relative storage in the Webapp itself
        logger.log(Level.INFO, "Read StorageItem from local applications base" + item.getLogId());
        return readStorageItemFromUrl(this.getLocalStorageUrl(), item,
                getReadStorageItemHandler(item, callback, null));
        // for testing in browser use this. But Chrome must run without security to
        // work
        // return readFromUrl(this.appRemoteStorageUrl,item,callback);
    }

    /**
     * Compare the given versions.
     * 
     * @param expectedVersion The version we do expect at least / if <=0, we don't
     *          care about versions, it is always ok
     * @param realVersion The real version of the item
     * @return true if the realversion >= expectedVersion
     */
    private boolean checkStorageItemVersion(int expectedVersion, int realVersion) {
        if (expectedVersion <= 0)
            return true; // the version doesn't care
        if (realVersion >= expectedVersion)
            return true;
        return false;
    }

    /**
     * Read the item asynch from the configured remote application base
     * 
     * @param item
     * @param callback is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed.
     */
    public boolean readStorageItemFromRemoteApplication(final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        logger.log(Level.INFO, "Read StorageItem from remote application base" + item.getLogId());
        return readStorageItemFromUrl(this.getRemoteStorageUrl(), item,
                getReadStorageItemHandler(item, callback, this.getLocalStorageUrl()));
    }

    /**
     * Creates and returns a Callback which treats the result for a url Item
     * retrieval
     * 
     * @param item The StorageItem (or a inheriting object) which must be read
     *          (filled in with the retrieved data)
     * @param callback The final resp. initial callback to be notified of the
     *          result
     * @param fallBack A URL to which a further request must be done if the call
     *          fails
     * @return The callback which deals with the asynch result of the remote item
     *         retrieval
     */
    private Callback<StorageItem, StorageError> getReadStorageItemHandler(final StorageItem item,
            final Callback<StorageItem, StorageError> callback, final String fallbackUrl) {
        return new Callback<StorageItem, StorageError>() {
            public void onSuccess(StorageItem newItem) { // loading succeeded
                                                         // store it in the cache
                logger.log(Level.INFO, "Completed read item from url" + item.getLogId());
                writeStorageItemToLocalStorage(newItem);
                callback.onSuccess(newItem);
            }

            public void onFailure(StorageError error) {
                logger.log(Level.WARNING, "Failure url loading" + item.getLogId());
                // nothing found, check if we must retrieve it from a remote location
                if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
                    readStorageItemFromUrl(fallbackUrl, item, getReadStorageItemHandler(item, callback, null));
                } else {
                    callback.onFailure(error);
                }
            }
        };
    }

    /**
     * Read the JSON item asynch from the given url and the
     * standard name of the item eg. ch.gbrain.testapp.model.items-1.json
     * 
     * @param url The url to read from eg. for local application relative path
     *          "storage/v1/" eg. for remote location
     *          "http://host.domain.ch/testapp/storage/v1/"
     * @param item
     * @param callback is called once the asynch action completed or failed
     * @return false if the asynchronous action invocation failed and no callback will be invoked
     */
    public boolean readStorageItemFromUrl(String url, final StorageItem item,
            final Callback<StorageItem, StorageError> callback) {
        if (item == null)
            return false;
        try {
            Resource resource = new Resource(url + item.getJsonFileName() + "?noCache=" + new Date().getTime());
            Method method = resource.get();
            /**
             * if (username.isEmpty()) { method = resource.get(); }else { method =
             * resource.get().user(username).password(password); }
             */
            logger.log(Level.INFO, "Read from url:" + method.builder.getUrl());
            method.send(new JsonCallback() {
                public void onSuccess(Method method, JSONValue response) {
                    logger.log(Level.INFO, "Read from url success");
                    if (response != null) {
                        try {
                            logger.log(Level.INFO, "Successfully url read" + item.getLogId());
                            item.fromJson(response);
                            if (callback != null) {
                                callback.onSuccess(item);
                            }
                        } catch (Exception ex) {
                            logger.log(Level.SEVERE, "Failure url read" + item.getLogId(), ex);
                        }
                    }
                }

                public void onFailure(Method method, Throwable exception) {
                    logger.log(Level.WARNING, "Failure url read" + item.getLogId(), exception);
                    if (callback != null) {
                        callback.onFailure(new StorageError(FileError.NOT_READABLE_ERR, exception.getMessage()));
                    }
                }
            });
            logger.log(Level.INFO, "Read from url call complete");
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Error url read" + item.getLogId(), ex);
            if (callback != null) {
                callback.onFailure(new StorageError(FileError.ABORT_ERR));
            }
        }
        return false;
    }

    /****************************************************************************************************************
     * Read Resource from URL path
     ****************************************************************************************************************/

    /**
     * Retrieve the url of the resource on the remote server based on the current
     * runtime environement
     * 
     * @param relativeUrl relative url of the resource according the app base
     * @return The url pointing to the resource dependent on if it is running in
     *         Phonegap container or Webbrowser
     *
     */
    public String getRemoteResourceUrl(String relativeUrl) {
        if (phonegap.isPhoneGapDevice()) {
            return getRemoteAppBaseUrl() + relativeUrl;
        } else {
            return relativeUrl;
        }
    }

    /**
     * Evaluates in case of the runtime (browser/phonegap) the full url where a
     * resource must be retrieved from. In case of Phonegap, it will check if we
     * have the resource already locally stored in the cache and return a url
     * pointing to this one instead.
     * 
     * @param relativeUrl of the resource as it is available in the application
     *          itself.
     * @param version Check if the version of the stored resource equals. Not
     *          checked if version=0
     * @return true if the retrieval was invoked successfully, means you could expect a callback, false otherwise.
     */
    public boolean retrieveResourceUrl(final String relativeUrl, Integer version,
            final Callback<String, FileError> callback) {
        try {
            if (relativeUrl == null || relativeUrl.isEmpty()) {
                if (callback != null) {
                    logger.log(Level.INFO,
                            "Web ResourceCacheReference retrieval impossible with invalid URL : " + relativeUrl);
                    callback.onFailure(
                            new StorageError(FileError.SYNTAX_ERR, "Invalid Url given : " + relativeUrl));
                }
                return false;
            }
            if (!phonegap.isPhoneGapDevice()) {
                if (callback != null) {
                    logger.log(Level.INFO, "Web ResourceCacheReference retrieval : " + relativeUrl);
                    callback.onSuccess(relativeUrl);
                }
                return true;
            }
            // check if we have a cached resource (eg. with a corresponding cache item
            // in the storage)
            StorageResource resource = new StorageResource(relativeUrl, version, null);
            Boolean checkVersion = checkResourceVersion(resource);
            if (checkVersion == null) {
                logger.log(Level.INFO,
                        "No resource cache item found for : " + relativeUrl + " / version:" + version);
                if (callback != null) {
                    callback.onFailure(new StorageError(FileError.NOT_FOUND_ERR, "No resource cache item found"));
                }
            } else if (checkVersion == true) {
                // it should be there already and version is ok
                logger.log(Level.INFO,
                        "Successful ResourceCacheReference retrieval : " + relativeUrl + " / version=" + version);
                getCacheDirectoryEntry(new Callback<DirectoryEntry, StorageError>() {
                    public void onSuccess(DirectoryEntry dirEntry) {
                        if (callback != null) {
                            String localResourceUrl = dirEntry.toURL() + "/"
                                    + convertFilePathToFileName(relativeUrl);
                            logger.log(Level.INFO, "Successful ResourceCacheUrl evaluation : " + localResourceUrl);
                            callback.onSuccess(localResourceUrl);
                        }
                    }

                    public void onFailure(StorageError error) {
                        logger.log(Level.WARNING, "Failure in ResourceCacheUrl evaluation : " + relativeUrl
                                + " error:" + error.getErrorCode());
                        if (callback != null) {
                            callback.onFailure(error);
                        }
                    }
                });
                return true;
            } else {
                logger.log(Level.INFO,
                        "No matching resource cache item found for : " + relativeUrl + "version:" + version);
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception resourceUrl evaluation for : " + relativeUrl, ex);
        }
        return false;
    }

    private static String CACHEFILEPATHDELIMITER = "@@";

    /**
     * Create from a url a proper filename which could be stored in the filesystem
     * 
     * @param filePath
     * @return The filename with all problematic characters replaced with working
     *         ones.
     */
    public static String convertFilePathToFileName(String filePath) {
        return filePath.replace("/", CACHEFILEPATHDELIMITER);
    }

    public static String extractFileNameFromCacheFile(String cachedFileName) {
        try {
            int pos = cachedFileName.indexOf(CACHEFILEPATHDELIMITER);
            if (pos >= 0) {
                return cachedFileName.substring(pos + 2);
            }
        } catch (Exception ex) {
            //
        }
        return cachedFileName;
    }

    /**
     * Check if the version registered in the cache does match this resources
     * version
     * 
     * @return true if the version matches the cache, false if not. If there was
     *         no cache yet, returns null
     */
    protected Boolean checkResourceVersion(StorageResource resource) {
        try {
            // check if we have a cached resource (eg. with a corresponding cache item
            // in the storage)
            String cachedResourceVersion = getLocalStorage().getItem(resource.getResourceVersionKey());
            if (cachedResourceVersion != null) {
                Integer cachedVersion = Integer.parseInt(cachedResourceVersion);
                Integer resourceVersion = resource.getVersion();
                if (resourceVersion == null || resourceVersion == 0 || cachedVersion == resourceVersion) {
                    return true;
                }
                logger.log(Level.WARNING, "Resource version mismatch:" + resource.getResourceUrl() + " version:"
                        + resource.getVersion() + " cachedVersion:" + cachedResourceVersion);
                return false;
            }
            // there was obviously no cache
            return null;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception checking resource version:" + resource.getResourceUrl()
                    + " version:" + resource.getVersion(), ex);
        }
        // something went wrong, we have not found a compatible version therefore
        return false;
    }

    /**
     * Remove all ResourceItem related keys from the LocalStorage and as well
     * related resource files, ClearCache
     */
    public void clearResourceItems() {
        try {
            Storage storage = this.getLocalStorage();
            if (storage == null)
                return;
            Integer len = storage.getLength();
            Integer index = 0;
            for (int i = 0; i < len; i++) {
                String key = storage.key(index);
                if (StorageResource.isResourceIdKey(key)) {
                    logger.log(Level.INFO, "Remove cached ResourceId : " + key);
                    final String fullFileUrl = storage.getItem(key);
                    storage.removeItem(key);
                    // now remove the corresponding file asynch
                    phonegap.getFile().resolveLocalFileSystemURI(fullFileUrl,
                            new FileCallback<EntryBase, FileError>() {
                                @Override
                                public void onSuccess(EntryBase entry) {
                                    try {
                                        logger.log(Level.INFO,
                                                "Remove resource file:" + entry.getAsFileEntry().getFullPath());
                                        entry.getAsFileEntry().remove(new FileCallback<Boolean, FileError>() {
                                            @Override
                                            public void onSuccess(Boolean entry) {
                                                logger.log(Level.INFO, "Successfully deleted file:" + fullFileUrl);
                                            }

                                            @Override
                                            public void onFailure(FileError error) {
                                                logger.log(Level.WARNING, "Unable to delete File:" + fullFileUrl
                                                        + " error:" + error.getErrorCode());
                                            }
                                        });
                                    } catch (Exception successEx) {
                                        logger.log(Level.WARNING, "Remove resource file failed:"
                                                + entry.getAsFileEntry().getFullPath(), successEx);
                                    }
                                }

                                @Override
                                public void onFailure(FileError error) {
                                    logger.log(Level.WARNING, "Unable to locate File for deletion:" + fullFileUrl
                                            + " error:" + error.getErrorCode());
                                }
                            });
                } else if (StorageResource.isResourceVersionKey(key)) {
                    logger.log(Level.INFO, "Remove cached ResourceVersion : " + key);
                    storage.removeItem(key);
                } else {
                    index++;
                }
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Execption clearing Resources", ex);
        }
    }

    private DirectoryEntry cacheDirectoryEntry = null;

    public boolean getCacheDirectoryEntry(final Callback<DirectoryEntry, StorageError> callback) {
        try {
            if (cacheDirectoryEntry != null && callback != null) {
                callback.onSuccess(cacheDirectoryEntry);
                return true;
            }
            if (!phonegap.isPhoneGapDevice())
                return false;
            String cacheDir = getCacheDirectory();
            getLocalDirectoryEntry(cacheDir, new FileCallback<DirectoryEntry, StorageError>() {
                @Override
                public void onSuccess(DirectoryEntry entry) {
                    logger.log(Level.INFO,
                            "CacheDirectory successfully retrieved with path:" + entry.getFullPath());
                    cacheDirectoryEntry = entry;
                    if (callback != null) {
                        callback.onSuccess(entry);
                    }
                }

                @Override
                public void onFailure(StorageError error) {
                    logger.log(Level.SEVERE,
                            "Failure Cache FileSystem Directory retrieval" + " : " + error.toString());
                    // stop the whole stuff, it doesn't work at all, we don't continue
                    // here. Caching will not work therefore
                    if (callback != null) {
                        callback.onFailure(error);
                    }
                }
            });
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception Cache FileSystem Directory retrieval", ex);
        }
        return false;
    }

    /**
     * Check first if the resource with the given url and version is already
     * present, if not try to download the same in a sequential way asynchronously
     * 
     * @param relativeUrl The relative url to the resource from the apps base path
     * @param version The requested resource version
     * @param callback Callback called once the resource was downloaded / or is available local at all
     * @return false if no resource retrieval is invoked really and therefore, no downloadNotification callback will happen.
     */
    public boolean addResourceToCache(final String relativeUrl, final Integer version,
            final FileDownloadCallback downloadNotification) {
        try {
            if (!this.isResourceCachingEnabled())
                return false;
            if (relativeUrl == null || relativeUrl.isEmpty())
                return false;
            StorageResource resource = new StorageResource(relativeUrl, version, downloadNotification);
            StorageResourceCollector collector = new StorageResourceCollector(this, resource);
            Scheduler.get().scheduleDeferred(collector);
            return true;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception adding ResourceToCache", ex);
        }
        return false;
    }

    /**
     * Clear all cached items - key/value pairs in the LocalStorage - Related
     * files in the cache directory
     */
    public void clearStorage() {
        try {
            this.clearResourceItems();
            this.clearStorageItems();
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception on Cache clearing", ex);
        }
    }

    /**
     * Retrieve all ResourceItems related keys from the LocalStorage
     * 
     * @return The number of resources which will be evaluated and for which
     *         callbacks have to be expected
     */
    public int getAllCachedResourceItems(final Callback<StorageInfo, FileError> callback) {
        int resCtr = 0;
        try {
            if (!this.isResourceCachingEnabled())
                return 0;
            if (callback == null)
                return 0;
            logger.log(Level.INFO, "getAllCachedResourceItems");
            Storage storage = this.getLocalStorage();
            int len = storage.getLength();
            for (int i = 0; i < len; i++) {
                String key = storage.key(i);
                if (StorageResource.isResourceIdKey(key)) {
                    logger.log(Level.INFO, "Read cached Resource : " + key);
                    StorageInfoCollector collector = new StorageInfoCollector(this, key, callback);
                    Scheduler.get().scheduleDeferred(collector);
                    resCtr++;
                }
            }
            return resCtr;
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Execption reading all cached Resources", ex);
        }
        return resCtr;
    }

}