1 /*
   2  * Copyright (c) 2014, 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 
  26 package com.sun.tools.jdeps;
  27 
  28 import java.io.BufferedReader;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.InputStreamReader;
  32 import java.io.UncheckedIOException;
  33 import java.lang.module.ModuleDescriptor;
  34 import java.net.URI;
  35 import java.util.Arrays;
  36 import java.util.Collections;
  37 import java.util.HashMap;
  38 import java.util.HashSet;
  39 import java.util.List;
  40 import java.util.Map;
  41 import java.util.Objects;
  42 import java.util.Optional;
  43 import java.util.Set;
  44 import java.util.jar.JarEntry;
  45 import java.util.jar.JarFile;
  46 import java.util.stream.Collectors;
  47 
  48 /**
  49  * JDeps internal representation of module for dependency analysis.
  50  */
  51 class Module extends Archive {
  52     static final boolean traceOn = Boolean.getBoolean("jdeps.debug");
  53     static void trace(String fmt, Object... args) {
  54         if (traceOn) {
  55             System.err.format(fmt, args);
  56         }
  57     }
  58 
  59     /*
  60      * Returns true if the given package name is JDK critical internal API
  61      * in jdk.unsupported module
  62      */
  63     static boolean isJDKUnsupported(Module m, String pn) {
  64         return JDK_UNSUPPORTED.equals(m.name()) || unsupported.contains(pn);
  65     };
  66 
  67     protected final ModuleDescriptor descriptor;
  68     protected final Map<String, Boolean> requires;
  69     protected final Map<String, Set<String>> exports;
  70     protected final Set<String> packages;
  71     protected final boolean isJDK;
  72     protected final URI location;
  73 
  74     private Module(String name,
  75                    URI location,
  76                    ModuleDescriptor descriptor,
  77                    Map<String, Boolean> requires,
  78                    Map<String, Set<String>> exports,
  79                    Set<String> packages,
  80                    boolean isJDK,
  81                    ClassFileReader reader) {
  82         super(name, location, reader);
  83         this.descriptor = descriptor;
  84         this.location = location;
  85         this.requires = Collections.unmodifiableMap(requires);
  86         this.exports = Collections.unmodifiableMap(exports);
  87         this.packages = Collections.unmodifiableSet(packages);
  88         this.isJDK = isJDK;
  89     }
  90 
  91     /**
  92      * Returns module name
  93      */
  94     public String name() {
  95         return descriptor.name();
  96     }
  97 
  98     public boolean isNamed() {
  99         return true;
 100     }
 101 
 102     public boolean isAutomatic() {
 103         return descriptor.isAutomatic();
 104     }
 105 
 106     public Module getModule() {
 107         return this;
 108     }
 109 
 110     public ModuleDescriptor descriptor() {
 111         return descriptor;
 112     }
 113 
 114     public boolean isJDK() {
 115         return isJDK;
 116     }
 117 
 118     public Map<String, Boolean> requires() {
 119         return requires;
 120     }
 121 
 122     public Map<String, Set<String>> exports() {
 123         return exports;
 124     }
 125 
 126     public Map<String, Set<String>> provides() {
 127         return descriptor.provides().entrySet().stream()
 128                 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().providers()));
 129     }
 130 
 131     public Set<String> packages() {
 132         return packages;
 133     }
 134 
 135     /**
 136      * Tests if the package of the given name is exported.
 137      */
 138     public boolean isExported(String pn) {
 139         return exports.containsKey(pn) ? exports.get(pn).isEmpty() : false;
 140     }
 141 
 142     /**
 143      * Converts this module to a strict module with the given dependences
 144      *
 145      * @throws IllegalArgumentException if this module is not an automatic module
 146      */
 147     public Module toStrictModule(Map<String, Boolean> requires) {
 148         if (!isAutomatic()) {
 149             throw new IllegalArgumentException(name() + " already a strict module");
 150         }
 151         return new StrictModule(this, requires);
 152     }
 153 
 154     /**
 155      * Tests if the package of the given name is qualifiedly exported
 156      * to the target.
 157      */
 158     public boolean isExported(String pn, String target) {
 159         return isExported(pn) || exports.containsKey(pn) && exports.get(pn).contains(target);
 160     }
 161 
 162     private final static String JDK_UNSUPPORTED = "jdk.unsupported";
 163 
 164     // temporary until jdk.unsupported module
 165     private final static List<String> unsupported = Arrays.asList("sun.misc", "sun.reflect");
 166 
 167     @Override
 168     public String toString() {
 169         return name();
 170     }
 171 
 172     public final static class Builder {
 173         final String name;
 174         final Map<String, Boolean> requires = new HashMap<>();
 175         final Map<String, Set<String>> exports = new HashMap<>();
 176         final Set<String> packages = new HashSet<>();
 177         final boolean isJDK;
 178         ClassFileReader reader;
 179         ModuleDescriptor descriptor;
 180         URI location;
 181 
 182         public Builder(String name) {
 183             this(name, false);
 184         }
 185 
 186         public Builder(String name, boolean isJDK) {
 187             this.name = name;
 188             this.isJDK = isJDK;
 189         }
 190 
 191         public Builder location(URI location) {
 192             this.location = location;
 193             return this;
 194         }
 195 
 196         public Builder descriptor(ModuleDescriptor md) {
 197             this.descriptor = md;
 198             return this;
 199         }
 200 
 201         public Builder require(String d, boolean reexport) {
 202             requires.put(d, reexport);
 203             return this;
 204         }
 205 
 206         public Builder packages(Set<String> pkgs) {
 207             packages.addAll(pkgs);
 208             return this;
 209         }
 210 
 211         public Builder export(String p, Set<String> ms) {
 212             Objects.requireNonNull(p);
 213             Objects.requireNonNull(ms);
 214             exports.put(p, new HashSet<>(ms));
 215             return this;
 216         }
 217         public Builder classes(ClassFileReader reader) {
 218             this.reader = reader;
 219             return this;
 220         }
 221 
 222         public Module build() {
 223             if (descriptor.isAutomatic() && isJDK) {
 224                 throw new InternalError("JDK module: " + name + " can't be automatic module");
 225             }
 226 
 227             return new Module(name, location, descriptor, requires, exports, packages, isJDK, reader);
 228         }
 229     }
 230 
 231     final static Module UNNAMED_MODULE = new UnnamedModule();
 232     private static class UnnamedModule extends Module {
 233         private UnnamedModule() {
 234             super("unnamed", null, null,
 235                   Collections.emptyMap(),
 236                   Collections.emptyMap(),
 237                   Collections.emptySet(),
 238                   false, null);
 239         }
 240 
 241         @Override
 242         public String name() {
 243             return "unnamed";
 244         }
 245 
 246         @Override
 247         public boolean isNamed() {
 248             return false;
 249         }
 250 
 251         @Override
 252         public boolean isAutomatic() {
 253             return false;
 254         }
 255 
 256         @Override
 257         public boolean isExported(String pn) {
 258             return true;
 259         }
 260     }
 261 
 262     private static class StrictModule extends Module {
 263         private static final String SERVICES_PREFIX = "META-INF/services/";
 264         private final Map<String, Set<String>> provides;
 265         private final Module module;
 266         private final JarFile jarfile;
 267 
 268         /**
 269          * Converts the given automatic module to a strict module.
 270          *
 271          * Replace this module's dependences with the given requires and also
 272          * declare service providers, if specified in META-INF/services configuration file
 273          */
 274         private StrictModule(Module m, Map<String, Boolean> requires) {
 275             super(m.name(), m.location, m.descriptor, requires, m.exports, m.packages, m.isJDK, m.reader());
 276             this.module = m;
 277             try {
 278                 this.jarfile = new JarFile(m.path().toFile(), false);
 279             } catch (IOException e) {
 280                 throw new UncheckedIOException(e);
 281             }
 282             this.provides = providers(jarfile);
 283         }
 284 
 285         @Override
 286         public Map<String, Set<String>> provides() {
 287             return provides;
 288         }
 289 
 290         private Map<String, Set<String>> providers(JarFile jf) {
 291             Map<String, Set<String>> provides = new HashMap<>();
 292             // map names of service configuration files to service names
 293             Set<String> serviceNames =  jf.stream()
 294                     .map(e -> e.getName())
 295                     .filter(e -> e.startsWith(SERVICES_PREFIX))
 296                     .distinct()
 297                     .map(this::toServiceName)
 298                     .flatMap(Optional::stream)
 299                     .collect(Collectors.toSet());
 300 
 301             // parse each service configuration file
 302             for (String sn : serviceNames) {
 303                 JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
 304                 Set<String> providerClasses = new HashSet<>();
 305                 try (InputStream in = jf.getInputStream(entry)) {
 306                     BufferedReader reader
 307                             = new BufferedReader(new InputStreamReader(in, "UTF-8"));
 308                     String cn;
 309                     while ((cn = nextLine(reader)) != null) {
 310                         if (isJavaIdentifier(cn)) {
 311                             providerClasses.add(cn);
 312                         }
 313                     }
 314                 } catch (IOException e) {
 315                     throw new UncheckedIOException(e);
 316                 }
 317                 if (!providerClasses.isEmpty())
 318                     provides.put(sn, providerClasses);
 319             }
 320 
 321             return provides;
 322         }
 323 
 324         /**
 325          * Returns a container with the service type corresponding to the name of
 326          * a services configuration file.
 327          *
 328          * For example, if called with "META-INF/services/p.S" then this method
 329          * returns a container with the value "p.S".
 330          */
 331         private Optional<String> toServiceName(String cf) {
 332             assert cf.startsWith(SERVICES_PREFIX);
 333             int index = cf.lastIndexOf("/") + 1;
 334             if (index < cf.length()) {
 335                 String prefix = cf.substring(0, index);
 336                 if (prefix.equals(SERVICES_PREFIX)) {
 337                     String sn = cf.substring(index);
 338                     if (isJavaIdentifier(sn))
 339                         return Optional.of(sn);
 340                 }
 341             }
 342             return Optional.empty();
 343         }
 344 
 345         /**
 346          * Reads the next line from the given reader and trims it of comments and
 347          * leading/trailing white space.
 348          *
 349          * Returns null if the reader is at EOF.
 350          */
 351         private String nextLine(BufferedReader reader) throws IOException {
 352             String ln = reader.readLine();
 353             if (ln != null) {
 354                 int ci = ln.indexOf('#');
 355                 if (ci >= 0)
 356                     ln = ln.substring(0, ci);
 357                 ln = ln.trim();
 358             }
 359             return ln;
 360         }
 361 
 362         /**
 363          * Returns {@code true} if the given identifier is a legal Java identifier.
 364          */
 365         private static boolean isJavaIdentifier(String id) {
 366             int n = id.length();
 367             if (n == 0)
 368                 return false;
 369             if (!Character.isJavaIdentifierStart(id.codePointAt(0)))
 370                 return false;
 371             int cp = id.codePointAt(0);
 372             int i = Character.charCount(cp);
 373             for (; i < n; i += Character.charCount(cp)) {
 374                 cp = id.codePointAt(i);
 375                 if (!Character.isJavaIdentifierPart(cp) && id.charAt(i) != '.')
 376                     return false;
 377             }
 378             if (cp == '.')
 379                 return false;
 380 
 381             return true;
 382         }
 383     }
 384 }