1 // Copyright (C) 2001-2003 Jon A. Maxwell (JAM) 2 // 3 // This library is free software; you can redistribute it and/or 4 // modify it under the terms of the GNU Lesser General Public 5 // License as published by the Free Software Foundation; either 6 // version 2.1 of the License, or (at your option) any later version. 7 // 8 // This library is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 // Lesser General Public License for more details. 12 // 13 // You should have received a copy of the GNU Lesser General Public 14 // License along with this library; if not, write to the Free Software 15 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 17 package net.sourceforge.jnlp.cache; 18 19 import java.io.BufferedInputStream; 20 import java.io.BufferedOutputStream; 21 import java.io.File; 22 import java.io.FileOutputStream; 23 import java.io.FilePermission; 24 import java.io.IOException; 25 import java.io.InputStream; 26 import java.io.OutputStream; 27 import java.net.MalformedURLException; 28 import java.net.URL; 29 import java.net.URLConnection; 30 import java.nio.channels.FileChannel; 31 import java.nio.channels.FileLock; 32 import java.security.Permission; 33 import java.util.ArrayList; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Map.Entry; 37 import java.util.Set; 38 39 import javax.jnlp.DownloadServiceListener; 40 41 import net.sourceforge.jnlp.Version; 42 import net.sourceforge.jnlp.config.DeploymentConfiguration; 43 import net.sourceforge.jnlp.config.PathsAndFiles; 44 import net.sourceforge.jnlp.runtime.ApplicationInstance; 45 import net.sourceforge.jnlp.runtime.JNLPRuntime; 46 import static net.sourceforge.jnlp.runtime.Translator.R; 47 48 import net.sourceforge.jnlp.security.ConnectionFactory; 49 import net.sourceforge.jnlp.util.FileUtils; 50 import net.sourceforge.jnlp.util.PropertiesFile; 51 import net.sourceforge.jnlp.util.logging.OutputController; 52 53 /** 54 * Provides static methods to interact with the cache, download 55 * indicator, and other utility methods. 56 * 57 * @author <a href="mailto:jmaxwell@users.sourceforge.net">Jon A. Maxwell (JAM)</a> - initial author 58 * @version $Revision: 1.17 $ 59 */ 60 public class CacheUtil { 61 62 63 64 /** 65 * Caches a resource and returns a URL for it in the cache; 66 * blocks until resource is cached. If the resource location is 67 * not cacheable (points to a local file, etc) then the original 68 * URL is returned. 69 * 70 * @param location location of the resource 71 * @param version the version, or {@code null} 72 * @param policy how to handle update 73 * @return either the location in the cache or the original location 74 */ getCachedResourceURL(URL location, Version version, UpdatePolicy policy)75 public static URL getCachedResourceURL(URL location, Version version, UpdatePolicy policy) { 76 try { 77 File f = getCachedResourceFile(location, version, policy); 78 //url was ponting to nowhere eg 404 79 if (f == null){ 80 //originally f.toUrl was throwing NPE 81 return null; 82 //returning null seems to be better 83 } 84 // TODO: Should be toURI().toURL() 85 return f.toURL(); 86 } catch (MalformedURLException ex) { 87 return location; 88 } 89 } 90 91 /** 92 * This is returning File object of cached resource originally from URL 93 * @param location original location of blob 94 * @param version version of resource 95 * @param policy update policy of resource 96 * @return location in ITW cache on filesystem 97 */ getCachedResourceFile(URL location, Version version, UpdatePolicy policy)98 public static File getCachedResourceFile(URL location, Version version, UpdatePolicy policy) { 99 ResourceTracker rt = new ResourceTracker(); 100 rt.addResource(location, version, null, policy); 101 File f = rt.getCacheFile(location); 102 return f; 103 } 104 105 /** 106 * Returns the Permission object necessary to access the 107 * resource, or {@code null} if no permission is needed. 108 * @param location location of the resource 109 * @param version the version, or {@code null} 110 * @return permissions of the location 111 */ getReadPermission(URL location, Version version)112 public static Permission getReadPermission(URL location, Version version) { 113 Permission result = null; 114 if (CacheUtil.isCacheable(location, version)) { 115 File file = CacheUtil.getCacheFile(location, version); 116 result = new FilePermission(file.getPath(), "read"); 117 } else { 118 try { 119 // this is what URLClassLoader does 120 URLConnection conn = ConnectionFactory.getConnectionFactory().openConnection(location); 121 result = conn.getPermission(); 122 ConnectionFactory.getConnectionFactory().disconnect(conn); 123 } catch (java.io.IOException ioe) { 124 // should try to figure out the permission 125 OutputController.getLogger().log(ioe); 126 } 127 } 128 129 return result; 130 } 131 132 /** 133 * Clears the cache by deleting all the Netx cache files 134 * 135 * Note: Because of how our caching system works, deleting jars of another javaws 136 * process is using them can be quite disasterous. Hence why Launcher creates lock files 137 * and we check for those by calling {@link #okToClearCache()} 138 * @return true if the cache could be cleared and was cleared 139 */ clearCache()140 public static boolean clearCache() { 141 142 if (!okToClearCache()) { 143 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("CCannotClearCache")); 144 return false; 145 } 146 147 CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance(); 148 File cacheDir = lruHandler.getCacheDir().getFile(); 149 if (!(cacheDir.isDirectory())) { 150 return false; 151 } 152 153 OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "Clearing cache directory: " + cacheDir); 154 lruHandler.lock(); 155 try { 156 cacheDir = cacheDir.getCanonicalFile(); 157 FileUtils.recursiveDelete(cacheDir, cacheDir); 158 cacheDir.mkdir(); 159 lruHandler.clearLRUSortedEntries(); 160 lruHandler.store(); 161 } catch (IOException e) { 162 throw new RuntimeException(e); 163 } finally { 164 lruHandler.unlock(); 165 } 166 return true; 167 } 168 169 /** 170 * Returns a boolean indicating if it ok to clear the netx application cache at this point 171 * @return true if the cache can be cleared at this time without problems 172 */ okToClearCache()173 private static boolean okToClearCache() { 174 File otherJavawsRunning = PathsAndFiles.MAIN_LOCK.getFile(); 175 FileLock locking = null; 176 try { 177 if (otherJavawsRunning.isFile()) { 178 FileOutputStream fis = new FileOutputStream(otherJavawsRunning); 179 180 FileChannel channel = fis.getChannel(); 181 locking = channel.tryLock(); 182 if (locking == null) { 183 OutputController.getLogger().log("Other instances of netx are running"); 184 return false; 185 } 186 OutputController.getLogger().log("No other instances of netx are running"); 187 return true; 188 189 } else { 190 OutputController.getLogger().log("No instance file found"); 191 return true; 192 } 193 } catch (IOException e) { 194 return false; 195 } finally { 196 if (locking != null) { 197 try { 198 locking.release(); 199 } catch (IOException ex) { 200 OutputController.getLogger().log(ex); 201 } 202 } 203 } 204 } 205 206 /** 207 * Returns whether there is a version of the URL contents in the 208 * cache and it is up to date. This method may not return 209 * immediately. 210 * 211 * @param source the source {@link URL} 212 * @param version the versions to check for 213 * @param lastModifed time in milis since epoch of last modfication 214 * @return whether the cache contains the version 215 * @throws IllegalArgumentException if the source is not cacheable 216 */ isCurrent(URL source, Version version, long lastModifed)217 public static boolean isCurrent(URL source, Version version, long lastModifed) { 218 219 if (!isCacheable(source, version)) 220 throw new IllegalArgumentException(R("CNotCacheable", source)); 221 222 try { 223 CacheEntry entry = new CacheEntry(source, version); // could pool this 224 boolean result = entry.isCurrent(lastModifed); 225 226 OutputController.getLogger().log("isCurrent: " + source + " = " + result); 227 228 return result; 229 } catch (Exception ex) { 230 OutputController.getLogger().log(ex); 231 return isCached(source, version); // if can't connect return whether already in cache 232 } 233 } 234 235 /** 236 * Returns true if the cache has a local copy of the contents of 237 * the URL matching the specified version string. 238 * 239 * @param source the source URL 240 * @param version the versions to check for 241 * @return true if the source is in the cache 242 * @throws IllegalArgumentException if the source is not cacheable 243 */ isCached(URL source, Version version)244 public static boolean isCached(URL source, Version version) { 245 if (!isCacheable(source, version)) 246 throw new IllegalArgumentException(R("CNotCacheable", source)); 247 248 CacheEntry entry = new CacheEntry(source, version); // could pool this 249 boolean result = entry.isCached(); 250 251 OutputController.getLogger().log("isCached: " + source + " = " + result); 252 253 return result; 254 } 255 256 /** 257 * Returns whether the resource can be cached as a local file; 258 * if not, then URLConnection.openStream can be used to obtain 259 * the contents. 260 * @param source the url of resource 261 * @param version version of resource 262 * @return whether this resource can be cached 263 */ isCacheable(URL source, Version version)264 public static boolean isCacheable(URL source, Version version) { 265 if (source == null) 266 return false; 267 268 if (source.getProtocol().equals("file")){ 269 return false; 270 } 271 if (source.getProtocol().equals("jar")){ 272 return false; 273 } 274 return true; 275 } 276 277 /** 278 * Returns the file for the locally cached contents of the 279 * source. This method returns the file location only and does 280 * not download the resource. The latest version of the 281 * resource that matches the specified version will be returned. 282 * 283 * @param source the source {@link URL} 284 * @param version the version id of the local file 285 * @return the file location in the cache, or {@code null} if no versions cached 286 * @throws IllegalArgumentException if the source is not cacheable 287 */ getCacheFile(URL source, Version version)288 public static File getCacheFile(URL source, Version version) { 289 // ensure that version is an version id not version string 290 291 if (!isCacheable(source, version)) 292 throw new IllegalArgumentException(R("CNotCacheable", source)); 293 294 File cacheFile = null; 295 CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance(); 296 synchronized (lruHandler) { 297 try { 298 lruHandler.lock(); 299 300 // We need to reload the cacheOrder file each time 301 // since another plugin/javaws instance may have updated it. 302 lruHandler.load(); 303 cacheFile = getCacheFileIfExist(urlToPath(source, "")); 304 if (cacheFile == null) { // We did not find a copy of it. 305 cacheFile = makeNewCacheFile(source, version); 306 } else 307 lruHandler.store(); 308 } finally { 309 lruHandler.unlock(); 310 } 311 } 312 return cacheFile; 313 } 314 315 /** 316 * This will return a File pointing to the location of cache item. 317 * 318 * @param urlPath Path of cache item within cache directory. 319 * @return File if we have searched before, {@code null} otherwise. 320 */ getCacheFileIfExist(File urlPath)321 private static File getCacheFileIfExist(File urlPath) { 322 CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance(); 323 synchronized (lruHandler) { 324 File cacheFile = null; 325 List<Entry<String, String>> entries = lruHandler.getLRUSortedEntries(); 326 // Start searching from the most recent to least recent. 327 for (Entry<String, String> e : entries) { 328 final String key = e.getKey(); 329 final String path = e.getValue(); 330 331 if (pathToURLPath(path).equals(urlPath.getPath())) { // Match found. 332 cacheFile = new File(path); 333 lruHandler.updateEntry(key); 334 break; // Stop searching since we got newest one already. 335 } 336 } 337 return cacheFile; 338 } 339 } 340 341 /** 342 * Get the path to file minus the cache directory and indexed folder. 343 */ pathToURLPath(String path)344 private static String pathToURLPath(String path) { 345 int len = CacheLRUWrapper.getInstance().getCacheDir().getFullPath().length(); 346 int index = path.indexOf(File.separatorChar, len + 1); 347 return path.substring(index); 348 } 349 350 /** 351 * Returns the parent directory of the cached resource. 352 * @param filePath The path of the cached resource directory. 353 * @return parent dir of cache 354 */ getCacheParentDirectory(String filePath)355 public static String getCacheParentDirectory(String filePath) { 356 String path = filePath; 357 String tempPath; 358 String cacheDir = CacheLRUWrapper.getInstance().getCacheDir().getFullPath(); 359 360 while(path.startsWith(cacheDir) && !path.equals(cacheDir)){ 361 tempPath = new File(path).getParent(); 362 363 if (tempPath.equals(cacheDir)) 364 break; 365 366 path = tempPath; 367 } 368 return path; 369 } 370 371 /** 372 * This will create a new entry for the cache item. It is however not 373 * initialized but any future calls to getCacheFile with the source and 374 * version given to here, will cause it to return this item. 375 * 376 * @param source the source URL 377 * @param version the version id of the local file 378 * @return the file location in the cache. 379 */ makeNewCacheFile(URL source, Version version)380 public static File makeNewCacheFile(URL source, Version version) { 381 CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance(); 382 synchronized (lruHandler) { 383 File cacheFile = null; 384 try { 385 lruHandler.lock(); 386 lruHandler.load(); 387 for (long i = 0; i < Long.MAX_VALUE; i++) { 388 String path = lruHandler.getCacheDir().getFullPath()+ File.separator + i; 389 File cDir = new File(path); 390 if (!cDir.exists()) { 391 // We can use this directory. 392 try { 393 cacheFile = urlToPath(source, path); 394 FileUtils.createParentDir(cacheFile); 395 File pf = new File(cacheFile.getPath() + ".info"); 396 FileUtils.createRestrictedFile(pf, true); // Create the info file for marking later. 397 lruHandler.addEntry(lruHandler.generateKey(cacheFile.getPath()), cacheFile.getPath()); 398 } catch (IOException ioe) { 399 OutputController.getLogger().log(ioe); 400 } 401 402 break; 403 } 404 } 405 406 lruHandler.store(); 407 } finally { 408 lruHandler.unlock(); 409 } 410 return cacheFile; 411 } 412 } 413 414 /** 415 * Returns a buffered output stream open for writing to the 416 * cache file. 417 * 418 * @param source the remote location 419 * @param version the file version to write to 420 * @return the stream to write to resource 421 * @throws java.io.IOException if IO breaks 422 */ getOutputStream(URL source, Version version)423 public static OutputStream getOutputStream(URL source, Version version) throws IOException { 424 File localFile = getCacheFile(source, version); 425 OutputStream out = new FileOutputStream(localFile); 426 427 return new BufferedOutputStream(out); 428 } 429 430 /** 431 * Copies from an input stream to an output stream. On 432 * completion, both streams will be closed. Streams are 433 * buffered automatically. 434 * @param is stream to read from 435 * @param os stream to write to 436 * @throws java.io.IOException if copy fails 437 */ streamCopy(InputStream is, OutputStream os)438 public static void streamCopy(InputStream is, OutputStream os) throws IOException { 439 if (!(is instanceof BufferedInputStream)) 440 is = new BufferedInputStream(is); 441 442 if (!(os instanceof BufferedOutputStream)) 443 os = new BufferedOutputStream(os); 444 445 try { 446 byte b[] = new byte[4096]; 447 while (true) { 448 int c = is.read(b, 0, b.length); 449 if (c == -1) 450 break; 451 452 os.write(b, 0, c); 453 } 454 } finally { 455 is.close(); 456 os.close(); 457 } 458 } 459 460 /** 461 * Converts a URL into a local path string within the given directory. For 462 * example a url with subdirectory /tmp/ will 463 * result in a File that is located somewhere within /tmp/ 464 * 465 * @param location the url 466 * @param subdir the subdirectory 467 * @return the file 468 */ urlToPath(URL location, String subdir)469 public static File urlToPath(URL location, String subdir) { 470 if (subdir == null) { 471 throw new NullPointerException(); 472 } 473 474 StringBuilder path = new StringBuilder(); 475 476 path.append(subdir); 477 path.append(File.separatorChar); 478 479 path.append(location.getProtocol()); 480 path.append(File.separatorChar); 481 path.append(location.getHost()); 482 path.append(File.separatorChar); 483 path.append(location.getPath().replace('/', File.separatorChar)); 484 if (location.getQuery() != null && !location.getQuery().trim().isEmpty()) { 485 path.append(".").append(location.getQuery()); 486 } 487 488 return new File(FileUtils.sanitizePath(path.toString())); 489 } 490 491 /** 492 * Waits until the resources are downloaded, while showing a 493 * progress indicator. 494 * 495 * @param app application instance with context for this resource 496 * @param tracker the resource tracker 497 * @param resources the resources to wait for 498 * @param title name of the download 499 */ waitForResources(ApplicationInstance app, ResourceTracker tracker, URL resources[], String title)500 public static void waitForResources(ApplicationInstance app, ResourceTracker tracker, URL resources[], String title) { 501 DownloadIndicator indicator = JNLPRuntime.getDefaultDownloadIndicator(); 502 DownloadServiceListener listener = null; 503 504 try { 505 if (indicator == null) { 506 tracker.waitForResources(resources, 0); 507 return; 508 } 509 510 // see if resources can be downloaded very quickly; avoids 511 // overhead of creating display components for the resources 512 if (tracker.waitForResources(resources, indicator.getInitialDelay())) 513 return; 514 515 // only resources not starting out downloaded are displayed 516 List<URL> urlList = new ArrayList<>(); 517 for (URL url : resources) { 518 if (!tracker.checkResource(url)) 519 urlList.add(url); 520 } 521 URL undownloaded[] = urlList.toArray(new URL[urlList.size()]); 522 523 listener = indicator.getListener(app, title, undownloaded); 524 525 do { 526 long read = 0; 527 long total = 0; 528 529 for (URL url : undownloaded) { 530 // add in any -1's; they're insignificant 531 total += tracker.getTotalSize(url); 532 read += tracker.getAmountRead(url); 533 } 534 535 int percent = (int) ((100 * read) / Math.max(1, total)); 536 537 for (URL url : undownloaded) { 538 listener.progress(url, "version", 539 tracker.getAmountRead(url), 540 tracker.getTotalSize(url), 541 percent); 542 } 543 } while (!tracker.waitForResources(resources, indicator.getUpdateRate())); 544 545 // make sure they read 100% until indicator closes 546 for (URL url : undownloaded) { 547 listener.progress(url, "version", 548 tracker.getTotalSize(url), 549 tracker.getTotalSize(url), 550 100); 551 } 552 } catch (InterruptedException ex) { 553 OutputController.getLogger().log(ex); 554 } finally { 555 if (listener != null) 556 indicator.disposeListener(listener); 557 } 558 } 559 560 /** 561 * This will remove all old cache items. 562 */ cleanCache()563 public static void cleanCache() { 564 CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance(); 565 if (okToClearCache()) { 566 // First we want to figure out which stuff we need to delete. 567 HashSet<String> keep = new HashSet<>(); 568 HashSet<String> remove = new HashSet<>(); 569 try { 570 lruHandler.lock(); 571 lruHandler.load(); 572 573 long maxSize = -1; // Default 574 try { 575 maxSize = Long.parseLong(JNLPRuntime.getConfiguration().getProperty(DeploymentConfiguration.KEY_CACHE_MAX_SIZE)); 576 } catch (NumberFormatException nfe) { 577 } 578 579 maxSize = maxSize << 20; // Convert from megabyte to byte (Negative values will be considered unlimited.) 580 long curSize = 0; 581 582 for (Entry<String, String> e : lruHandler.getLRUSortedEntries()) { 583 // Check if the item is contained in cacheOrder. 584 final String key = e.getKey(); 585 final String path = e.getValue(); 586 587 File file = new File(path); 588 PropertiesFile pf = new PropertiesFile(new File(path + ".info")); 589 boolean delete = Boolean.parseBoolean(pf.getProperty("delete")); 590 591 /* 592 * This will get me the root directory specific to this cache item. 593 * Example: 594 * cacheDir = /home/user1/.icedtea/cache 595 * file.getPath() = /home/user1/.icedtea/cache/0/http/www.example.com/subdir/a.jar 596 * rStr first becomes: /0/http/www.example.com/subdir/a.jar 597 * then rstr becomes: /home/user1/.icedtea/cache/0 598 */ 599 String rStr = file.getPath().substring(lruHandler.getCacheDir().getFullPath().length()); 600 rStr = lruHandler.getCacheDir().getFullPath()+ rStr.substring(0, rStr.indexOf(File.separatorChar, 1)); 601 long len = file.length(); 602 603 if (keep.contains(file.getPath().substring(rStr.length()))) { 604 lruHandler.removeEntry(key); 605 continue; 606 } 607 608 /* 609 * we remove entries from our lru if any of the following condition is met. 610 * Conditions: 611 * - delete: file has been marked for deletion. 612 * - !file.isFile(): if someone tampered with the directory, file doesn't exist. 613 * - maxSize >= 0 && curSize + len > maxSize: If a limit was set and the new size 614 * on disk would exceed the maximum size. 615 */ 616 if (delete || !file.isFile() || (maxSize >= 0 && curSize + len > maxSize)) { 617 lruHandler.removeEntry(key); 618 remove.add(rStr); 619 continue; 620 } 621 622 curSize += len; 623 keep.add(file.getPath().substring(rStr.length())); 624 625 for (File f : file.getParentFile().listFiles()) { 626 if (!(f.equals(file) || f.equals(pf.getStoreFile()))) { 627 try { 628 FileUtils.recursiveDelete(f, f); 629 } catch (IOException e1) { 630 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e1); 631 } 632 } 633 634 } 635 } 636 lruHandler.store(); 637 } finally { 638 lruHandler.unlock(); 639 } 640 removeSetOfDirectories(remove); 641 } 642 } 643 removeSetOfDirectories(Set<String> remove)644 private static void removeSetOfDirectories(Set<String> remove) { 645 for (String s : remove) { 646 File f = new File(s); 647 try { 648 FileUtils.recursiveDelete(f, f); 649 } catch (IOException e) { 650 } 651 } 652 } 653 } 654