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