1 /* 2 * Copyright (c) 2015, 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.tools.jlink.internal.plugins.asm; 26 27 import java.io.ByteArrayInputStream; 28 import java.io.IOException; 29 import java.io.UncheckedIOException; 30 import java.lang.module.ModuleDescriptor; 31 import java.lang.module.ModuleDescriptor.Requires; 32 import java.lang.module.ModuleDescriptor.Requires.Modifier; 33 import java.lang.module.ModuleDescriptor.Exports; 34 import java.util.ArrayList; 35 import java.util.Collection; 36 import java.util.Collections; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.LinkedHashMap; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 import java.util.Objects; 44 import java.util.Set; 45 import jdk.internal.org.objectweb.asm.ClassReader; 46 import jdk.internal.org.objectweb.asm.ClassWriter; 47 import jdk.tools.jlink.internal.ImageFileCreator; 48 import jdk.tools.jlink.internal.ModulePoolImpl; 49 import jdk.tools.jlink.plugin.ModuleEntry; 50 import jdk.tools.jlink.plugin.PluginException; 51 import jdk.tools.jlink.plugin.ModulePool; 52 53 /** 54 * A pool of ClassReader and other resource files. This class allows to 55 * transform and sort classes and resource files. 56 * <p> 57 * Classes in the class pool are named following java binary name specification. 58 * For example, java.lang.Object class is named java/lang/Object 59 * <p> 60 * Module information has been stripped out from class and other resource files 61 * (.properties, binary files, ...).</p> 62 */ 63 final class AsmPoolImpl implements AsmModulePool { 64 65 /** 66 * Contains the transformed classes. When the jimage file is generated, 67 * transformed classes take precedence on unmodified ones. 68 */ 69 public final class WritableClassPoolImpl implements WritableClassPool { 70 71 private WritableClassPoolImpl() { 72 } 73 74 /** 75 * Add a class to the pool, if a class already exists, it is replaced. 76 * 77 * @param writer The class writer. 78 * @throws java.io.IOException 79 */ 80 @Override 81 public void addClass(ClassWriter writer) { 82 Objects.requireNonNull(writer); 83 // Retrieve the className 84 ClassReader reader = newClassReader(writer.toByteArray()); 85 String className = reader.getClassName(); 86 String path; 87 if (className.endsWith("module-info")) { 88 // remove the module name contained in the class name 89 className = className.substring(className.indexOf("/") + 1); 90 path = "/" + moduleName + "/" + className; 91 } else { 92 path = toClassNamePath(className); 93 } 94 95 byte[] content = writer.toByteArray(); 96 ModuleEntry res = ModuleEntry.create(path, content); 97 transformedClasses.put(className, res); 98 } 99 100 /** 101 * The class will be not added to the jimage file. 102 * 103 * @param className The class name to forget. 104 */ 105 @Override 106 public void forgetClass(String className) { 107 Objects.requireNonNull(className); 108 // do we have a resource? 109 ModuleEntry res = transformedClasses.get(className); 110 if (res == null) { 111 res = inputClasses.get(className); 112 if (res == null) { 113 throw new PluginException("Unknown class " + className); 114 } 115 } 116 String path = toClassNamePath(className); 117 forgetResources.add(path); 118 // Just in case it has been added. 119 transformedClasses.remove(className); 120 } 121 122 /** 123 * Get a transformed class. 124 * 125 * @param binaryName The java class binary name 126 * @return The ClassReader or null if the class is not found. 127 */ 128 @Override 129 public ClassReader getClassReader(String binaryName) { 130 Objects.requireNonNull(binaryName); 131 ModuleEntry res = transformedClasses.get(binaryName); 132 ClassReader reader = null; 133 if (res != null) { 134 reader = getClassReader(res); 135 } 136 return reader; 137 } 138 139 /** 140 * Returns all the classes contained in the writable pool. 141 * 142 * @return The array of transformed classes. 143 */ 144 @Override 145 public Collection<ModuleEntry> getClasses() { 146 List<ModuleEntry> classes = new ArrayList<>(); 147 for (Entry<String, ModuleEntry> entry : transformedClasses.entrySet()) { 148 classes.add(entry.getValue()); 149 } 150 return classes; 151 } 152 153 @Override 154 public ClassReader getClassReader(ModuleEntry res) { 155 return newClassReader(res.getBytes()); 156 } 157 } 158 159 /** 160 * Contains the transformed resources. When the jimage file is generated, 161 * transformed resources take precedence on unmodified ones. 162 */ 163 public final class WritableResourcePoolImpl implements WritableResourcePool { 164 165 private WritableResourcePoolImpl() { 166 } 167 168 /** 169 * Add a resource, if the resource exists, it is replaced. 170 * 171 * @param resFile The resource file to add. 172 */ 173 @Override 174 public void addResourceFile(ResourceFile resFile) { 175 Objects.requireNonNull(resFile); 176 String path = toResourceNamePath(resFile.getPath()); 177 ModuleEntry res = ModuleEntry.create(path, resFile.getContent()); 178 transformedResources.put(resFile.getPath(), res); 179 } 180 181 /** 182 * The resource will be not added to the jimage file. 183 * 184 * @param resourceName 185 * @throws java.io.IOException 186 */ 187 @Override 188 public void forgetResourceFile(String resourceName) { 189 Objects.requireNonNull(resourceName); 190 String path = toResourceNamePath(resourceName); 191 // do we have a resource? 192 ModuleEntry res = transformedResources.get(resourceName); 193 if (res == null) { 194 res = inputResources.get(resourceName); 195 if (res == null) { 196 throw new PluginException("Unknown resource " + resourceName); 197 } 198 } 199 forgetResources.add(path); 200 // Just in case it has been added. 201 transformedResources.remove(resourceName); 202 } 203 204 /** 205 * Get a transformed resource. 206 * 207 * @param name The java resource name 208 * @return The Resource or null if the resource is not found. 209 */ 210 @Override 211 public ResourceFile getResourceFile(String name) { 212 Objects.requireNonNull(name); 213 ModuleEntry res = transformedResources.get(name); 214 ResourceFile resFile = null; 215 if (res != null) { 216 resFile = getResourceFile(res); 217 } 218 return resFile; 219 } 220 221 /** 222 * Returns all the resources contained in the writable pool. 223 * 224 * @return The array of transformed classes. 225 */ 226 @Override 227 public Collection<ModuleEntry> getResourceFiles() { 228 List<ModuleEntry> resources = new ArrayList<>(); 229 for (Entry<String, ModuleEntry> entry : transformedResources.entrySet()) { 230 resources.add(entry.getValue()); 231 } 232 return resources; 233 } 234 235 @Override 236 public ResourceFile getResourceFile(ModuleEntry res) { 237 return new ResourceFile(toJavaBinaryResourceName(res.getPath()), 238 res.getBytes()); 239 } 240 } 241 242 private final ModulePool jimageResources; 243 private final Map<String, ModuleEntry> inputClasses; 244 private final Map<String, ModuleEntry> inputResources; 245 private final Map<String, String> inputClassPackageMapping; 246 private final Map<String, String> inputOtherPackageMapping; 247 248 private final WritableClassPool transClassesPool 249 = new WritableClassPoolImpl(); 250 private final WritableResourcePool transResourcesPool 251 = new WritableResourcePoolImpl(); 252 253 private Sorter sorter; 254 255 private final Map<String, ModuleEntry> transformedClasses 256 = new LinkedHashMap<>(); 257 private final Map<String, ModuleEntry> transformedResources 258 = new LinkedHashMap<>(); 259 private final List<String> forgetResources = new ArrayList<>(); 260 private final Map<String, String> newPackageMapping = new HashMap<>(); 261 262 private final String moduleName; 263 264 private final ModuleDescriptor descriptor; 265 private final AsmPools pools; 266 267 /** 268 * A new Asm pool. 269 * 270 * @param inputResources The raw resources to build the pool from. 271 * @param moduleName The name of a module. 272 * @param pools The resource pools. 273 * @param descriptor The module descriptor. 274 */ 275 AsmPoolImpl(ModulePool inputResources, String moduleName, 276 AsmPools pools, 277 ModuleDescriptor descriptor) { 278 Objects.requireNonNull(inputResources); 279 Objects.requireNonNull(moduleName); 280 Objects.requireNonNull(pools); 281 Objects.requireNonNull(descriptor); 282 this.jimageResources = inputResources; 283 this.moduleName = moduleName; 284 this.pools = pools; 285 this.descriptor = descriptor; 286 Map<String, ModuleEntry> classes = new LinkedHashMap<>(); 287 Map<String, ModuleEntry> resources = new LinkedHashMap<>(); 288 Map<String, String> packageClassToModule = new HashMap<>(); 289 Map<String, String> packageOtherToModule = new HashMap<>(); 290 inputResources.entries().forEach(res -> { 291 if (res.getPath().endsWith(".class")) { 292 classes.put(toJavaBinaryClassName(res.getPath()), res); 293 } else { 294 resources.put(toJavaBinaryResourceName(res.getPath()), res); 295 } 296 String[] split = ImageFileCreator.splitPath(res.getPath()); 297 if (ImageFileCreator.isClassPackage(res.getPath())) { 298 packageClassToModule.put(split[1], res.getModule()); 299 } else { 300 // Keep a map of other resources 301 // Same resource names such as META-INF/* should be handled with full path name. 302 if (!split[1].isEmpty()) { 303 packageOtherToModule.put(split[1], res.getModule()); 304 } 305 } 306 }); 307 this.inputClasses = Collections.unmodifiableMap(classes); 308 this.inputResources = Collections.unmodifiableMap(resources); 309 310 this.inputClassPackageMapping = Collections.unmodifiableMap(packageClassToModule); 311 this.inputOtherPackageMapping = Collections.unmodifiableMap(packageOtherToModule); 312 } 313 314 @Override 315 public String getModuleName() { 316 return moduleName; 317 } 318 319 /** 320 * The writable pool used to store transformed resources. 321 * 322 * @return The writable pool. 323 */ 324 @Override 325 public WritableClassPool getTransformedClasses() { 326 return transClassesPool; 327 } 328 329 /** 330 * The writable pool used to store transformed resource files. 331 * 332 * @return The writable pool. 333 */ 334 @Override 335 public WritableResourcePool getTransformedResourceFiles() { 336 return transResourcesPool; 337 } 338 339 /** 340 * Set a sorter instance to sort all files. If no sorter is set, then input 341 * Resources will be added in the order they have been received followed by 342 * newly added resources. 343 * 344 * @param sorter 345 */ 346 @Override 347 public void setSorter(Sorter sorter) { 348 this.sorter = sorter; 349 } 350 351 /** 352 * Returns the classes contained in the pool. 353 * 354 * @return The array of classes. 355 */ 356 @Override 357 public Collection<ModuleEntry> getClasses() { 358 return inputClasses.values(); 359 } 360 361 /** 362 * Returns the resources contained in the pool. Resources are all the file 363 * that are not classes (eg: properties file, binary files, ...) 364 * 365 * @return The array of classes. 366 */ 367 @Override 368 public Collection<ModuleEntry> getResourceFiles() { 369 return inputResources.values(); 370 } 371 372 /** 373 * Retrieves a resource based on the binary name. This name doesn't contain 374 * the module name. 375 * <b>NB:</b> When dealing with resources that have the same name in various 376 * modules (eg: META-INFO/*), you should use the <code>ResourcePool</code> 377 * referenced from this <code>AsmClassPool</code>. 378 * 379 * @param binaryName Name of a Java resource or null if the resource doesn't 380 * exist. 381 * @return 382 */ 383 @Override 384 public ResourceFile getResourceFile(String binaryName) { 385 Objects.requireNonNull(binaryName); 386 ModuleEntry res = inputResources.get(binaryName); 387 ResourceFile resFile = null; 388 if (res != null) { 389 resFile = getResourceFile(res); 390 } 391 return resFile; 392 } 393 394 /** 395 * Retrieve a ClassReader from the pool. 396 * 397 * @param binaryName Class binary name 398 * @return A reader or null if the class is unknown 399 */ 400 @Override 401 public ClassReader getClassReader(String binaryName) { 402 Objects.requireNonNull(binaryName); 403 ModuleEntry res = inputClasses.get(binaryName); 404 ClassReader reader = null; 405 if (res != null) { 406 reader = getClassReader(res); 407 } 408 return reader; 409 } 410 411 @Override 412 public ResourceFile getResourceFile(ModuleEntry res) { 413 return new ResourceFile(toJavaBinaryResourceName(res.getPath()), 414 res.getBytes()); 415 } 416 417 @Override 418 public ClassReader getClassReader(ModuleEntry res) { 419 return newClassReader(res.getBytes()); 420 } 421 422 /** 423 * Lookup the class in this pool and the required pools. NB: static module 424 * readability can be different at execution time. 425 * 426 * @param binaryName The class to lookup. 427 * @return The reader or null if not found 428 */ 429 @Override 430 public ClassReader getClassReaderInDependencies(String binaryName) { 431 Objects.requireNonNull(binaryName); 432 ClassReader reader = getClassReader(binaryName); 433 if (reader == null) { 434 for (Requires requires : descriptor.requires()) { 435 AsmModulePool pool = pools.getModulePool(requires.name()); 436 reader = pool.getExportedClassReader(moduleName, binaryName); 437 if (reader != null) { 438 break; 439 } 440 } 441 } 442 return reader; 443 } 444 445 /** 446 * Lookup the class in the exported packages of this module. "public 447 * requires" modules are looked up. NB: static module readability can be 448 * different at execution time. 449 * 450 * @param callerModule Name of calling module. 451 * @param binaryName The class to lookup. 452 * @return The reader or null if not found 453 */ 454 @Override 455 public ClassReader getExportedClassReader(String callerModule, String binaryName) { 456 Objects.requireNonNull(callerModule); 457 Objects.requireNonNull(binaryName); 458 boolean exported = false; 459 ClassReader clazz = null; 460 for (Exports e : descriptor.exports()) { 461 String pkg = e.source(); 462 Set<String> targets = e.targets(); 463 System.out.println("PKG " + pkg); 464 if (targets.isEmpty() || targets.contains(callerModule)) { 465 if (binaryName.startsWith(pkg)) { 466 String className = binaryName.substring(pkg.length()); 467 System.out.println("CLASS " + className); 468 exported = !className.contains("."); 469 } 470 if (exported) { 471 break; 472 } 473 } 474 } 475 // public requires (re-export) 476 if (!exported) { 477 for (Requires requires : descriptor.requires()) { 478 if (requires.modifiers().contains(Modifier.PUBLIC)) { 479 AsmModulePool pool = pools.getModulePool(requires.name()); 480 clazz = pool.getExportedClassReader(moduleName, binaryName); 481 if (clazz != null) { 482 break; 483 } 484 } 485 } 486 } else { 487 clazz = getClassReader(binaryName); 488 } 489 return clazz; 490 491 } 492 493 @Override 494 public ModuleDescriptor getDescriptor() { 495 return descriptor; 496 } 497 498 /** 499 * To visit the set of ClassReaders. 500 * 501 * @param visitor The visitor. 502 */ 503 @Override 504 public void visitClassReaders(ClassReaderVisitor visitor) { 505 Objects.requireNonNull(visitor); 506 for (ModuleEntry res : getClasses()) { 507 ClassReader reader = newClassReader(res.getBytes()); 508 ClassWriter writer = visitor.visit(reader); 509 if (writer != null) { 510 511 getTransformedClasses().addClass(writer); 512 } 513 } 514 } 515 516 /** 517 * To visit the set of ClassReaders. 518 * 519 * @param visitor The visitor. 520 */ 521 @Override 522 public void visitResourceFiles(ResourceFileVisitor visitor) { 523 Objects.requireNonNull(visitor); 524 for (ModuleEntry resource : getResourceFiles()) { 525 ResourceFile resFile 526 = new ResourceFile(toJavaBinaryResourceName(resource.getPath()), 527 resource.getBytes()); 528 ResourceFile res = visitor.visit(resFile); 529 if (res != null) { 530 getTransformedResourceFiles().addResourceFile(res); 531 } 532 } 533 } 534 535 /** 536 * Returns the pool of all the resources (transformed and unmodified). The 537 * input resources are replaced by the transformed ones. If a sorter has 538 * been set, it is used to sort the returned resources. * 539 */ 540 @Override 541 public void fillOutputResources(ModulePool outputResources) { 542 List<String> added = new ArrayList<>(); 543 // If the sorter is null, use the input order. 544 // New resources are added at the end 545 // First input classes that have not been removed 546 ModulePool output = new ModulePoolImpl(outputResources.getByteOrder(), 547 ((ModulePoolImpl)outputResources).getStringTable()); 548 jimageResources.entries().forEach(inResource -> { 549 if (!forgetResources.contains(inResource.getPath())) { 550 ModuleEntry resource = inResource; 551 // Do we have a transformed class with the same name? 552 ModuleEntry res = transformedResources. 553 get(toJavaBinaryResourceName(inResource.getPath())); 554 if (res != null) { 555 resource = res; 556 } else { 557 res = transformedClasses. 558 get(toJavaBinaryClassName(inResource.getPath())); 559 if (res != null) { 560 resource = res; 561 } 562 } 563 output.add(resource); 564 added.add(resource.getPath()); 565 } 566 }); 567 // Then new resources 568 for (Map.Entry<String, ModuleEntry> entry : transformedResources.entrySet()) { 569 ModuleEntry resource = entry.getValue(); 570 if (!forgetResources.contains(resource.getPath())) { 571 if (!added.contains(resource.getPath())) { 572 output.add(resource); 573 } 574 } 575 } 576 // And new classes 577 for (Map.Entry<String, ModuleEntry> entry : transformedClasses.entrySet()) { 578 ModuleEntry resource = entry.getValue(); 579 if (!forgetResources.contains(resource.getPath())) { 580 if (!added.contains(resource.getPath())) { 581 output.add(resource); 582 } 583 } 584 } 585 586 AsmPools.sort(outputResources, output, sorter); 587 } 588 589 /** 590 * Associate a package to this module, useful when adding new classes in new 591 * packages. WARNING: In order to properly handle new package and/or new 592 * module, module-info class must be added and/or updated. 593 * 594 * @param pkg The new package, following java binary syntax (/-separated 595 * path name). 596 * @throws PluginException If a mapping already exist for this package. 597 */ 598 @Override 599 public void addPackage(String pkg) { 600 Objects.requireNonNull(pkg); 601 Objects.requireNonNull(moduleName); 602 pkg = pkg.replaceAll("/", "."); 603 String mod = newPackageMapping.get(pkg); 604 if (mod != null) { 605 throw new PluginException(mod + " module already contains package " + pkg); 606 } 607 newPackageMapping.put(pkg, moduleName); 608 } 609 610 @Override 611 public Set<String> getAllPackages() { 612 ModuleDescriptor desc = getDescriptor(); 613 Set<String> packages = new HashSet<>(); 614 for (String p : desc.conceals()) { 615 packages.add(p.replaceAll("\\.", "/")); 616 } 617 for (String p : newPackageMapping.keySet()) { 618 packages.add(p.replaceAll("\\.", "/")); 619 } 620 for (Exports ex : desc.exports()) { 621 packages.add(ex.source().replaceAll("\\.", "/")); 622 } 623 return packages; 624 } 625 626 private static ClassReader newClassReader(byte[] bytes) { 627 try { 628 ByteArrayInputStream stream = new ByteArrayInputStream(bytes); 629 ClassReader reader = new ClassReader(stream); 630 return reader; 631 } catch (IOException ex) { 632 throw new UncheckedIOException(ex); 633 } 634 } 635 636 private static String toJavaBinaryClassName(String path) { 637 if (path.endsWith("module-info.class")) { 638 path = removeClassExtension(path); 639 } else { 640 path = removeModuleName(path); 641 path = removeClassExtension(path); 642 } 643 return path; 644 } 645 646 private static String toJavaBinaryResourceName(String path) { 647 if (!path.endsWith("module-info.class")) { 648 path = removeModuleName(path); 649 } 650 return path; 651 } 652 653 private static String removeClassExtension(String path) { 654 return path.substring(0, path.length() - ".class".length()); 655 } 656 657 private static String removeModuleName(String path) { 658 path = path.substring(1); 659 return path.substring(path.indexOf("/") + 1, path.length()); 660 } 661 662 private String toClassNamePath(String className) { 663 return toResourceNamePath(className) + ".class"; 664 } 665 666 /** 667 * Entry point to manage resource<->module association. 668 */ 669 private String toResourceNamePath(String resourceName) { 670 if (!resourceName.startsWith("/")) { 671 resourceName = "/" + resourceName; 672 } 673 String pkg = toPackage(resourceName); 674 String module = inputClassPackageMapping.get(pkg); 675 if (module == null) { 676 module = newPackageMapping.get(pkg); 677 if (module == null) { 678 module = inputOtherPackageMapping.get(pkg); 679 if (module == null) { 680 throw new PluginException("No module for package" + pkg); 681 } 682 } 683 } 684 return "/" + module + resourceName; 685 } 686 687 private static String toPackage(String path) { 688 if (path.startsWith("/")) { 689 path = path.substring(1); 690 } 691 int i = path.lastIndexOf("/"); 692 if (i == -1) { 693 // Default package... 694 return ""; 695 } 696 return path.substring(0, i).replaceAll("/", "."); 697 } 698 }