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