1 /*
   2  * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.nashorn.internal.codegen;
  26 
  27 import java.io.BufferedInputStream;
  28 import java.io.BufferedOutputStream;
  29 import java.io.DataInputStream;
  30 import java.io.DataOutputStream;
  31 import java.io.File;
  32 import java.io.FileInputStream;
  33 import java.io.FileNotFoundException;
  34 import java.io.FileOutputStream;
  35 import java.io.IOException;
  36 import java.io.InputStream;
  37 import java.io.PrintWriter;
  38 import java.io.StringWriter;
  39 import java.io.UncheckedIOException;
  40 import java.net.URI;
  41 import java.net.URL;
  42 import java.nio.file.Files;
  43 import java.nio.file.FileSystem;
  44 import java.nio.file.FileSystems;
  45 import java.nio.file.Path;
  46 import java.security.AccessController;
  47 import java.security.MessageDigest;
  48 import java.security.PrivilegedAction;
  49 import java.text.SimpleDateFormat;
  50 import java.util.Base64;
  51 import java.util.Date;
  52 import java.util.Map;
  53 import java.util.Timer;
  54 import java.util.TimerTask;
  55 import java.util.concurrent.TimeUnit;
  56 import java.util.concurrent.atomic.AtomicBoolean;
  57 import java.util.function.Consumer;
  58 import java.util.function.Function;
  59 import java.util.function.IntFunction;
  60 import java.util.function.Predicate;
  61 import java.util.stream.Stream;
  62 import jdk.nashorn.internal.codegen.types.Type;
  63 import jdk.nashorn.internal.runtime.Context;
  64 import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData;
  65 import jdk.nashorn.internal.runtime.Source;
  66 import jdk.nashorn.internal.runtime.logging.DebugLogger;
  67 import jdk.nashorn.internal.runtime.options.Options;
  68 
  69 /**
  70  * <p>Static utility that encapsulates persistence of type information for functions compiled with optimistic
  71  * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized,
  72  * the type information for deoptimization is stored in a cache file. If the same function is compiled in a
  73  * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system
  74  * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its
  75  * optimistic types at (or near) the steady state.
  76  * </p><p>
  77  * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system
  78  * property is specified with a value greater than 0, it is enabled and operates in an operating-system
  79  * specific per-user cache directory. You can override the directory by specifying it in the
  80  * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that
  81  * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is
  82  * added to the cache. The default delay is 20 seconds, and can be set using the
  83  * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word
  84  * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is
  85  * allowed to grow without limits.
  86  * </p>
  87  */
  88 public final class OptimisticTypesPersistence {
  89     // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is
  90     // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will
  91     // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one
  92     // file per JavaScript function.
  93     private static final int DEFAULT_MAX_FILES = 0;
  94     // Constants for signifying that the cache should not be limited
  95     private static final int UNLIMITED_FILES = -1;
  96     // Maximum number of files that should be cached on disk. The maximum will be softly enforced.
  97     private static final int MAX_FILES = getMaxFiles();
  98     // Number of seconds to wait between adding a new file to the cache and running a cleanup process
  99     private static final int DEFAULT_CLEANUP_DELAY = 20;
 100     private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty(
 101             "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY));
 102     // The name of the default subdirectory within the system cache directory where we store type info.
 103     private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo";
 104     // The directory where we cache type info
 105     private static final File baseCacheDir = createBaseCacheDir();
 106     private static final File cacheDir = createCacheDir(baseCacheDir);
 107     // In-process locks to make sure we don't have a cross-thread race condition manipulating any file.
 108     private static final Object[] locks = cacheDir == null ? null : createLockArray();
 109     // Only report one read/write error every minute
 110     private static final long ERROR_REPORT_THRESHOLD = 60000L;
 111 
 112     private static volatile long lastReportedError;
 113     private static final AtomicBoolean scheduledCleanup;
 114     private static final Timer cleanupTimer;
 115     static {
 116         if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) {
 117             scheduledCleanup = null;
 118             cleanupTimer = null;
 119         } else {
 120             scheduledCleanup = new AtomicBoolean();
 121             cleanupTimer = new Timer(true);
 122         }
 123     }
 124     /**
 125      * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed
 126      * to {@link #load(Object)} and {@link #store(Object, Map)} methods.
 127      * @param source the source where the function comes from
 128      * @param functionId the unique ID number of the function within the source
 129      * @param paramTypes the types of the function parameters (as persistence is per parameter type
 130      * specialization).
 131      * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled.
 132      */
 133     public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) {
 134         if(cacheDir == null) {
 135             return null;
 136         }
 137         final StringBuilder b = new StringBuilder(48);
 138         // Base64-encode the digest of the source, and append the function id.
 139         b.append(source.getDigest()).append('-').append(functionId);
 140         // Finally, if this is a parameter-type specialized version of the function, add the parameter types
 141         // to the file name.
 142         if(paramTypes != null && paramTypes.length > 0) {
 143             b.append('-');
 144             for(final Type t: paramTypes) {
 145                 b.append(Type.getShortSignatureDescriptor(t));
 146             }
 147         }
 148         return new LocationDescriptor(new File(cacheDir, b.toString()));
 149     }
 150 
 151     private static final class LocationDescriptor {
 152         private final File file;
 153 
 154         LocationDescriptor(final File file) {
 155             this.file = file;
 156         }
 157     }
 158 
 159 
 160     /**
 161      * Stores the map of optimistic types for a given function.
 162      * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling
 163      * {@link #getLocationDescriptor(Source, int, Type[])}.
 164      * @param optimisticTypes the map of optimistic types.
 165      */
 166     @SuppressWarnings("resource")
 167     public static void store(final Object locationDescriptor, final Map<Integer, Type> optimisticTypes) {
 168         if(locationDescriptor == null || optimisticTypes.isEmpty()) {
 169             return;
 170         }
 171         final File file = ((LocationDescriptor)locationDescriptor).file;
 172 
 173         AccessController.doPrivileged(new PrivilegedAction<Void>() {
 174             @Override
 175             public Void run() {
 176                 synchronized(getFileLock(file)) {
 177                     if (!file.exists()) {
 178                         // If the file already exists, we aren't increasing the number of cached files, so
 179                         // don't schedule cleanup.
 180                         scheduleCleanup();
 181                     }
 182                     try (final FileOutputStream out = new FileOutputStream(file)) {
 183                         out.getChannel().lock(); // lock exclusive
 184                         final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
 185                         Type.writeTypeMap(optimisticTypes, dout);
 186                         dout.flush();
 187                     } catch(final Exception e) {
 188                         reportError("write", file, e);
 189                     }
 190                 }
 191                 return null;
 192             }
 193         });
 194     }
 195 
 196     /**
 197      * Loads the map of optimistic types for a given function.
 198      * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling
 199      * {@link #getLocationDescriptor(Source, int, Type[])}.
 200      * @return the map of optimistic types, or null if persisted type information could not be retrieved.
 201      */
 202     @SuppressWarnings("resource")
 203     public static Map<Integer, Type> load(final Object locationDescriptor) {
 204         if (locationDescriptor == null) {
 205             return null;
 206         }
 207         final File file = ((LocationDescriptor)locationDescriptor).file;
 208         return AccessController.doPrivileged(new PrivilegedAction<Map<Integer, Type>>() {
 209             @Override
 210             public Map<Integer, Type> run() {
 211                 try {
 212                     if(!file.isFile()) {
 213                         return null;
 214                     }
 215                     synchronized(getFileLock(file)) {
 216                         try (final FileInputStream in = new FileInputStream(file)) {
 217                             in.getChannel().lock(0, Long.MAX_VALUE, true); // lock shared
 218                             final DataInputStream din = new DataInputStream(new BufferedInputStream(in));
 219                             return Type.readTypeMap(din);
 220                         }
 221                     }
 222                 } catch (final Exception e) {
 223                     reportError("read", file, e);
 224                     return null;
 225                 }
 226             }
 227         });
 228     }
 229 
 230     private static void reportError(final String msg, final File file, final Exception e) {
 231         final long now = System.currentTimeMillis();
 232         if(now - lastReportedError > ERROR_REPORT_THRESHOLD) {
 233             reportError(String.format("Failed to %s %s", msg, file), e);
 234             lastReportedError = now;
 235         }
 236     }
 237 
 238     /**
 239      * Logs an error message with warning severity (reasoning being that we're reporting an error that'll disable the
 240      * type info cache, but it's only logged as a warning because that doesn't prevent Nashorn from running, it just
 241      * disables a performance-enhancing cache).
 242      * @param msg the message to log
 243      * @param e the exception that represents the error.
 244      */
 245     private static void reportError(final String msg, final Exception e) {
 246         getLogger().warning(msg, "\n", exceptionToString(e));
 247     }
 248 
 249     /**
 250      * A helper that prints an exception stack trace into a string. We have to do this as if we just pass the exception
 251      * to {@link DebugLogger#warning(Object...)}, it will only log the exception message and not the stack, making
 252      * problems harder to diagnose.
 253      * @param e the exception
 254      * @return the string representation of {@link Exception#printStackTrace()} output.
 255      */
 256     private static String exceptionToString(final Exception e) {
 257         final StringWriter sw = new StringWriter();
 258         final PrintWriter pw = new PrintWriter(sw, false);
 259         e.printStackTrace(pw);
 260         pw.flush();
 261         return sw.toString();
 262     }
 263 
 264     private static File createBaseCacheDir() {
 265         if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) {
 266             return null;
 267         }
 268         try {
 269             return createBaseCacheDirPrivileged();
 270         } catch(final Exception e) {
 271             reportError("Failed to create cache dir", e);
 272             return null;
 273         }
 274     }
 275 
 276     private static File createBaseCacheDirPrivileged() {
 277         return AccessController.doPrivileged(new PrivilegedAction<File>() {
 278             @Override
 279             public File run() {
 280                 final String explicitDir = System.getProperty("nashorn.typeInfo.cacheDir");
 281                 final File dir;
 282                 if(explicitDir != null) {
 283                     dir = new File(explicitDir);
 284                 } else {
 285                     // When no directory is explicitly specified, get an operating system specific cache
 286                     // directory, and create "com.oracle.java.NashornTypeInfo" in it.
 287                     final File systemCacheDir = getSystemCacheDir();
 288                     dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME);
 289                     if (isSymbolicLink(dir)) {
 290                         return null;
 291                     }
 292                 }
 293                 return dir;
 294             }
 295         });
 296     }
 297 
 298     private static File createCacheDir(final File baseDir) {
 299         if (baseDir == null) {
 300             return null;
 301         }
 302         try {
 303             return createCacheDirPrivileged(baseDir);
 304         } catch(final Exception e) {
 305             reportError("Failed to create cache dir", e);
 306             return null;
 307         }
 308     }
 309 
 310     private static File createCacheDirPrivileged(final File baseDir) {
 311         return AccessController.doPrivileged(new PrivilegedAction<File>() {
 312             @Override
 313             public File run() {
 314                 final String versionDirName;
 315                 try {
 316                     versionDirName = getVersionDirName();
 317                 } catch(final Exception e) {
 318                     reportError("Failed to calculate version dir name", e);
 319                     return null;
 320                 }
 321                 final File versionDir = new File(baseDir, versionDirName);
 322                 if (isSymbolicLink(versionDir)) {
 323                     return null;
 324                 }
 325                 versionDir.mkdirs();
 326                 if (versionDir.isDirectory()) {
 327                     //FIXME:Logger is disabled as Context.getContext() always returns null here because global scope object will not be created
 328                     //by the time this method gets invoked
 329                     getLogger().info("Optimistic type persistence directory is " + versionDir);
 330                     return versionDir;
 331                 }
 332                 getLogger().warning("Could not create optimistic type persistence directory " + versionDir);
 333                 return null;
 334             }
 335         });
 336     }
 337 
 338     /**
 339      * Returns an operating system specific root directory for cache files.
 340      * @return an operating system specific root directory for cache files.
 341      */
 342     private static File getSystemCacheDir() {
 343         final String os = System.getProperty("os.name", "generic");
 344         if("Mac OS X".equals(os)) {
 345             // Mac OS X stores caches in ~/Library/Caches
 346             return new File(new File(System.getProperty("user.home"), "Library"), "Caches");
 347         } else if(os.startsWith("Windows")) {
 348             // On Windows, temp directory is the best approximation of a cache directory, as its contents
 349             // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally
 350             // points to a user-specific temp directory, %HOME%\LocalSettings\Temp.
 351             return new File(System.getProperty("java.io.tmpdir"));
 352         } else {
 353             // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache"
 354             return new File(System.getProperty("user.home"), ".cache");
 355         }
 356     }
 357 
 358     private static final String ANCHOR_PROPS = "anchor.properties";
 359 
 360     /**
 361      * In order to ensure that changes in Nashorn code don't cause corruption in the data, we'll create a
 362      * per-code-version directory. Normally, this will create the SHA-1 digest of the nashorn.jar. In case the classpath
 363      * for nashorn is local directory (e.g. during development), this will create the string "dev-" followed by the
 364      * timestamp of the most recent .class file.
 365      *
 366      * @return digest of currently running nashorn
 367      * @throws Exception if digest could not be created
 368      */
 369     public static String getVersionDirName() throws Exception {
 370         // NOTE: getResource("") won't work if the JAR file doesn't have directory entries (and JAR files in JDK distro
 371         // don't, or at least it's a bad idea counting on it). Alternatively, we could've tried
 372         // getResource("OptimisticTypesPersistence.class") but behavior of getResource with regard to its willingness
 373         // to hand out URLs to .class files is also unspecified. Therefore, the most robust way to obtain an URL to our
 374         // package is to have a small non-class anchor file and start out from its URL.
 375         final URL url = OptimisticTypesPersistence.class.getResource(ANCHOR_PROPS);
 376         final String protocol = url.getProtocol();
 377         if (protocol.equals("jar")) {
 378             // Normal deployment: nashorn.jar
 379             final String jarUrlFile = url.getFile();
 380             final String filePath = jarUrlFile.substring(0, jarUrlFile.indexOf('!'));
 381             final URL file = new URL(filePath);
 382             try (final InputStream in = file.openStream()) {
 383                 final byte[] buf = new byte[128*1024];
 384                 final MessageDigest digest = MessageDigest.getInstance("SHA-1");
 385                 for(;;) {
 386                     final int l = in.read(buf);
 387                     if(l == -1) {
 388                         return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest());
 389                     }
 390                     digest.update(buf, 0, l);
 391                 }
 392             }
 393         } else if(protocol.equals("file")) {
 394             // Development
 395             final String fileStr = url.getFile();
 396             final String className = OptimisticTypesPersistence.class.getName();
 397             final int packageNameLen = className.lastIndexOf('.');
 398             final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1 - ANCHOR_PROPS.length());
 399             final File dir = new File(dirStr);
 400             return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(
 401                     dir, 0L)));
 402         } else if(protocol.equals("jrt")) {
 403             return getJrtVersionDirName();
 404         } else {
 405             throw new AssertionError("unknown protocol");
 406         }
 407     }
 408 
 409     private static long getLastModifiedClassFile(final File dir, final long max) {
 410         long currentMax = max;
 411         for(final File f: dir.listFiles()) {
 412             if(f.getName().endsWith(".class")) {
 413                 final long lastModified = f.lastModified();
 414                 if (lastModified > currentMax) {
 415                     currentMax = lastModified;
 416                 }
 417             } else if (f.isDirectory()) {
 418                 final long lastModified = getLastModifiedClassFile(f, currentMax);
 419                 if (lastModified > currentMax) {
 420                     currentMax = lastModified;
 421                 }
 422             }
 423         }
 424         return currentMax;
 425     }
 426 
 427     /**
 428      * Returns true if the specified file is a symbolic link, and also logs a warning if it is.
 429      * @param file the file
 430      * @return true if file is a symbolic link, false otherwise.
 431      */
 432     private static boolean isSymbolicLink(final File file) {
 433         if (Files.isSymbolicLink(file.toPath())) {
 434             getLogger().warning("Directory " + file + " is a symlink");
 435             return true;
 436         }
 437         return false;
 438     }
 439 
 440     private static Object[] createLockArray() {
 441         final Object[] lockArray = new Object[Runtime.getRuntime().availableProcessors() * 2];
 442         for (int i = 0; i < lockArray.length; ++i) {
 443             lockArray[i] = new Object();
 444         }
 445         return lockArray;
 446     }
 447 
 448     private static Object getFileLock(final File file) {
 449         return locks[(file.hashCode() & Integer.MAX_VALUE) % locks.length];
 450     }
 451 
 452     private static DebugLogger getLogger() {
 453         try {
 454             return Context.getContext().getLogger(RecompilableScriptFunctionData.class);
 455         } catch (final NullPointerException e) {
 456             //Don't print stacktrace until we revisit this, NPE is a known issue here
 457         } catch (final Exception e) {
 458             e.printStackTrace();
 459         }
 460         return DebugLogger.DISABLED_LOGGER;
 461     }
 462 
 463     private static void scheduleCleanup() {
 464         if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) {
 465             cleanupTimer.schedule(new TimerTask() {
 466                 @Override
 467                 public void run() {
 468                     scheduledCleanup.set(false);
 469                     try {
 470                         doCleanup();
 471                     } catch (final IOException e) {
 472                         // Ignore it. While this is unfortunate, we don't have good facility for reporting
 473                         // this, as we're running in a thread that has no access to Context, so we can't grab
 474                         // a DebugLogger.
 475                     }
 476                 }
 477             }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY));
 478         }
 479     }
 480 
 481     private static void doCleanup() throws IOException {
 482         final Path[] files = getAllRegularFilesInLastModifiedOrder();
 483         final int nFiles = files.length;
 484         final int filesToDelete = Math.max(0, nFiles - MAX_FILES);
 485         int filesDeleted = 0;
 486         for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) {
 487             try {
 488                 Files.deleteIfExists(files[i]);
 489                 // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something
 490                 // else deleted it for us; that's okay with us.
 491                 filesDeleted++;
 492             } catch (final Exception e) {
 493                 // does not increase filesDeleted
 494             }
 495             files[i] = null; // gc eligible
 496         }
 497     }
 498 
 499     private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException {
 500         try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) {
 501             // TODO: rewrite below once we can use JDK8 syntactic constructs
 502             return filesStream
 503             .filter(new Predicate<Path>() {
 504                 @Override
 505                 public boolean test(final Path path) {
 506                     return !Files.isDirectory(path);
 507                 }
 508             })
 509             .map(new Function<Path, PathAndTime>() {
 510                 @Override
 511                 public PathAndTime apply(final Path path) {
 512                     return new PathAndTime(path);
 513                 }
 514             })
 515             .sorted()
 516             .map(new Function<PathAndTime, Path>() {
 517                 @Override
 518                 public Path apply(final PathAndTime pathAndTime) {
 519                     return pathAndTime.path;
 520                 }
 521             })
 522             .toArray(new IntFunction<Path[]>() { // Replace with Path::new
 523                 @Override
 524                 public Path[] apply(final int length) {
 525                     return new Path[length];
 526                 }
 527             });
 528         }
 529     }
 530 
 531     private static class PathAndTime implements Comparable<PathAndTime> {
 532         private final Path path;
 533         private final long time;
 534 
 535         PathAndTime(final Path path) {
 536             this.path = path;
 537             this.time = getTime(path);
 538         }
 539 
 540         @Override
 541         public int compareTo(final PathAndTime other) {
 542             return Long.compare(time, other.time);
 543         }
 544 
 545         private static long getTime(final Path path) {
 546             try {
 547                 return Files.getLastModifiedTime(path).toMillis();
 548             } catch (final IOException e) {
 549                 // All files for which we can't retrieve the last modified date will be considered oldest.
 550                 return -1L;
 551             }
 552         }
 553     }
 554 
 555     private static int getMaxFiles() {
 556         final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null);
 557         if (str == null) {
 558             return DEFAULT_MAX_FILES;
 559         } else if ("unlimited".equals(str)) {
 560             return UNLIMITED_FILES;
 561         }
 562         return Math.max(0, Integer.parseInt(str));
 563     }
 564 
 565     private static final String JRT_NASHORN_DIR = "/modules/jdk.scripting.nashorn";
 566 
 567     // version directory name if nashorn is loaded from jrt:/ URL
 568     private static String getJrtVersionDirName() throws Exception {
 569         final FileSystem fs = getJrtFileSystem();
 570         // consider all .class resources under nashorn module to compute checksum
 571         final Path nashorn = fs.getPath(JRT_NASHORN_DIR);
 572         if (! Files.isDirectory(nashorn)) {
 573             throw new FileNotFoundException("missing " + JRT_NASHORN_DIR + " dir in jrt fs");
 574         }
 575         final MessageDigest digest = MessageDigest.getInstance("SHA-1");
 576         Files.walk(nashorn).forEach(new Consumer<Path>() {
 577             @Override
 578             public void accept(final Path p) {
 579                 // take only the .class resources.
 580                 if (Files.isRegularFile(p) && p.toString().endsWith(".class")) {
 581                     try {
 582                         digest.update(Files.readAllBytes(p));
 583                     } catch (final IOException ioe) {
 584                         throw new UncheckedIOException(ioe);
 585                     }
 586                 }
 587             }
 588         });
 589         return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest());
 590     }
 591 
 592     // get the default jrt FileSystem instance
 593     private static FileSystem getJrtFileSystem() {
 594         return AccessController.doPrivileged(
 595             new PrivilegedAction<FileSystem>() {
 596                 @Override
 597                 public FileSystem run() {
 598                     return FileSystems.getFileSystem(URI.create("jrt:/"));
 599                 }
 600             });
 601     }
 602 }