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}