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 build.tools.module;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.File;
  30 import java.io.IOException;
  31 import java.io.InputStream;
  32 import java.io.OutputStream;
  33 import java.nio.file.Files;
  34 import java.nio.file.NoSuchFileException;
  35 import java.nio.file.Path;
  36 import java.nio.file.Paths;
  37 import java.nio.file.attribute.BasicFileAttributes;
  38 import java.util.Collections;
  39 import java.util.Comparator;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.Map;
  43 import java.util.Objects;
  44 import java.util.Set;
  45 import java.util.jar.JarEntry;
  46 import java.util.jar.JarFile;
  47 import javax.xml.namespace.QName;
  48 import javax.xml.stream.*;
  49 import javax.xml.stream.events.Attribute;
  50 import javax.xml.stream.events.XMLEvent;
  51 
  52 /**
  53  * GenJdepsModulesXml augments the input modules.xml file(s)
  54  * to include the module membership from the given path to
  55  * the JDK exploded image.  The output file is used by jdeps
  56  * to analyze dependencies and enforce module boundaries.
  57  *
  58  * The input modules.xml file defines the modular structure of
  59  * the JDK as described in JEP 200: The Modular JDK
  60  * (http://openjdk.java.net/jeps/200).
  61  *
  62  * $ java build.tools.module.GenJdepsModulesXml \
  63  *        -o com/sun/tools/jdeps/resources/modules.xml \
  64  *        -mp $OUTPUTDIR/modules \
  65  *        top/modules.xml
  66  */
  67 public final class GenJdepsModulesXml {
  68     private final static String USAGE =
  69         "Usage: GenJdepsModulesXml -o <output file> -mp build/modules path-to-modules-xml";
  70 
  71     public static void main(String[] args) throws Exception {
  72         Path outfile = null;
  73         Path modulepath = null;
  74         int i = 0;
  75         while (i < args.length) {
  76             String arg = args[i];
  77             if (arg.equals("-o")) {
  78                 outfile = Paths.get(args[i+1]);
  79                 i = i+2;
  80             } else if (arg.equals("-mp")) {
  81                 modulepath = Paths.get(args[i+1]);
  82                 i = i+2;
  83                 if (!Files.isDirectory(modulepath)) {
  84                     System.err.println(modulepath + " is not a directory");
  85                     System.exit(1);
  86                 }
  87             } else {
  88                 break;
  89             }
  90         }
  91         if (outfile == null || modulepath == null || i >= args.length) {
  92             System.err.println(USAGE);
  93             System.exit(-1);
  94         }
  95 
  96         GenJdepsModulesXml gentool = new GenJdepsModulesXml(modulepath);
  97         Set<Module> modules = new HashSet<>();
  98         for (; i < args.length; i++) {
  99             Path p = Paths.get(args[i]);
 100             try (InputStream in = new BufferedInputStream(Files.newInputStream(p))) {
 101                 Set<Module> mods = gentool.load(in);
 102                 modules.addAll(mods);
 103             }
 104         }
 105 
 106         Files.createDirectories(outfile.getParent());
 107         gentool.writeXML(modules, outfile);
 108     }
 109 
 110     final Path modulepath;
 111     public GenJdepsModulesXml(Path modulepath) {
 112         this.modulepath = modulepath;
 113     }
 114 
 115     private static final String MODULES   = "modules";
 116     private static final String MODULE    = "module";
 117     private static final String NAME      = "name";
 118     private static final String DEPEND    = "depend";
 119     private static final String EXPORT    = "export";
 120     private static final String TO        = "to";
 121     private static final String INCLUDE   = "include";
 122     private static final QName  REEXPORTS = new QName("re-exports");
 123     private Set<Module> load(InputStream in) throws XMLStreamException, IOException {
 124         Set<Module> modules = new HashSet<>();
 125         XMLInputFactory factory = XMLInputFactory.newInstance();
 126         XMLEventReader stream = factory.createXMLEventReader(in);
 127         Module.Builder mb = null;
 128         String modulename = null;
 129         String pkg = null;
 130         Set<String> permits = new HashSet<>();
 131         while (stream.hasNext()) {
 132             XMLEvent event = stream.nextEvent();
 133             if (event.isStartElement()) {
 134                 String startTag = event.asStartElement().getName().getLocalPart();
 135                 switch (startTag) {
 136                     case MODULES:
 137                         break;
 138                     case MODULE:
 139                         if (mb != null) {
 140                             throw new RuntimeException("end tag for module is missing");
 141                         }
 142                         modulename = getNextTag(stream, NAME);
 143                         mb = new Module.Builder();
 144                         mb.name(modulename);
 145                         break;
 146                     case NAME:
 147                         throw new RuntimeException(event.toString());
 148                     case DEPEND:
 149                         boolean reexports = false;
 150                         Attribute attr = event.asStartElement().getAttributeByName(REEXPORTS);
 151                         if (attr != null) {
 152                             String value = attr.getValue();
 153                             if (value.equals("true") || value.equals("false")) {
 154                                 reexports = Boolean.parseBoolean(value);
 155                             } else {
 156                                 throw new RuntimeException("unexpected attribute " + attr.toString());
 157                             }
 158                         }
 159                         mb.require(getData(stream), reexports);
 160                         break;
 161                     case INCLUDE:
 162                         throw new RuntimeException("unexpected " + event);
 163                     case EXPORT:
 164                         pkg = getNextTag(stream, NAME);
 165                         break;
 166                     case TO:
 167                         permits.add(getData(stream));
 168                         break;
 169                     default:
 170                 }
 171             } else if (event.isEndElement()) {
 172                 String endTag = event.asEndElement().getName().getLocalPart();
 173                 switch (endTag) {
 174                     case MODULE:
 175                         buildIncludes(mb, modulename);
 176                         modules.add(mb.build());
 177                         mb = null;
 178                         break;
 179                     case EXPORT:
 180                         if (pkg == null) {
 181                             throw new RuntimeException("export-to is malformed");
 182                         }
 183                         mb.exportTo(pkg, permits);
 184                         pkg = null;
 185                         permits.clear();
 186                         break;
 187                     default:
 188                 }
 189             } else if (event.isCharacters()) {
 190                 String s = event.asCharacters().getData();
 191                 if (!s.trim().isEmpty()) {
 192                     throw new RuntimeException("export-to is malformed");
 193                 }
 194             }
 195         }
 196         return modules;
 197     }
 198 
 199     private String getData(XMLEventReader reader) throws XMLStreamException {
 200         XMLEvent e = reader.nextEvent();
 201         if (e.isCharacters()) {
 202             return e.asCharacters().getData();
 203         }
 204         throw new RuntimeException(e.toString());
 205     }
 206 
 207     private String getNextTag(XMLEventReader reader, String tag) throws XMLStreamException {
 208         XMLEvent e = reader.nextTag();
 209         if (e.isStartElement()) {
 210             String t = e.asStartElement().getName().getLocalPart();
 211             if (!tag.equals(t)) {
 212                 throw new RuntimeException(e + " expected: " + tag);
 213             }
 214             return getData(reader);
 215         }
 216         throw new RuntimeException("export-to name is missing:" + e);
 217     }
 218     private void writeXML(Set<Module> modules, Path path)
 219             throws IOException, XMLStreamException
 220     {
 221         XMLOutputFactory xof = XMLOutputFactory.newInstance();
 222         try (OutputStream out = Files.newOutputStream(path)) {
 223             int depth = 0;
 224             XMLStreamWriter xtw = xof.createXMLStreamWriter(out, "UTF-8");
 225             xtw.writeStartDocument("utf-8","1.0");
 226             writeStartElement(xtw, MODULES, depth);
 227             modules.stream()
 228                    .sorted(Comparator.comparing(Module::name))
 229                    .forEach(m -> writeModuleElement(xtw, m, depth+1));
 230             writeEndElement(xtw, depth);
 231             xtw.writeCharacters("\n");
 232             xtw.writeEndDocument();
 233             xtw.flush();
 234             xtw.close();
 235         }
 236     }
 237 
 238     private void writeElement(XMLStreamWriter xtw, String element, String value, int depth) {
 239         try {
 240             writeStartElement(xtw, element, depth);
 241             xtw.writeCharacters(value);
 242             xtw.writeEndElement();
 243         } catch (XMLStreamException e) {
 244             throw new RuntimeException(e);
 245         }
 246     }
 247 
 248     private void writeDependElement(XMLStreamWriter xtw, Module.Dependence d, int depth) {
 249         try {
 250             writeStartElement(xtw, DEPEND, depth);
 251             if (d.reexport) {
 252                 xtw.writeAttribute("re-exports", "true");
 253             }
 254             xtw.writeCharacters(d.name);
 255             xtw.writeEndElement();
 256         } catch (XMLStreamException e) {
 257             throw new RuntimeException(e);
 258         }
 259     }
 260 
 261     private void writeExportElement(XMLStreamWriter xtw, String pkg, int depth) {
 262         writeExportElement(xtw, pkg, Collections.emptySet(), depth);
 263     }
 264 
 265     private void writeExportElement(XMLStreamWriter xtw, String pkg,
 266                                     Set<String> permits, int depth) {
 267         try {
 268             writeStartElement(xtw, EXPORT, depth);
 269             writeElement(xtw, NAME, pkg, depth+1);
 270             if (!permits.isEmpty()) {
 271                 permits.stream().sorted()
 272                        .forEach(m -> writeElement(xtw, TO, m, depth + 1));
 273             }
 274             writeEndElement(xtw, depth);
 275         } catch (XMLStreamException e) {
 276             throw new RuntimeException(e);
 277         }
 278     }
 279     private void writeModuleElement(XMLStreamWriter xtw, Module m, int depth) {
 280         try {
 281             writeStartElement(xtw, MODULE, depth);
 282             writeElement(xtw, NAME, m.name(), depth+1);
 283             m.requires().stream().sorted(Comparator.comparing(d -> d.name))
 284                         .forEach(d -> writeDependElement(xtw, d, depth+1));
 285             m.exports().keySet().stream()
 286                        .filter(pn -> m.exports().get(pn).isEmpty())
 287                        .sorted()
 288                        .forEach(pn -> writeExportElement(xtw, pn, depth+1));
 289             m.exports().entrySet().stream()
 290                        .filter(e -> !e.getValue().isEmpty())
 291                        .sorted(Map.Entry.comparingByKey())
 292                        .forEach(e -> writeExportElement(xtw, e.getKey(), e.getValue(), depth+1));
 293             m.packages().stream().sorted()
 294                         .forEach(p -> writeElement(xtw, INCLUDE, p, depth+1));
 295             writeEndElement(xtw, depth);
 296         } catch (XMLStreamException e) {
 297             throw new RuntimeException(e);
 298 
 299         }
 300     }
 301 
 302     /** Two spaces; the default indentation. */
 303     public static final String DEFAULT_INDENT = "  ";
 304 
 305     /** stack[depth] indicates what's been written into the current scope. */
 306     private static String[] stack = new String[] { "\n",
 307         "\n" + DEFAULT_INDENT,
 308         "\n" + DEFAULT_INDENT + DEFAULT_INDENT,
 309         "\n" + DEFAULT_INDENT + DEFAULT_INDENT + DEFAULT_INDENT};
 310 
 311     private void writeStartElement(XMLStreamWriter xtw, String name, int depth)
 312             throws XMLStreamException
 313     {
 314         xtw.writeCharacters(stack[depth]);
 315         xtw.writeStartElement(name);
 316     }
 317 
 318     private void writeEndElement(XMLStreamWriter xtw, int depth) throws XMLStreamException {
 319         xtw.writeCharacters(stack[depth]);
 320         xtw.writeEndElement();
 321     }
 322 
 323     private String packageName(Path p) {
 324         return packageName(p.toString().replace(File.separatorChar, '/'));
 325     }
 326     private String packageName(String name) {
 327         int i = name.lastIndexOf('/');
 328         return (i > 0) ? name.substring(0, i).replace('/', '.') : "";
 329     }
 330 
 331     private boolean includes(String name) {
 332         return name.endsWith(".class") && !name.equals("module-info.class");
 333     }
 334 
 335     public void buildIncludes(Module.Builder mb, String modulename) throws IOException {
 336         Path mclasses = modulepath.resolve(modulename);
 337         try {
 338             Files.find(mclasses, Integer.MAX_VALUE, (Path p, BasicFileAttributes attr)
 339                          -> includes(p.getFileName().toString()))
 340                  .map(p -> packageName(mclasses.relativize(p)))
 341                  .forEach(mb::include);
 342         } catch (NoSuchFileException e) {
 343             // aggregate module may not have class
 344         }
 345     }
 346 
 347     static class Module {
 348         static class Dependence {
 349             final String name;
 350             final boolean reexport;
 351             Dependence(String name) {
 352                 this(name, false);
 353             }
 354             Dependence(String name, boolean reexport) {
 355                 this.name = name;
 356                 this.reexport = reexport;
 357             }
 358 
 359             @Override
 360             public int hashCode() {
 361                 int hash = 5;
 362                 hash = 11 * hash + Objects.hashCode(this.name);
 363                 hash = 11 * hash + (this.reexport ? 1 : 0);
 364                 return hash;
 365             }
 366 
 367             public boolean equals(Object o) {
 368                 Dependence d = (Dependence)o;
 369                 return this.name.equals(d.name) && this.reexport == d.reexport;
 370             }
 371         }
 372         private final String moduleName;
 373         private final Set<Dependence> requires;
 374         private final Map<String, Set<String>> exports;
 375         private final Set<String> packages;
 376 
 377         private Module(String name,
 378                 Set<Dependence> requires,
 379                 Map<String, Set<String>> exports,
 380                 Set<String> packages) {
 381             this.moduleName = name;
 382             this.requires = Collections.unmodifiableSet(requires);
 383             this.exports = Collections.unmodifiableMap(exports);
 384             this.packages = Collections.unmodifiableSet(packages);
 385         }
 386 
 387         public String name() {
 388             return moduleName;
 389         }
 390 
 391         public Set<Dependence> requires() {
 392             return requires;
 393         }
 394 
 395         public Map<String, Set<String>> exports() {
 396             return exports;
 397         }
 398 
 399         public Set<String> packages() {
 400             return packages;
 401         }
 402 
 403         @Override
 404         public boolean equals(Object ob) {
 405             if (!(ob instanceof Module)) {
 406                 return false;
 407             }
 408             Module that = (Module) ob;
 409             return (moduleName.equals(that.moduleName)
 410                     && requires.equals(that.requires)
 411                     && exports.equals(that.exports)
 412                     && packages.equals(that.packages));
 413         }
 414 
 415         @Override
 416         public int hashCode() {
 417             int hc = moduleName.hashCode();
 418             hc = hc * 43 + requires.hashCode();
 419             hc = hc * 43 + exports.hashCode();
 420             hc = hc * 43 + packages.hashCode();
 421             return hc;
 422         }
 423 
 424         @Override
 425         public String toString() {
 426             StringBuilder sb = new StringBuilder();
 427             sb.append("module ").append(moduleName).append(" {").append("\n");
 428             requires.stream().sorted().forEach(d ->
 429                     sb.append(String.format("   requires %s%s%n", d.reexport ? "public " : "", d.name)));
 430             exports.entrySet().stream().filter(e -> e.getValue().isEmpty())
 431                     .sorted(Map.Entry.comparingByKey())
 432                     .forEach(e -> sb.append(String.format("   exports %s%n", e.getKey())));
 433             exports.entrySet().stream().filter(e -> !e.getValue().isEmpty())
 434                     .sorted(Map.Entry.comparingByKey())
 435                     .forEach(e -> sb.append(String.format("   exports %s to %s%n", e.getKey(), e.getValue())));
 436             packages.stream().sorted().forEach(pn -> sb.append(String.format("   includes %s%n", pn)));
 437             sb.append("}");
 438             return sb.toString();
 439         }
 440 
 441         static class Builder {
 442             private String name;
 443             private final Set<Dependence> requires = new HashSet<>();
 444             private final Map<String, Set<String>> exports = new HashMap<>();
 445             private final Set<String> packages = new HashSet<>();
 446 
 447             public Builder() {
 448             }
 449 
 450             public Builder name(String n) {
 451                 name = n;
 452                 return this;
 453             }
 454 
 455             public Builder require(String d, boolean reexport) {
 456                 requires.add(new Dependence(d, reexport));
 457                 return this;
 458             }
 459 
 460             public Builder include(String p) {
 461                 packages.add(p);
 462                 return this;
 463             }
 464 
 465             public Builder export(String p) {
 466                 return exportTo(p, Collections.emptySet());
 467             }
 468 
 469             public Builder exportTo(String p, Set<String> ms) {
 470                 Objects.requireNonNull(p);
 471                 Objects.requireNonNull(ms);
 472                 if (exports.containsKey(p)) {
 473                     throw new RuntimeException(name + " already exports " + p);
 474                 }
 475                 exports.put(p, new HashSet<>(ms));
 476                 return this;
 477             }
 478 
 479             public Module build() {
 480                 Module m = new Module(name, requires, exports, packages);
 481                 return m;
 482             }
 483         }
 484     }
 485 }