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 }