1 /* 2 * Copyright (c) 2015, 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.internal.module; 26 27 import java.io.Closeable; 28 import java.io.File; 29 import java.io.IOError; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.UncheckedIOException; 33 import java.lang.module.ModuleDescriptor; 34 import java.lang.module.ModuleReader; 35 import java.lang.module.ModuleReference; 36 import java.net.MalformedURLException; 37 import java.net.URI; 38 import java.net.URL; 39 import java.nio.ByteBuffer; 40 import java.nio.file.Files; 41 import java.nio.file.Path; 42 import java.nio.file.Paths; 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.HashMap; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Optional; 50 import java.util.Set; 51 import java.util.jar.JarEntry; 52 import java.util.jar.JarFile; 53 import java.util.stream.Collectors; 54 import java.util.stream.Stream; 55 56 import jdk.internal.loader.Resource; 57 import jdk.internal.misc.JavaLangModuleAccess; 58 import jdk.internal.misc.SharedSecrets; 59 import sun.net.www.ParseUtil; 60 61 62 /** 63 * Provides support for patching modules, mostly the boot layer. 64 */ 65 66 public final class ModulePatcher { 67 68 private static final JavaLangModuleAccess JLMA 69 = SharedSecrets.getJavaLangModuleAccess(); 70 71 // module name -> sequence of patches (directories or JAR files) 72 private final Map<String, List<Path>> map; 73 74 /** 75 * Initialize the module patcher with the given map. The map key is 76 * the module name, the value is a list of path strings. 77 */ 78 public ModulePatcher(Map<String, List<String>> input) { 79 if (input.isEmpty()) { 80 this.map = Collections.emptyMap(); 81 } else { 82 Map<String, List<Path>> map = new HashMap<>(); 83 for (Map.Entry<String, List<String>> e : input.entrySet()) { 84 String mn = e.getKey(); 85 List<Path> paths = e.getValue().stream() 86 .map(Paths::get) 87 .collect(Collectors.toList()); 88 map.put(mn, paths); 89 } 90 this.map = map; 91 } 92 } 93 94 /** 95 * Returns a module reference that interposes on the given module if 96 * needed. If there are no patches for the given module then the module 97 * reference is simply returned. Otherwise the patches for the module 98 * are scanned (to find any new packages) and a new module reference is 99 * returned. 100 * 101 * @throws UncheckedIOException if an I/O error is detected 102 */ 103 public ModuleReference patchIfNeeded(ModuleReference mref) { 104 // if there are no patches for the module then nothing to do 105 ModuleDescriptor descriptor = mref.descriptor(); 106 String mn = descriptor.name(); 107 List<Path> paths = map.get(mn); 108 if (paths == null) 109 return mref; 110 111 // scan the JAR file or directory tree to get the set of packages 112 Set<String> packages = new HashSet<>(); 113 try { 114 for (Path file : paths) { 115 if (Files.isRegularFile(file)) { 116 117 // JAR file - do not open as a multi-release JAR as this 118 // is not supported by the boot class loader 119 try (JarFile jf = new JarFile(file.toFile())) { 120 jf.stream() 121 .map(e -> toPackageName(file, e)) 122 .filter(Checks::isJavaIdentifier) 123 .forEach(packages::add); 124 } 125 126 } else if (Files.isDirectory(file)) { 127 128 // exploded directory without following sym links 129 Path top = file; 130 Files.find(top, Integer.MAX_VALUE, 131 ((path, attrs) -> attrs.isRegularFile())) 132 .map(path -> toPackageName(top, path)) 133 .filter(Checks::isJavaIdentifier) 134 .forEach(packages::add); 135 136 } 137 } 138 139 } catch (IOException ioe) { 140 throw new UncheckedIOException(ioe); 141 } 142 143 // if there are new packages then we need a new ModuleDescriptor 144 Set<String> original = descriptor.packages(); 145 packages.addAll(original); 146 if (packages.size() > original.size()) { 147 descriptor = JLMA.newModuleDescriptor(descriptor, packages); 148 } 149 150 // return a module reference to the patched module 151 URI location = mref.location().orElse(null); 152 153 ModuleHashes recordedHashes = null; 154 ModuleResolution mres = null; 155 if (mref instanceof ModuleReferenceImpl) { 156 ModuleReferenceImpl impl = (ModuleReferenceImpl)mref; 157 recordedHashes = impl.recordedHashes(); 158 mres = impl.moduleResolution(); 159 } 160 161 return new ModuleReferenceImpl(descriptor, 162 location, 163 () -> new PatchedModuleReader(paths, mref), 164 this, 165 recordedHashes, 166 null, 167 mres); 168 169 } 170 171 /** 172 * Returns true is this module patcher has no patches. 173 */ 174 public boolean isEmpty() { 175 return map.isEmpty(); 176 } 177 178 /* 179 * Returns the names of the patched modules. 180 */ 181 Set<String> patchedModules() { 182 return map.keySet(); 183 } 184 185 /** 186 * A ModuleReader that reads resources from a patched module. 187 * 188 * This class is public so as to expose the findResource method to the 189 * built-in class loaders and avoid locating the resource twice during 190 * class loading (once to locate the resource, the second to gets the 191 * URL for the CodeSource). 192 */ 193 public static class PatchedModuleReader implements ModuleReader { 194 private final List<ResourceFinder> finders; 195 private final ModuleReference mref; 196 private final URL delegateCodeSourceURL; 197 private volatile ModuleReader delegate; 198 199 /** 200 * Creates the ModuleReader to reads resources a patched module. 201 */ 202 PatchedModuleReader(List<Path> patches, ModuleReference mref) { 203 List<ResourceFinder> finders = new ArrayList<>(); 204 boolean initialized = false; 205 try { 206 for (Path file : patches) { 207 if (Files.isRegularFile(file)) { 208 finders.add(new JarResourceFinder(file)); 209 } else { 210 finders.add(new ExplodedResourceFinder(file)); 211 } 212 } 213 initialized = true; 214 } catch (IOException ioe) { 215 throw new UncheckedIOException(ioe); 216 } finally { 217 // close all ResourceFinder in the event of an error 218 if (!initialized) closeAll(finders); 219 } 220 221 this.finders = finders; 222 this.mref = mref; 223 this.delegateCodeSourceURL = codeSourceURL(mref); 224 } 225 226 /** 227 * Closes all resource finders. 228 */ 229 private static void closeAll(List<ResourceFinder> finders) { 230 for (ResourceFinder finder : finders) { 231 try { finder.close(); } catch (IOException ioe) { } 232 } 233 } 234 235 /** 236 * Returns the code source URL for the given module. 237 */ 238 private static URL codeSourceURL(ModuleReference mref) { 239 try { 240 Optional<URI> ouri = mref.location(); 241 if (ouri.isPresent()) 242 return ouri.get().toURL(); 243 } catch (MalformedURLException e) { } 244 return null; 245 } 246 247 /** 248 * Returns the ModuleReader to delegate to when the resource is not 249 * found in a patch location. 250 */ 251 private ModuleReader delegate() throws IOException { 252 ModuleReader r = delegate; 253 if (r == null) { 254 synchronized (this) { 255 r = delegate; 256 if (r == null) { 257 delegate = r = mref.open(); 258 } 259 } 260 } 261 return r; 262 } 263 264 /** 265 * Finds a resources in the patch locations. Returns null if not found. 266 */ 267 private Resource findResourceInPatch(String name) throws IOException { 268 for (ResourceFinder finder : finders) { 269 Resource r = finder.find(name); 270 if (r != null) 271 return r; 272 } 273 return null; 274 } 275 276 /** 277 * Finds a resource of the given name in the patched module. 278 */ 279 public Resource findResource(String name) throws IOException { 280 281 // patch locations 282 Resource r = findResourceInPatch(name); 283 if (r != null) 284 return r; 285 286 // original module 287 ByteBuffer bb = delegate().read(name).orElse(null); 288 if (bb == null) 289 return null; 290 291 return new Resource() { 292 private <T> T shouldNotGetHere(Class<T> type) { 293 throw new InternalError("should not get here"); 294 } 295 @Override 296 public String getName() { 297 return shouldNotGetHere(String.class); 298 } 299 @Override 300 public URL getURL() { 301 return shouldNotGetHere(URL.class); 302 } 303 @Override 304 public URL getCodeSourceURL() { 305 return delegateCodeSourceURL; 306 } 307 @Override 308 public ByteBuffer getByteBuffer() throws IOException { 309 return bb; 310 } 311 @Override 312 public InputStream getInputStream() throws IOException { 313 return shouldNotGetHere(InputStream.class); 314 } 315 @Override 316 public int getContentLength() throws IOException { 317 return shouldNotGetHere(int.class); 318 } 319 }; 320 } 321 322 @Override 323 public Optional<URI> find(String name) throws IOException { 324 Resource r = findResourceInPatch(name); 325 if (r != null) { 326 URI uri = URI.create(r.getURL().toString()); 327 return Optional.of(uri); 328 } else { 329 return delegate().find(name); 330 } 331 } 332 333 @Override 334 public Optional<InputStream> open(String name) throws IOException { 335 Resource r = findResourceInPatch(name); 336 if (r != null) { 337 return Optional.of(r.getInputStream()); 338 } else { 339 return delegate().open(name); 340 } 341 } 342 343 @Override 344 public Optional<ByteBuffer> read(String name) throws IOException { 345 Resource r = findResourceInPatch(name); 346 if (r != null) { 347 ByteBuffer bb = r.getByteBuffer(); 348 assert !bb.isDirect(); 349 return Optional.of(bb); 350 } else { 351 return delegate().read(name); 352 } 353 } 354 355 @Override 356 public void release(ByteBuffer bb) { 357 if (bb.isDirect()) { 358 try { 359 delegate().release(bb); 360 } catch (IOException ioe) { 361 throw new InternalError(ioe); 362 } 363 } 364 } 365 366 @Override 367 public Stream<String> list() throws IOException { 368 Stream<String> s = delegate().list(); 369 for (ResourceFinder finder : finders) { 370 s = Stream.concat(s, finder.list()); 371 } 372 return s.distinct(); 373 } 374 375 @Override 376 public void close() throws IOException { 377 closeAll(finders); 378 delegate().close(); 379 } 380 } 381 382 383 /** 384 * A resource finder that find resources in a patch location. 385 */ 386 private static interface ResourceFinder extends Closeable { 387 Resource find(String name) throws IOException; 388 Stream<String> list() throws IOException; 389 } 390 391 392 /** 393 * A ResourceFinder that finds resources in a JAR file. 394 */ 395 private static class JarResourceFinder implements ResourceFinder { 396 private final JarFile jf; 397 private final URL csURL; 398 399 JarResourceFinder(Path path) throws IOException { 400 this.jf = new JarFile(path.toFile()); 401 this.csURL = path.toUri().toURL(); 402 } 403 404 @Override 405 public void close() throws IOException { 406 jf.close(); 407 } 408 409 @Override 410 public Resource find(String name) throws IOException { 411 JarEntry entry = jf.getJarEntry(name); 412 if (entry == null) 413 return null; 414 415 return new Resource() { 416 @Override 417 public String getName() { 418 return name; 419 } 420 @Override 421 public URL getURL() { 422 String encodedPath = ParseUtil.encodePath(name, false); 423 try { 424 return new URL("jar:" + csURL + "!/" + encodedPath); 425 } catch (MalformedURLException e) { 426 return null; 427 } 428 } 429 @Override 430 public URL getCodeSourceURL() { 431 return csURL; 432 } 433 @Override 434 public ByteBuffer getByteBuffer() throws IOException { 435 byte[] bytes = getInputStream().readAllBytes(); 436 return ByteBuffer.wrap(bytes); 437 } 438 @Override 439 public InputStream getInputStream() throws IOException { 440 return jf.getInputStream(entry); 441 } 442 @Override 443 public int getContentLength() throws IOException { 444 long size = entry.getSize(); 445 return (size > Integer.MAX_VALUE) ? -1 : (int) size; 446 } 447 }; 448 } 449 450 @Override 451 public Stream<String> list() throws IOException { 452 return jf.stream() 453 .filter(e -> !e.isDirectory()) 454 .map(JarEntry::getName); 455 } 456 } 457 458 459 /** 460 * A ResourceFinder that finds resources on the file system. 461 */ 462 private static class ExplodedResourceFinder implements ResourceFinder { 463 private final Path dir; 464 465 ExplodedResourceFinder(Path dir) { 466 this.dir = dir; 467 } 468 469 @Override 470 public void close() { } 471 472 @Override 473 public Resource find(String name) throws IOException { 474 Path file = Paths.get(name.replace('/', File.separatorChar)); 475 if (file.getRoot() == null) { 476 file = dir.resolve(file); 477 } else { 478 // drop the root component so that the resource is 479 // located relative to the module directory 480 int n = file.getNameCount(); 481 if (n == 0) 482 return null; 483 file = dir.resolve(file.subpath(0, n)); 484 } 485 486 if (Files.isRegularFile(file)) { 487 return newResource(name, dir, file); 488 } else { 489 return null; 490 } 491 } 492 493 private Resource newResource(String name, Path top, Path file) { 494 return new Resource() { 495 @Override 496 public String getName() { 497 return name; 498 } 499 @Override 500 public URL getURL() { 501 try { 502 return file.toUri().toURL(); 503 } catch (IOException | IOError e) { 504 return null; 505 } 506 } 507 @Override 508 public URL getCodeSourceURL() { 509 try { 510 return top.toUri().toURL(); 511 } catch (IOException | IOError e) { 512 return null; 513 } 514 } 515 @Override 516 public ByteBuffer getByteBuffer() throws IOException { 517 return ByteBuffer.wrap(Files.readAllBytes(file)); 518 } 519 @Override 520 public InputStream getInputStream() throws IOException { 521 return Files.newInputStream(file); 522 } 523 @Override 524 public int getContentLength() throws IOException { 525 long size = Files.size(file); 526 return (size > Integer.MAX_VALUE) ? -1 : (int)size; 527 } 528 }; 529 } 530 531 @Override 532 public Stream<String> list() throws IOException { 533 return Files.find(dir, Integer.MAX_VALUE, 534 (path, attrs) -> attrs.isRegularFile()) 535 .map(f -> dir.relativize(f) 536 .toString() 537 .replace(File.separatorChar, '/')); 538 } 539 } 540 541 542 /** 543 * Derives a package name from a file path to a .class file. 544 */ 545 private static String toPackageName(Path top, Path file) { 546 Path entry = top.relativize(file); 547 Path parent = entry.getParent(); 548 if (parent == null) { 549 return warnIfModuleInfo(top, entry.toString()); 550 } else { 551 return parent.toString().replace(File.separatorChar, '.'); 552 } 553 } 554 555 /** 556 * Derives a package name from the name of an entry in a JAR file. 557 */ 558 private static String toPackageName(Path file, JarEntry entry) { 559 String name = entry.getName(); 560 int index = name.lastIndexOf("/"); 561 if (index == -1) { 562 return warnIfModuleInfo(file, name); 563 } else { 564 return name.substring(0, index).replace('/', '.'); 565 } 566 } 567 568 private static String warnIfModuleInfo(Path file, String e) { 569 if (e.equals("module-info.class")) 570 System.err.println("WARNING: " + e + " ignored in patch: " + file); 571 return ""; 572 } 573 }