001package ch.gbrain.gwtstorage.manager;
002
003/*
004 * #%L
005 * GwtStorage
006 * %%
007 * Copyright (C) 2016 gbrain.ch
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.util.ArrayList;
024import java.util.Date;
025import java.util.List;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.fusesource.restygwt.client.JsonCallback;
030import org.fusesource.restygwt.client.Method;
031import org.fusesource.restygwt.client.Resource;
032
033import ch.gbrain.gwtstorage.model.StorageItem;
034import ch.gbrain.gwtstorage.model.StorageResource;
035
036import com.google.gwt.core.client.Callback;
037import com.google.gwt.core.client.GWT;
038import com.google.gwt.i18n.shared.DateTimeFormat;
039import com.google.gwt.i18n.shared.DateTimeFormat.PredefinedFormat;
040import com.google.gwt.json.client.JSONValue;
041import com.google.gwt.storage.client.Storage;
042import com.googlecode.gwtphonegap.client.PhoneGap;
043import com.googlecode.gwtphonegap.client.connection.Connection;
044import com.googlecode.gwtphonegap.client.file.DirectoryEntry;
045import com.googlecode.gwtphonegap.client.file.EntryBase;
046import com.googlecode.gwtphonegap.client.file.FileCallback;
047import com.googlecode.gwtphonegap.client.file.FileDownloadCallback;
048import com.googlecode.gwtphonegap.client.file.FileEntry;
049import com.googlecode.gwtphonegap.client.file.FileError;
050import com.googlecode.gwtphonegap.client.file.FileObject;
051import com.googlecode.gwtphonegap.client.file.FileReader;
052import com.googlecode.gwtphonegap.client.file.FileSystem;
053import com.googlecode.gwtphonegap.client.file.FileTransfer;
054import com.googlecode.gwtphonegap.client.file.FileTransferError;
055import com.googlecode.gwtphonegap.client.file.FileTransferProgressEvent;
056import com.googlecode.gwtphonegap.client.file.FileWriter;
057import com.googlecode.gwtphonegap.client.file.Flags;
058import com.googlecode.gwtphonegap.client.file.Metadata;
059import com.googlecode.gwtphonegap.client.file.ReaderCallback;
060import com.googlecode.gwtphonegap.client.file.WriterCallback;
061
062/**
063 * This class deals about writing and reading objects from Type StorageItem and
064 * as well loading application resource files (eg. video files) from the
065 * applications home base (server base). This is useful when running the App in
066 * the Web but as well in a Phonegap container as we could store the files
067 * locally (cache) when running in the phonegap container.
068 */
069public class StorageManager
070{
071
072  /**
073   * The remote base url of the application
074   */
075  private String remoteAppBaseUrl = "http://www.exmaple.com/myapp/";
076
077  public String getRemoteAppBaseUrl()
078  {
079    return remoteAppBaseUrl;
080  }
081
082  public void setRemoteAppBaseUrl(String baseUrl)
083  {
084    this.remoteAppBaseUrl = baseUrl;
085    logger.log(Level.INFO, "SetRemoteAppBaseUrl:" + baseUrl);
086  }
087
088  /**
089   * The default storage url in the application. This is the relative location
090   * from the applications base directory.
091   */
092  private String storageUrl = "storage/v1/";
093
094  public String getStorageUrl()
095  {
096    return storageUrl;
097  }
098
099  private String getLocalStorageUrl()
100  {
101    return storageUrl;
102  }
103
104  private String getRemoteStorageUrl()
105  {
106    return getRemoteAppBaseUrl() + storageUrl;
107  }
108
109  public void setStorageUrl(String storageUrl)
110  {
111    this.storageUrl = storageUrl;
112    logger.log(Level.INFO, "SetStorageUrl:" + storageUrl);
113  }
114
115  /**
116   * The directory we are going to create locally on the mobile device as cache
117   * directory when running in the phonegap container.
118   */
119  private String cacheDirectory = "myApp";
120
121  public String getCacheDirectory()
122  {
123    return cacheDirectory;
124  }
125
126  public void setCacheDirectory(String cacheDirectory)
127  {
128    if (this.cacheDirectory.equals(cacheDirectory)) return; // nothing to do it
129                                                            // is the same as
130                                                            // before
131    this.cacheDirectory = cacheDirectory;
132    this.cacheDirectoryEntry = null;
133    logger.log(Level.INFO, "SetCacheDirectory:" + cacheDirectory);
134  }
135
136  /**
137   * Needs to be enabled to perform any caching at all.
138   */
139  private boolean cacheEnabled = true;
140
141  public void setCacheEnabled(boolean cacheEnabled)
142  {
143    this.cacheEnabled = cacheEnabled;
144  }
145
146  public boolean getCacheEnabled()
147  {
148    return this.cacheEnabled;
149  }
150
151  /**
152   * If enabled, the download of the big resource files for local caching (eg.
153   * videos) is only invoked if we are in a wlan network connected. Else it is
154   * taken from backend url always by need.
155   */
156  private boolean wlanEnabled = true;
157
158  public void setWlanEnabled(boolean wlanEnabled)
159  {
160    this.wlanEnabled = wlanEnabled;
161  }
162
163  public boolean getWlanEnabled()
164  {
165    return this.wlanEnabled;
166  }
167
168  private Boolean lastCachingState = null;
169
170  private void logResourceCachingState(boolean state, String msg)
171  {
172    if (lastCachingState == null || lastCachingState != state)
173    {
174      logger.log(Level.INFO, msg);
175    }
176    lastCachingState = state;
177  }
178
179  /**
180   * Evaluates if resource caching is currently enabled at all.
181   * 
182   * @return true if resouces shall be downloaded actually.
183   */
184  private boolean isResourceCachingEnabled()
185  {
186    if (!phonegap.isPhoneGapDevice()) return false;
187    if (!this.getCacheEnabled())
188    {
189      logResourceCachingState(false, "ResourceCaching is disabled");
190      return false;
191    }
192    if (this.getWlanEnabled())
193    {
194      // check connection state
195      if (phonegap.isPhoneGapDevice() && phonegap.getConnection().getType().equalsIgnoreCase(Connection.WIFI))
196      {
197        logResourceCachingState(true, "Wlan requested and available, ResourceCaching enabled");
198        return true;
199      }
200      logResourceCachingState(false, "Wlan requested but not available, ResourceCaching disabled");
201      return false;
202    }
203    logResourceCachingState(true, "ResourceCaching enabled");
204    return true;
205  }
206
207  /**
208   * Evaluates if Caching is possible at all on the given client device at the
209   * moment of evaluation.
210   * 
211   * @return True if we have access to the local file system and therefore are able to provide a local resource caching.
212   */
213  public boolean isResourceCachingPossible()
214  {
215    if (!phonegap.isPhoneGapDevice()) return false;
216    return true;
217  }
218
219  private PhoneGap phonegap;
220
221  private Logger logger;
222
223  private Storage localStorage = null;
224
225  /**
226   * Default constructor setting up the StorageManager all inclusive Logger and
227   * Phonegap references are setup locally
228   */
229  public StorageManager()
230  {
231    this(null, null);
232  }
233
234  /**
235   * Constructor allowing to inject the Phonegap reference and a Logger
236   * 
237   * @param phonegap Give the phonegap reference you have already. Give null if
238   *          you want it to be treated here automatically.
239   * @param logger Give a logger reference or null if you want it to be treated
240   *          here locally.
241   */
242  public StorageManager(PhoneGap phonegap, Logger logger)
243  {
244    if (phonegap != null)
245    {
246      this.phonegap = phonegap;
247    } else
248    {
249      this.phonegap = GWT.create(PhoneGap.class);
250    }
251    if (logger != null)
252    {
253      this.logger = logger;
254    } else
255    {
256      this.logger = Logger.getLogger("StorageManager");
257    }
258    getLocalStorage();
259  }
260
261  /**
262   * Retrieve a reference to the local Storage (Browsers HTML5 key-value store)
263   * 
264   * @return The local storage or null if not supported
265   */
266  public Storage getLocalStorage()
267  {
268    if (localStorage == null)
269    {
270      localStorage = Storage.getLocalStorageIfSupported();
271      if (localStorage == null)
272      {
273        logger.log(Level.SEVERE, "No LocalStorage available!!!!!!!!!!!!!");
274      }
275    }
276    return localStorage;
277  }
278
279  /****************************************************************************************************************
280   * Read / Write StorageItem to private local HTML5 storage
281   ****************************************************************************************************************/
282
283  /**
284   * Write the given item to the local HTML5 storage.
285   * 
286   * @param item The object to be serialized and stored under the ID within the
287   *          local storage
288   * @return true if the write operation succeeded
289   */
290  public boolean writeStorageItemToLocalStorage(StorageItem item)
291  {
292    if (item == null) return false;
293    try
294    {
295      JSONValue json = item.toJson();
296      getLocalStorage().setItem(item.getStorageItemIdKey(), json.toString());
297      writeStorageItemStorageTimeToLocalStorage(item);
298      logger.log(Level.INFO, "Local StorageItem written" + item.getLogId());
299      return true;
300    } catch (Exception ex)
301    {
302      logger.log(Level.SEVERE, "Failure local write" + item.getLogId(), ex);
303    }
304    return false;
305  }
306
307  /**
308   * Read the item from the local html5 storage.
309   * 
310   * @param item The item to be read from the local storage with the given ID as
311   *          key.
312   * @return false if the read operation failed or nothing is found.
313   */
314  public boolean readStorageItemFromLocalStorage(StorageItem item)
315  {
316    return readStorageItemFromLocalStorage(item, 0, 0);
317  }
318
319  /**
320   * Read the item from the local html5 storage and take the cacheTime in
321   * account
322   * 
323   * @param item
324   * @param expectedVersion The minimum item version, not checked if <=0
325   * @param cacheTime If the item was stored longer than the cacheTime, it isn't
326   *          accepted, not checked if <=0
327   * @return false if the read operation failed or nothing is found, the version
328   *         wasn't ok or the cacheTime elapsed already
329   */
330  private boolean readStorageItemFromLocalStorage(StorageItem item, int expectedVersion, int cacheTime)
331  {
332    if (item == null) return false;
333    try
334    {
335      String val = getLocalStorage().getItem(item.getStorageItemIdKey());
336      if (val != null)
337      {
338        logger.log(Level.INFO, "Local StorageItem found" + item.getLogId());
339        item.fromJson(val);
340        // check if the version is ok
341        if (expectedVersion > 0)
342        {
343          if (!checkStorageItemVersion(expectedVersion, item.getVersion()))
344          {
345            logger.log(Level.INFO, "Local StorageItem version mismatch" + item.getLogId());
346            return false;
347          }
348        }
349        // check if cache is valid
350        if (cacheTime > 0)
351        {
352          Date storeTime = readStorageItemStorageTimeFromLocalStorage(item);
353          if (storeTime != null)
354          { // there was a time available, so compare it
355            Date nowTime = new Date();
356            if (nowTime.getTime() - (cacheTime * 1000) > storeTime.getTime())
357            { // elapsed
358              logger.log(Level.INFO, "Local StorageItem time elapsed" + item.getLogId());
359              return false;
360            }
361          }
362        }
363        logger.log(Level.INFO, "Local readStorageItem complete" + item.getLogId());
364        return true;
365      } else
366      {
367        logger.log(Level.INFO, "Local readStorageItem not found" + item.getLogId());
368      }
369    } catch (Exception ex)
370    {
371      logger.log(Level.SEVERE, "Exception local readStorageItem" + item.getLogId(), ex);
372    }
373    return false;
374  }
375
376  /**
377   * Write the items Date/Time store value to html5 storage for later usage in
378   * relation to the cache time
379   * 
380   * @param item The storage time for the given item (ID) is written to the
381   *          key-value HTML5 storage.
382   * @return false if the read operation failed or nothing is found.
383   */
384  private void writeStorageItemStorageTimeToLocalStorage(StorageItem item)
385  {
386    if (item == null || item.getStorageItemTimeKey() == null) return;
387    try
388    {
389      String saveTime = DateTimeFormat.getFormat(PredefinedFormat.DATE_TIME_FULL).format(new Date());
390      getLocalStorage().setItem(item.getStorageItemTimeKey(), saveTime);
391    } catch (Exception ex)
392    {
393      logger.log(Level.SEVERE, "Exception local writeStorageItem time" + item.getLogId(), ex);
394    }
395  }
396
397  /**
398   * Read the items Date/Time store value from html5 storage.
399   * 
400   * @param item The time when a certain StorageItem was written to the HTML5
401   *          key-value storage is read.
402   * @return null if the read operation failed or nothing is found.
403   */
404  private Date readStorageItemStorageTimeFromLocalStorage(StorageItem item)
405  {
406    if (item == null || item.getStorageItemTimeKey() == null) return null;
407    try
408    {
409      String val = getLocalStorage().getItem(item.getStorageItemTimeKey());
410      if (val != null)
411      {
412        return DateTimeFormat.getFormat(PredefinedFormat.DATE_TIME_FULL).parse(val);
413      }
414    } catch (Exception ex)
415    {
416      logger.log(Level.SEVERE, "Exception local readStorageItem time" + item.getLogId(), ex);
417    }
418    return null;
419  }
420
421  /**
422   * Remove all StorageItem related keys from the LocalStorage, thus clear the
423   * cached json objects and references etc.
424   */
425  public void clearStorageItems()
426  {
427    try
428    {
429      Storage storage = this.getLocalStorage();
430      if (storage == null) return;
431      Integer len = storage.getLength();
432      int index = 0;
433      for (int i = 0; i < len; i++)
434      {
435        String key = storage.key(index);
436        if (StorageItem.isStorageItemKey(key))
437        {
438          logger.log(Level.INFO, "Remove cached StorageItem:" + key);
439          storage.removeItem(key);
440        } else
441        {
442          index++;
443        }
444      }
445    } catch (Exception ex)
446    {
447      logger.log(Level.SEVERE, "Execption clearing StorageItems", ex);
448    }
449  }
450
451  /****************************************************************************************************************
452   * Read / Write StorageItems to local Files
453   ****************************************************************************************************************/
454
455  /**
456   * Write the item to a local file
457   * 
458   * @param item The StorageItem to be stored to the file system within the
459   *          defined cache directory
460   * @param callback Is called once the asynch action completed or failed.
461   * @return false if the asynchronous action invocation failed.
462   */
463  public boolean writeStorageItemToLocalFile(final StorageItem item, final Callback<StorageItem, StorageError> callback)
464  {
465    if (item == null) return false;
466    try
467    {
468      logger.log(Level.INFO, "local writeStorageItem invoked " + item.toString());
469      return getLocalFileReference(getCacheDirectory(), item.getJsonFileName(), true, new FileCallback<FileEntry, StorageError>()
470      {
471        @Override
472        public void onSuccess(FileEntry entry)
473        {
474          logger.log(Level.INFO, "local writeStorageItem FileEntry successfully retrieved" + item.getLogId());
475          // store the file content
476          writeStorageItemToLocalFile(entry, item, callback);
477        }
478
479        @Override
480        public void onFailure(StorageError error)
481        {
482          logger.log(Level.SEVERE, "Failure local writeStorageItem FileSystem creation" + item.getLogId() + " " + error.toString());
483        }
484      });
485    } catch (Exception ex)
486    {
487      logger.log(Level.SEVERE, "Exception local writeStorageItem " + item.getLogId(), ex);
488      if (callback != null)
489      {
490        callback.onFailure(new StorageError(FileError.ABORT_ERR));
491      }
492    }
493    return false;
494  }
495
496  /**
497   * Write the item to the local fileentry asynchronous
498   * 
499   * @param fileEntry
500   * @param item
501   * @param callback Is called once the asynch action completed or failed
502   * @return false if the asynchronous action invocation failed.
503   */
504  private boolean writeStorageItemToLocalFile(FileEntry fileEntry, final StorageItem item, final Callback<StorageItem, StorageError> callback)
505  {
506    if (item == null) return false;
507    try
508    {
509      logger.log(Level.INFO, "writeStorageItem to local file invoked" + item.getLogId());
510      fileEntry.createWriter(new FileCallback<FileWriter, FileError>()
511      {
512        @Override
513        public void onSuccess(FileWriter writer)
514        {
515          writer.setOnWriteEndCallback(new WriterCallback<FileWriter>()
516          {
517            @Override
518            public void onCallback(FileWriter result)
519            {
520              // file written
521              logger.log(Level.INFO, "writeToLocalFile successfully written" + item.getLogId());
522              if (callback != null)
523              {
524                callback.onSuccess(item);
525              }
526            }
527          });
528          writer.setOnErrorCallback(new WriterCallback<FileWriter>()
529          {
530            @Override
531            public void onCallback(FileWriter result)
532            {
533              // Error while writing file
534              logger.log(Level.SEVERE, "Failure file write StorageItem" + item.getLogId() + " : " + result.toString());
535              if (callback != null)
536              {
537                callback.onFailure(new StorageError(result.getError()));
538              }
539            }
540          });
541          JSONValue json = item.toJson();
542          writer.write(json.toString());
543        }
544
545        @Override
546        public void onFailure(FileError error)
547        {
548          // can not create writer
549          logger.log(Level.SEVERE, "Failure file writer creation StorageItem" + item.getLogId() + " : " + error.toString());
550          if (callback != null)
551          {
552            callback.onFailure(new StorageError(error));
553          }
554        }
555      });
556      return true;
557    } catch (Exception ex)
558    {
559      logger.log(Level.SEVERE, "Exception file write StorageItem" + item.toString(), ex);
560      if (callback != null)
561      {
562        callback.onFailure(new StorageError(FileError.ABORT_ERR));
563      }
564    }
565    return false;
566  }
567
568  /**
569   * Read the item from the Local File storage
570   * 
571   * @param item The StorageItem (or inherited objects) to be read from the
572   *          local cache file system location.
573   * @param callback Is called once the asynch action completed or failed
574   * @return false if the asynchronous action invocation failed.
575   */
576  public boolean readStorageItemFromLocalFile(final StorageItem item, final Callback<StorageItem, StorageError> callback)
577  {
578    if (item == null) return false;
579    try
580    {
581      // get the file reference
582      return getLocalFileReference(getCacheDirectory(), item.getJsonFileName(), false, new FileCallback<FileEntry, StorageError>()
583      {
584        @Override
585        public void onSuccess(FileEntry entry)
586        {
587          logger.log(Level.INFO, "StorageItem File successfully retrieved" + item.getLogId());
588          readStorageItemFromLocalFile(entry, item, callback);
589        }
590
591        @Override
592        public void onFailure(StorageError error)
593        {
594          logger.log(Level.SEVERE, "Failure LocalFileReference retrieval" + item.getLogId() + " : " + error.toString());
595          if (callback != null)
596          {
597            callback.onFailure(error);
598          }
599        }
600      });
601    } catch (Exception ex)
602    {
603      logger.log(Level.SEVERE, "Exception file write StorageItem" + item.getLogId(), ex);
604      if (callback != null)
605      {
606        callback.onFailure(new StorageError(FileError.ABORT_ERR));
607      }
608    }
609    return false;
610  }
611
612  /**
613   * Read the StorageItem from the given FileEntry and refresh the given
614   * CommonView
615   * 
616   * @param fileEntry
617   * @param item
618   * @param callback Is called once the asynch action completed or failed
619   * @return false if the asynchronous action invocation failed.
620   */
621  private boolean readStorageItemFromLocalFile(FileEntry fileEntry, final StorageItem item, final Callback<StorageItem, StorageError> callback)
622  {
623    if (item == null) return false;
624    try
625    {
626      // logger.log(Level.INFO,"readStorageItem from local file invoked" +
627      // item.getLogId());
628      FileReader reader = phonegap.getFile().createReader();
629      reader.setOnloadCallback(new ReaderCallback<FileReader>()
630      {
631        @Override
632        public void onCallback(FileReader result)
633        {
634          String json = result.getResult();
635          // do something with the content
636          item.fromJson(json);
637          logger.log(Level.INFO, "readStorageItem from local file load completed for item" + item.getLogId());
638          if (callback != null)
639          {
640            callback.onSuccess(item);
641          }
642        }
643      });
644      reader.setOnErrorCallback(new ReaderCallback<FileReader>()
645      {
646        @Override
647        public void onCallback(FileReader result)
648        {
649          // error while reading file...
650          logger.log(Level.SEVERE, "Error StorageItem file writer reading" + item.getLogId() + " : " + result.toString());
651          if (callback != null)
652          {
653            callback.onFailure(new StorageError(result.getError()));
654          }
655        }
656      });
657      return true;
658    } catch (Exception ex)
659    {
660      logger.log(Level.SEVERE, "Exception file read StorageItem" + item.getLogId(), ex);
661      if (callback != null)
662      {
663        callback.onFailure(new StorageError(FileError.ABORT_ERR));
664      }
665    }
666    return false;
667  }
668
669  /**
670   * Our reference to the file system
671   */
672  private FileSystem fileSystem = null;
673
674  /**
675   * Retrieve the FileEntry Reference on the local filesystem of the device if
676   * running in a local container eg. Phonegap
677   * 
678   * @param directory
679   * @param filename
680   * @param callback is called once the asynch action completed or failed
681   * @return false if the asynchronous action invocation failed.
682   */
683  private boolean getFileSystem(final FileCallback<FileSystem, StorageError> callback)
684  {
685    try
686    {
687      if (!phonegap.isPhoneGapDevice()) return false;
688      if (fileSystem != null)
689      {
690        if (callback != null)
691        {
692          callback.onSuccess(fileSystem);
693        }
694        return true;
695      }
696      logger.log(Level.INFO, "getFileReference - Request Local File System");
697      phonegap.getFile().requestFileSystem(FileSystem.LocalFileSystem_PERSISTENT, 0, new FileCallback<FileSystem, FileError>()
698      {
699        @Override
700        public void onSuccess(FileSystem entry)
701        {
702          logger.log(Level.INFO, "FileSystem retrieved");
703          fileSystem = entry;
704          if (callback != null)
705          {
706            callback.onSuccess(fileSystem);
707          }
708        }
709
710        @Override
711        public void onFailure(FileError error)
712        {
713          logger.log(Level.SEVERE, "Failure filesystem retrieval " + error.toString());
714          if (callback != null)
715          {
716            callback.onFailure(new StorageError(error));
717          }
718        }
719      });
720      return true;
721    } catch (Exception ex)
722    {
723      logger.log(Level.SEVERE, "General failure FileSystem retrieval", ex);
724      if (callback != null)
725      {
726        callback.onFailure(new StorageError(FileError.ABORT_ERR));
727      }
728    }
729    return false;
730  }
731
732  /**
733   * Retrieve the FileEntry Reference on the local filesystem of the device if
734   * running in a local container eg. Phonegap
735   * 
736   * @param directory
737   * @param filename
738   * @param callback is called once the asynch action completed or failed
739   * @return false if the asynchronous action invocation failed.
740   */
741  private boolean getLocalFileReference(final String directory, final String filename, final boolean create, final FileCallback<FileEntry, StorageError> callback)
742  {
743    try
744    {
745      if (!phonegap.isPhoneGapDevice()) return false;
746      getFileSystem(new FileCallback<FileSystem, StorageError>()
747      {
748        @Override
749        public void onSuccess(FileSystem entry)
750        {
751          logger.log(Level.INFO, "getLocalFileReference - Request Local File System Directory for " + directory);
752          final DirectoryEntry root = entry.getRoot();
753          root.getDirectory(directory, new Flags(create, false), new FileCallback<DirectoryEntry, FileError>()
754          {
755            @Override
756            public void onSuccess(DirectoryEntry entry)
757            {
758              logger.log(Level.INFO, "getLocalFileReference - Directory retrieved : " + directory);
759              entry.getFile(filename, new Flags(create, false), new FileCallback<FileEntry, FileError>()
760              {
761                @Override
762                public void onSuccess(FileEntry entry)
763                {
764                  logger.log(Level.INFO, "getLocalFileReference - File retrieved : " + filename);
765                  if (callback != null)
766                  {
767                    callback.onSuccess(entry);
768                  }
769                }
770
771                @Override
772                public void onFailure(FileError error)
773                {
774                  logger.log(Level.SEVERE, "Failure file retrieval " + filename + " " + error.toString());
775                  if (callback != null)
776                  {
777                    callback.onFailure(new StorageError(error));
778                  }
779                }
780              });
781            }
782
783            @Override
784            public void onFailure(FileError error)
785            {
786              logger.log(Level.SEVERE, "Failure directory retrieval " + directory + " : " + error.toString());
787            }
788          });
789        }
790
791        @Override
792        public void onFailure(StorageError error)
793        {
794          logger.log(Level.SEVERE, "Failure filesystem retrieval " + error.toString());
795        }
796      });
797      return true;
798    } catch (Exception ex)
799    {
800      logger.log(Level.SEVERE, "General failure directory/file creator", ex);
801      if (callback != null)
802      {
803        callback.onFailure(new StorageError(FileError.ABORT_ERR));
804      }
805    }
806    return false;
807  }
808
809  /**
810   * Retrieve the FileEntry Reference on the local filesystem of the device if
811   * running in a local container eg. Phonegap
812   * 
813   * @param directory The directory which we want to get a reference for. It will be created if it doesn't exist yet. 
814   *                  It is based on the Filesystem reference.
815   * @param callback is called once the asynch action completed or failed
816   * @return false if the asynchronous action invocation failed.
817   */
818  private boolean getLocalDirectoryEntry(final String directory, final FileCallback<DirectoryEntry, StorageError> callback)
819  {
820    try
821    {
822      if (!phonegap.isPhoneGapDevice()) return false;
823      getFileSystem(new FileCallback<FileSystem, StorageError>()
824      {
825        @Override
826        public void onSuccess(FileSystem entry)
827        {
828          logger.log(Level.INFO, "getLocalDirectoryEntry - FileSystem retrieved");
829          final DirectoryEntry root = entry.getRoot();
830          root.getDirectory(directory, new Flags(true, false), new FileCallback<DirectoryEntry, FileError>()
831          {
832            @Override
833            public void onSuccess(final DirectoryEntry dirEntry)
834            {
835              logger.log(Level.INFO, "getLocalDirectoryEntry - Directory retrieved : " + directory);
836              if (callback != null)
837              {
838                callback.onSuccess(dirEntry);
839              }
840            }
841
842            @Override
843            public void onFailure(FileError error)
844            {
845              logger.log(Level.SEVERE, "Failure directory retrieval " + directory + " : " + error.toString() + " : " + error.getErrorCode());
846              if (callback != null)
847              {
848                callback.onFailure(new StorageError(error));
849              }
850            }
851          });
852        }
853
854        @Override
855        public void onFailure(StorageError error)
856        {
857          logger.log(Level.SEVERE, "Failure filesystem retrieval " + error.toString() + " : " + error.getErrorCode());
858          if (callback != null)
859          {
860            callback.onFailure(error);
861          }
862        }
863      });
864      return true;
865    } catch (Exception ex)
866    {
867      logger.log(Level.SEVERE, "Exception in getLocalDirectory : " + directory, ex);
868      if (callback != null)
869      {
870        callback.onFailure(new StorageError(FileError.ABORT_ERR));
871      }
872    }
873    return false;
874  }
875
876  /****************************************************************************************************************
877   * Read StorageItem from URL path
878   ****************************************************************************************************************/
879
880  /**
881   * Retrieve the item from the storage. First try local storage, if the version
882   * and the validTime are valid, it is returned. Else it tries to retrieve it
883   * from the remote backend. If found, it is cached locally for fast access.
884   * 
885   * @param item The item to be read by ID from 1. the cache, 2. localAppPath 3.
886   *          remoteAppPath in this priority order
887   * @param useCache If true, the system will first try to retrieve the value
888   *          from the local cache before it reads the same from the
889   *          applications path
890   * @param validTime The maximum age in seconds of the cache to be accepted as
891   *          a valid item value, if elapsed it will try to read from the
892   *          applications path / If <= 0 don't care
893   * @param expectedVersion The versionNumber which must be available in the
894   *          cache to be a valid cache item. If <=0 don't care.
895   */
896  public boolean readStorageItem(final StorageItem item, boolean useCache, int expectedVersion, int validTime, final Callback<StorageItem, StorageError> callback)
897  {
898    try
899    {
900      logger.log(Level.INFO, "readStorageItem" + item.getLogId());
901      if (useCache && this.getCacheEnabled())
902      { // retrieve the item first from local storage cache
903        if (this.readStorageItemFromLocalStorage(item, expectedVersion, validTime))
904        { // found it valid in the cache
905          callback.onSuccess(item);
906          return true;
907        }
908      }
909      // didn't found a matching item in the cache yet or version mismatch or
910      // cache time elapsed
911      if (phonegap.isPhoneGapDevice())
912      {
913        // we run in a locally installed app and want to retrieve now the value
914        // from the given backend
915        return this.readStorageItemFromRemoteApplication(item, callback);
916      } else
917      { // in the case of web app, load it from the applications relative base path
918        // this is automatically from the backend server where the app was
919        // loaded from
920        return this.readStorageItemFromLocalApplication(item, callback);
921      }
922    } catch (Exception ex)
923    {
924      logger.log(Level.SEVERE, "Exception readStorageItem" + item.getLogId(), ex);
925      if (callback != null)
926      {
927        callback.onFailure(new StorageError(FileError.ABORT_ERR));
928      }
929    }
930    return false;
931  }
932
933  /**
934   * Read the item asynch from the applications own source server with the given
935   * credentials and base path eg.
936   * /storage/v1/ch.gbrain.testapp.model.items-1.json Note: We just give the
937   * local relative url which means: - If running as local application (eg.
938   * started locally in Browser) it will load from local base - If running as
939   * web application it will load from the servers base - If running as Phonegap
940   * app it will load from the local installed base
941   * 
942   * @param item
943   * @param callback is called once the asynch action completed or failed
944   * @return false if the asynchronous action invocation failed.
945   */
946  public boolean readStorageItemFromLocalApplication(final StorageItem item, final Callback<StorageItem, StorageError> callback)
947  {
948    // we run in the web directly and therefore we read it directly from the
949    // application relative storage in the Webapp itself
950    logger.log(Level.INFO, "Read StorageItem from local applications base" + item.getLogId());
951    return readStorageItemFromUrl(this.getLocalStorageUrl(), item, getReadStorageItemHandler(item, callback, null));
952    // for testing in browser use this. But Chrome must run without security to
953    // work
954    // return readFromUrl(this.appRemoteStorageUrl,item,callback);
955  }
956
957  /**
958   * Compare the given versions.
959   * 
960   * @param expectedVersion The version we do expect at least / if <=0, we don't
961   *          care about versions, it is always ok
962   * @param realVersion The real version of the item
963   * @return true if the realversion >= expectedVersion
964   */
965  private boolean checkStorageItemVersion(int expectedVersion, int realVersion)
966  {
967    if (expectedVersion <= 0) return true; // the version doesn't care
968    if (realVersion >= expectedVersion) return true;
969    return false;
970  }
971
972  /**
973   * Read the item asynch from the configured remote application base
974   * 
975   * @param item
976   * @param callback is called once the asynch action completed or failed
977   * @return false if the asynchronous action invocation failed.
978   */
979  public boolean readStorageItemFromRemoteApplication(final StorageItem item, final Callback<StorageItem, StorageError> callback)
980  {
981    logger.log(Level.INFO, "Read StorageItem from remote application base" + item.getLogId());
982    return readStorageItemFromUrl(this.getRemoteStorageUrl(), item, getReadStorageItemHandler(item, callback, this.getLocalStorageUrl()));
983  }
984
985  /**
986   * Creates and returns a Callback which treats the result for a url Item
987   * retrieval
988   * 
989   * @param item The StorageItem (or a inheriting object) which must be read
990   *          (filled in with the retrieved data)
991   * @param callback The final resp. initial callback to be notified of the
992   *          result
993   * @param fallBack A URL to which a further request must be done if the call
994   *          fails
995   * @return The callback which deals with the asynch result of the remote item
996   *         retrieval
997   */
998  private Callback<StorageItem, StorageError> getReadStorageItemHandler(final StorageItem item, final Callback<StorageItem, StorageError> callback, final String fallbackUrl)
999  {
1000    return new Callback<StorageItem, StorageError>()
1001    {
1002      public void onSuccess(StorageItem newItem)
1003      { // loading succeeded
1004        // store it in the cache
1005        logger.log(Level.INFO, "Completed read item from url" + item.getLogId());
1006        writeStorageItemToLocalStorage(newItem);
1007        callback.onSuccess(newItem);
1008      }
1009
1010      public void onFailure(StorageError error)
1011      {
1012        logger.log(Level.WARNING, "Failure url loading" + item.getLogId());
1013        // nothing found, check if we must retrieve it from a remote location
1014        if (fallbackUrl != null && !fallbackUrl.isEmpty())
1015        {
1016          readStorageItemFromUrl(fallbackUrl, item, getReadStorageItemHandler(item, callback, null));
1017        } else
1018        {
1019          callback.onFailure(error);
1020        }
1021      }
1022    };
1023  }
1024
1025  /**
1026   * Read the JSON item asynch from the given url and the
1027   * standard name of the item eg. ch.gbrain.testapp.model.items-1.json
1028   * 
1029   * @param url The url to read from eg. for local application relative path
1030   *          "storage/v1/" eg. for remote location
1031   *          "http://host.domain.ch/testapp/storage/v1/"
1032   * @param item
1033   * @param callback is called once the asynch action completed or failed
1034   * @return false if the asynchronous action invocation failed.
1035   */
1036  public boolean readStorageItemFromUrl(String url, final StorageItem item, final Callback<StorageItem, StorageError> callback)
1037  {
1038    if (item == null) return false;
1039    try
1040    {
1041      Resource resource = new Resource(url + item.getJsonFileName() + "?noCache=" + new Date().getTime());
1042      Method method = resource.get();
1043      /**
1044       * if (username.isEmpty()) { method = resource.get(); }else { method =
1045       * resource.get().user(username).password(password); }
1046       */
1047      logger.log(Level.INFO, "Read from url:" + method.builder.getUrl());
1048      method.send(new JsonCallback()
1049      {
1050        public void onSuccess(Method method, JSONValue response)
1051        {
1052          logger.log(Level.INFO, "Read from url success");
1053          if (response != null)
1054          {
1055            try
1056            {
1057              logger.log(Level.INFO, "Successfully url read" + item.getLogId());
1058              item.fromJson(response);
1059              if (callback != null)
1060              {
1061                callback.onSuccess(item);
1062              }
1063            } catch (Exception ex)
1064            {
1065              logger.log(Level.SEVERE, "Failure url read" + item.getLogId(), ex);
1066            }
1067          }
1068        }
1069
1070        public void onFailure(Method method, Throwable exception)
1071        {
1072          logger.log(Level.WARNING, "Failure url read" + item.getLogId(), exception);
1073          if (callback != null)
1074          {
1075            callback.onFailure(new StorageError(FileError.NOT_READABLE_ERR, exception.getMessage()));
1076          }
1077        }
1078      });
1079      logger.log(Level.INFO, "Read from url call complete");
1080      return true;
1081    } catch (Exception ex)
1082    {
1083      logger.log(Level.SEVERE, "Error url read" + item.getLogId(), ex);
1084      if (callback != null)
1085      {
1086        callback.onFailure(new StorageError(FileError.ABORT_ERR));
1087      }
1088    }
1089    return false;
1090  }
1091
1092  /****************************************************************************************************************
1093   * Read Resource from URL path
1094   ****************************************************************************************************************/
1095
1096  /**
1097   * Retrieve the url of the resource on the remote server based on the current
1098   * runtime environement
1099   * 
1100   * @param relativeUrl relative url of the resource according the app base
1101   * @return The url pointing to the resource dependent on if it is running in
1102   *         Phonegap container or Webbrowser
1103   *
1104   */
1105  public String getRemoteResourceUrl(String relativeUrl)
1106  {
1107    if (phonegap.isPhoneGapDevice())
1108    {
1109      return getRemoteAppBaseUrl() + relativeUrl;
1110    } else
1111    {
1112      return relativeUrl;
1113    }
1114  }
1115
1116  /**
1117   * Evaluates in case of the runtime (browser/phonegap) the full url where a
1118   * resource must be retrieved from. In case of Phonegap, it will check if we
1119   * have the resource already locally stored in the cache and return a url
1120   * pointing to this one instead.
1121   * 
1122   * @param relativeUrl of the resource as it is available in the application
1123   *          itself.
1124   * @param version Check if the version of the stored resource equals. Not
1125   *          checked if version=0
1126   * @return The full url where the resource must be retrieved from (either
1127   *         points to the home server of the application as configured or it
1128   *         points to the found local cached resource file.
1129   */
1130  public boolean retrieveResourceUrl(final String relativeUrl, Integer version, final Callback<String, FileError> callback)
1131  {
1132    try
1133    {
1134      if (relativeUrl == null || relativeUrl.isEmpty())
1135      {
1136        if (callback != null)
1137        {
1138          logger.log(Level.INFO, "Web ResourceCacheReference retrieval impossible with invalid URL : " + relativeUrl);
1139          callback.onFailure(new StorageError(FileError.SYNTAX_ERR, "Invalid Url given : " + relativeUrl));
1140        }
1141        return false;
1142      }
1143      if (!phonegap.isPhoneGapDevice())
1144      {
1145        if (callback != null)
1146        {
1147          logger.log(Level.INFO, "Web ResourceCacheReference retrieval : " + relativeUrl);
1148          callback.onSuccess(relativeUrl);
1149        }
1150        return true;
1151      }
1152      // check if we have a cached resource (eg. with a corresponding cache item
1153      // in the storage)
1154      StorageResource resource = new StorageResource(relativeUrl, version);
1155      Boolean checkVersion = checkResourceVersion(resource);
1156      if (checkVersion == null)
1157      {
1158        logger.log(Level.INFO, "No resource cache item found for : " + relativeUrl + " / version:" + version);
1159        if (callback != null)
1160        {
1161          callback.onFailure(new StorageError(FileError.NOT_FOUND_ERR, "No resource cache item found"));
1162        }
1163      } else if (checkVersion == true)
1164      {
1165        // it should be there already and version is ok
1166        logger.log(Level.INFO, "Successful ResourceCacheReference retrieval : " + relativeUrl + " / version=" + version);
1167        getCacheDirectoryEntry(new Callback<DirectoryEntry, StorageError>()
1168        {
1169          public void onSuccess(DirectoryEntry dirEntry)
1170          {
1171            if (callback != null)
1172            {
1173              String localResourceUrl = dirEntry.toURL() + "/" + convertFilePathToFileName(relativeUrl);
1174              logger.log(Level.INFO, "Successful ResourceCacheUrl evaluation : " + localResourceUrl);
1175              callback.onSuccess(localResourceUrl);
1176            }
1177          }
1178
1179          public void onFailure(StorageError error)
1180          {
1181            logger.log(Level.WARNING, "Failure in ResourceCacheUrl evaluation : " + relativeUrl + " error:" + error.getErrorCode());
1182            if (callback != null)
1183            {
1184              callback.onFailure(error);
1185            }
1186          }
1187        });
1188        return true;
1189      } else
1190      {
1191        logger.log(Level.INFO, "No matching resource cache item found for : " + relativeUrl + "version:" + version);
1192      }
1193    } catch (Exception ex)
1194    {
1195      logger.log(Level.SEVERE, "Exception resourceUrl evaluation for : " + relativeUrl, ex);
1196    }
1197    return false;
1198  }
1199
1200  /**
1201   * Create from a url a proper filename which could be stored in the filesystem
1202   * 
1203   * @param filePath
1204   * @return The filename with all problematic characters replaced with working
1205   *         ones.
1206   */
1207  private String convertFilePathToFileName(String filePath)
1208  {
1209    return filePath.replace("/", "@@");
1210  }
1211
1212  /**
1213   * Download the given resource url and store it in the local cache Directory.
1214   * 
1215   * @param resource
1216   * @param destinationDir The URL of the destination Directoy
1217   */
1218  private void downloadCacheResource(final StorageResource resource)
1219  {
1220    try
1221    {
1222      if (resource == null) return;
1223      logger.log(Level.INFO, "downloadCacheResource " + resource.getResourceUrl() + " Version:" + resource.getVersion());
1224      getCacheDirectoryEntry(new Callback<DirectoryEntry, StorageError>()
1225      {
1226        public void onSuccess(DirectoryEntry cacheDir)
1227        {
1228          try
1229          {
1230            FileTransfer fileTransfer = phonegap.getFile().createFileTransfer();
1231            String localFileName = convertFilePathToFileName(resource.getResourceUrl());
1232            String sourceUrl = getRemoteAppBaseUrl() + resource.getResourceUrl();
1233            String destUrl = cacheDir.toURL() + localFileName;
1234            // String destUrl =
1235            // "cdvfile://localhost/persistent/testapp/test.mp4";
1236            logger.log(Level.INFO, "downloadResource invoked for : " + sourceUrl + " to : " + destUrl);
1237            fileTransfer.download(sourceUrl, destUrl, getResourceDownloadHandler(resource));
1238          } catch (Exception lex)
1239          {
1240            logger.log(Level.SEVERE, "Exception in downloadCacheResource success handler", lex);
1241          }
1242        }
1243
1244        public void onFailure(StorageError error)
1245        {
1246          logger.log(Level.WARNING, "Failed to download CacheResource for : " + resource.getResourceUrl());
1247        }
1248      });
1249    } catch (Exception ex)
1250    {
1251      logger.log(Level.SEVERE, "Exception resourceDownload for : " + resource.getResourceUrl(), ex);
1252    }
1253  }
1254
1255  /**
1256   * Creates and returns a Callback which treats the result for a url resource
1257   * retrieval The just downloaded resource is registered in the local storage
1258   * with the version for future cache handling
1259   * 
1260   * @return The callback which deals with the asynch result of the remote
1261   *         resource retrieval
1262   */
1263  private FileDownloadCallback getResourceDownloadHandler(final StorageResource resource)
1264  {
1265    return new FileDownloadCallback()
1266    {
1267      public void onSuccess(FileEntry fileEntry)
1268      {
1269        try
1270        {
1271          logger.log(Level.INFO, "FileDownload success " + fileEntry.getFullPath() + " for resource=" + resource.getResourceUrl() + " version=" + resource.getVersion());
1272          // register now in the storage the version for the cache checks in the
1273          // future
1274          getLocalStorage().setItem(resource.getResourceIdKey(), fileEntry.toURL());
1275          getLocalStorage().setItem(resource.getResourceVersionKey(), resource.getVersion().toString());
1276          downloadInProgress = false;
1277          checkNextCacheResource();
1278        } catch (Exception lex)
1279        {
1280          logger.log(Level.SEVERE, "Exception on cacheResource download success handler", lex);
1281        }
1282      }
1283
1284      public void onProgress(FileTransferProgressEvent progress)
1285      {
1286        // logger.log(Level.INFO,"FileDownload Progress " +
1287        // progress.getLoadedBytes());
1288      }
1289
1290      public void onFailure(FileTransferError error)
1291      {
1292        logger.log(Level.SEVERE, "FileDownload Failure " + error.toString() + " : " + resource.getResourceUrl());
1293        downloadInProgress = false;
1294        checkNextCacheResource();
1295      }
1296    };
1297  }
1298
1299  /**
1300   * Check if the version registered in the cache does match this resources
1301   * version
1302   * 
1303   * @return true if the version matches the cache, false if not. If there was
1304   *         no cache yet, returns null
1305   */
1306  public Boolean checkResourceVersion(StorageResource resource)
1307  {
1308    try
1309    {
1310      // check if we have a cached resource (eg. with a corresponding cache item
1311      // in the storage)
1312      String cachedResourceVersion = getLocalStorage().getItem(resource.getResourceVersionKey());
1313      if (cachedResourceVersion != null)
1314      {
1315        Integer cachedVersion = Integer.parseInt(cachedResourceVersion);
1316        Integer resourceVersion = resource.getVersion();
1317        if (resourceVersion == null || resourceVersion == 0 || cachedVersion == resourceVersion)
1318        {
1319          return true;
1320        }
1321        logger.log(Level.WARNING, "Resource version mismatch:" + resource.getResourceUrl() + " version:" + resource.getVersion() + " cachedVersion:" + cachedResourceVersion);
1322        return false;
1323      }
1324      // there was obviously no cache
1325      return null;
1326    } catch (Exception ex)
1327    {
1328      logger.log(Level.SEVERE, "Exception checking resource version:" + resource.getResourceUrl() + " version:" + resource.getVersion(), ex);
1329    }
1330    // something went wrong, we have not found a compatible version therefore
1331    return false;
1332  }
1333
1334  /**
1335   * Remove all ResourceItem related keys from the LocalStorage and as well
1336   * related resource files, ClearCache
1337   */
1338  public void clearResourceItems()
1339  {
1340    try
1341    {
1342      Storage storage = this.getLocalStorage();
1343      if (storage == null) return;
1344      Integer len = storage.getLength();
1345      Integer index = 0;
1346      for (int i = 0; i < len; i++)
1347      {
1348        String key = storage.key(index);
1349        if (StorageResource.isResourceIdKey(key))
1350        {
1351          logger.log(Level.INFO, "Remove cached ResourceId : " + key);
1352          final String fullFileUrl = storage.getItem(key);
1353          storage.removeItem(key);
1354          // now remove the corresponding file asynch
1355          phonegap.getFile().resolveLocalFileSystemURI(fullFileUrl, new FileCallback<EntryBase, FileError>()
1356          {
1357            @Override
1358            public void onSuccess(EntryBase entry)
1359            {
1360              try
1361              {
1362                logger.log(Level.INFO, "Remove resource file:" + entry.getAsFileEntry().getFullPath());
1363                entry.getAsFileEntry().remove(new FileCallback<Boolean, FileError>()
1364                {
1365                  @Override
1366                  public void onSuccess(Boolean entry)
1367                  {
1368                    logger.log(Level.INFO, "Successfully deleted file:" + fullFileUrl);
1369                  }
1370
1371                  @Override
1372                  public void onFailure(FileError error)
1373                  {
1374                    logger.log(Level.WARNING, "Unable to delete File:" + fullFileUrl + " error:" + error.getErrorCode());
1375                  }
1376                });
1377              } catch (Exception successEx)
1378              {
1379                logger.log(Level.WARNING, "Remove resource file failed:" + entry.getAsFileEntry().getFullPath(), successEx);
1380              }
1381            }
1382
1383            @Override
1384            public void onFailure(FileError error)
1385            {
1386              logger.log(Level.WARNING, "Unable to locate File for deletion:" + fullFileUrl + " error:" + error.getErrorCode());
1387            }
1388          });
1389        } else if (StorageResource.isResourceVersionKey(key))
1390        {
1391          logger.log(Level.INFO, "Remove cached ResourceVersion : " + key);
1392          storage.removeItem(key);
1393        } else
1394        {
1395          index++;
1396        }
1397      }
1398    } catch (Exception ex)
1399    {
1400      logger.log(Level.SEVERE, "Execption clearing Resources", ex);
1401    }
1402  }
1403
1404  private List<StorageResource> cacheResourceQueue = new ArrayList<StorageResource>();
1405  private boolean downloadInProgress = false;
1406  private DirectoryEntry cacheDirectoryEntry = null;
1407
1408  private boolean getCacheDirectoryEntry(final Callback<DirectoryEntry, StorageError> callback)
1409  {
1410    try
1411    {
1412      if (cacheDirectoryEntry != null && callback != null)
1413      {
1414        callback.onSuccess(cacheDirectoryEntry);
1415        return true;
1416      }
1417      if (!phonegap.isPhoneGapDevice()) return false;
1418      String cacheDir = getCacheDirectory();
1419      getLocalDirectoryEntry(cacheDir, new FileCallback<DirectoryEntry, StorageError>()
1420      {
1421        @Override
1422        public void onSuccess(DirectoryEntry entry)
1423        {
1424          logger.log(Level.INFO, "CacheDirectory successfully retrieved with path:" + entry.getFullPath());
1425          cacheDirectoryEntry = entry;
1426          if (callback != null)
1427          {
1428            callback.onSuccess(entry);
1429          }
1430        }
1431
1432        @Override
1433        public void onFailure(StorageError error)
1434        {
1435          logger.log(Level.SEVERE, "Failure Cache FileSystem Directory retrieval" + " : " + error.toString());
1436          // stop the whole stuff, it doesn't work at all, we don't continue
1437          // here. Caching will not work therefore
1438          if (callback != null)
1439          {
1440            callback.onFailure(error);
1441          }
1442        }
1443      });
1444      return true;
1445    } catch (Exception ex)
1446    {
1447      logger.log(Level.SEVERE, "Exception Cache FileSystem Directory retrieval", ex);
1448    }
1449    return false;
1450  }
1451
1452  /**
1453   * Check if there are further resources to be downloaded into the cache
1454   * 
1455   * Note: GWT compiler will ignore the synchronized here, I leave it to show
1456   * that we take care about. As JS is single threaded, there won't be any
1457   * concurrency issues anyhow, so we don't have to take any more special
1458   * actions.
1459   */
1460  private synchronized void checkNextCacheResource()
1461  {
1462    try
1463    {
1464      if (downloadInProgress) return;
1465      if (cacheResourceQueue.size() > 0)
1466      {
1467        downloadInProgress = true;
1468        StorageResource next = cacheResourceQueue.remove(0);
1469        downloadCacheResource(next);
1470      }
1471    } catch (Exception ex)
1472    {
1473      logger.log(Level.SEVERE, "Failure Downloading Cache Resource", ex);
1474    } finally
1475    {
1476      downloadInProgress = false;
1477    }
1478  }
1479
1480  /**
1481   * Check first if the resource with the given url and version is already
1482   * present, if not try to download the same in a sequential way asynchronously
1483   * 
1484   * @param relativeUrl
1485   * @param version
1486   */
1487  public void addResourceToCache(final String relativeUrl, final Integer version)
1488  {
1489    try
1490    {
1491      if (!this.isResourceCachingEnabled()) return;
1492      if (relativeUrl == null || relativeUrl.isEmpty()) return;
1493      // check if we have it already in the cache with the right version
1494      StorageResource resource = new StorageResource(relativeUrl, version);
1495      Boolean versionCheck = checkResourceVersion(resource);
1496      if (versionCheck == null)
1497      {
1498        logger.log(Level.WARNING, "ResourceCacheReference retrieval no chache entry found : " + relativeUrl + " requestedVersion:" + version + " -> invoke loading");
1499      } else if (versionCheck == true)
1500      {
1501        // it should be there already and version is ok
1502        logger.log(Level.INFO, "Successful ResourceCacheReference retrieval : " + relativeUrl);
1503        // check if it really exists but asynch
1504        String fileName = convertFilePathToFileName(relativeUrl);
1505        this.getLocalFileReference(getCacheDirectory(), fileName, false, new FileCallback<FileEntry, StorageError>()
1506        {
1507          @Override
1508          public void onSuccess(FileEntry entry)
1509          {
1510            logger.log(Level.INFO, "Successful ResourceCacheFile retrieval : " + relativeUrl);
1511            // the cache is ok, file is there in right version, we don't have to
1512            // do something really.
1513          }
1514
1515          @Override
1516          public void onFailure(StorageError error)
1517          {
1518            logger.log(Level.SEVERE, "Failure ResourceCacheReference retrieval : " + relativeUrl + " : " + error.toString());
1519            // nothing found, we must try to download again
1520            // add it to the list of downloadable cache entries
1521            cacheResourceQueue.add(new StorageResource(relativeUrl, version));
1522            checkNextCacheResource();
1523          }
1524        });
1525        return;
1526      } else if (versionCheck == false)
1527      {
1528        // version doesn't match, invoke reload
1529        logger.log(Level.WARNING, "ResourceCacheReference retrieval version mismatch : " + relativeUrl + " requestedVersion:" + version + " -> invoke loading");
1530      }
1531      // add it to the list of downloadable cache entries
1532      cacheResourceQueue.add(new StorageResource(relativeUrl, version));
1533      checkNextCacheResource();
1534    } catch (Exception ex)
1535    {
1536      logger.log(Level.SEVERE, "Exception addingResourceToCache", ex);
1537    }
1538  }
1539
1540  /**
1541   * Clear all cached items - key/value pairs in the LocalStorage - Related
1542   * files in the cache directory
1543   */
1544  public void clearStorage()
1545  {
1546    try
1547    {
1548      this.clearResourceItems();
1549      this.clearStorageItems();
1550    } catch (Exception ex)
1551    {
1552      logger.log(Level.SEVERE, "Exception on Cache clearing", ex);
1553    }
1554
1555  }
1556
1557  /**
1558   * Retrieve all ResourceItems related keys from the LocalStorage
1559   * 
1560   * @return The number of resources which will be evaluated and for which
1561   *         callbacks have to be expected
1562   */
1563  public int getAllCachedResourceItems(final Callback<StorageInfo, FileError> callback)
1564  {
1565    try
1566    {
1567      if (!this.isResourceCachingEnabled()) return 0;
1568      if (callback == null) return 0;
1569      logger.log(Level.INFO, "getAllCachedResourceItems");
1570      Storage storage = this.getLocalStorage();
1571      int len = storage.getLength();
1572      int resCtr = 0;
1573      for (int i = 0; i < len; i++)
1574      {
1575        String key = storage.key(i);
1576        if (StorageResource.isResourceIdKey(key))
1577        {
1578          logger.log(Level.INFO, "Read cached Resource : " + key);
1579          resCtr++;
1580          StorageInfoCollector collector = new StorageInfoCollector(key, callback);
1581          collector.collectInfo();
1582        }
1583      }
1584      return resCtr;
1585    } catch (Exception ex)
1586    {
1587      logger.log(Level.SEVERE, "Execption reading all cached Resources", ex);
1588    }
1589    return 0;
1590  }
1591
1592  private class StorageInfoCollector
1593  {
1594
1595    private String storageKey;
1596    private String version;
1597    private String fileName;
1598    private String filePath;
1599    private String fileUrl;
1600    private Long fileSize;
1601    private Date lastModificationDate;
1602    private Callback<StorageInfo, FileError> callback;
1603    private FileEntry fileEntry;
1604
1605    public StorageInfoCollector(String storageKey, Callback<StorageInfo, FileError> callback)
1606    {
1607      this.storageKey = storageKey;
1608      this.callback = callback;
1609    }
1610
1611    private String logBaseInfo()
1612    {
1613      return "CollectInfo : " + storageKey + " / ";
1614    }
1615
1616    private void collectInfo()
1617    {
1618      Storage storage = getLocalStorage();
1619      String versionKey = StorageResource.getResourceVersionKey(storageKey);
1620      version = storage.getItem(versionKey);
1621      fileUrl = storage.getItem(storageKey);
1622      // now resolve the file asynch
1623      phonegap.getFile().resolveLocalFileSystemURI(fileUrl, new FileCallback<EntryBase, FileError>()
1624      {
1625        @Override
1626        public void onSuccess(EntryBase entry)
1627        {
1628          logger.log(Level.INFO, logBaseInfo() + "ResolveLocalFileSystemUri success");
1629          fileEntry = entry.getAsFileEntry();
1630          fileEntry.getFile(new FileCallback<FileObject, FileError>()
1631          {
1632            @Override
1633            public void onSuccess(FileObject entry)
1634            {
1635              logger.log(Level.INFO, logBaseInfo() + "FileEntry located : " + entry.getFullPath() + " name:" + entry.getName());
1636              fileName = entry.getName();
1637              filePath = entry.getFullPath();
1638              fileSize = entry.size();
1639              try
1640              { // might throw an exception (unknown method in Phonegap .....
1641                // lastModificationDate = entry.getLastModifiedDate(); -> take
1642                // it from Metadata instead
1643              } catch (Exception ex)
1644              {
1645                logger.log(Level.FINEST, logBaseInfo() + "Failure in File Modification Date evaluation", ex);
1646              }
1647              fileEntry.getMetadata(new FileCallback<Metadata, FileError>()
1648              {
1649                @Override
1650                public void onSuccess(Metadata metadata)
1651                {
1652                  logger.log(Level.INFO, logBaseInfo() + "Successful FileMetadata located");
1653                  try
1654                  {
1655                    lastModificationDate = metadata.getModificationTime();
1656                  } catch (Exception ex)
1657                  {
1658                    logger.log(Level.FINEST, logBaseInfo() + "Failure in Metadata Modification Date evaluation", ex);
1659                  }
1660                  invokeSuccessCallback();
1661                }
1662
1663                @Override
1664                public void onFailure(FileError error)
1665                {
1666                  logger.log(Level.WARNING, logBaseInfo() + "Failure cache FileEntry Metadata retrieval with error : " + error.toString());
1667                  // anyhow signal success even if we don't have the Metadata
1668                  invokeSuccessCallback();
1669                }
1670              });
1671            }
1672
1673            @Override
1674            public void onFailure(FileError error)
1675            {
1676              logger.log(Level.SEVERE, logBaseInfo() + "Failure cache FileEntry info retrieval with error : " + error.toString());
1677              callback.onFailure(error);
1678            }
1679          });
1680        }
1681
1682        @Override
1683        public void onFailure(FileError error)
1684        {
1685          logger.log(Level.WARNING, logBaseInfo() + "Unable to locate cache File information with error : " + error.getErrorCode());
1686          if (callback != null)
1687          {
1688            callback.onFailure(error);
1689          }
1690        }
1691      });
1692    }
1693
1694    private void invokeSuccessCallback()
1695    {
1696      StorageInfo info = new StorageInfo();
1697      info.setFileName(this.fileName);
1698      info.setFilePath(filePath);
1699      info.setFileUrl(fileUrl);
1700      info.setFileSize(fileSize);
1701      info.setLastModificationDate(lastModificationDate);
1702      info.setStorageKey(storageKey);
1703      info.setVersion(version);
1704      callback.onSuccess(info);
1705    }
1706
1707  }
1708
1709}