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 }