1 /* 2 * Copyright (c) 2012, 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 com.sun.tools.classfile.AccessFlags; 29 import com.sun.tools.classfile.ClassFile; 30 import com.sun.tools.classfile.ConstantPoolException; 31 import com.sun.tools.classfile.Dependencies.ClassFileError; 32 33 import jdk.internal.util.jar.VersionedStream; 34 35 import java.io.Closeable; 36 import java.io.File; 37 import java.io.FileNotFoundException; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.UncheckedIOException; 41 import java.nio.file.FileSystem; 42 import java.nio.file.FileSystems; 43 import java.nio.file.Files; 44 import java.nio.file.Path; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.Enumeration; 48 import java.util.Iterator; 49 import java.util.List; 50 import java.util.NoSuchElementException; 51 import java.util.Set; 52 import java.util.jar.JarEntry; 53 import java.util.jar.JarFile; 54 import java.util.stream.Collectors; 55 import java.util.stream.Stream; 56 import java.util.zip.ZipFile; 57 58 /** 59 * ClassFileReader reads ClassFile(s) of a given path that can be 60 * a .class file, a directory, or a JAR file. 61 */ 62 public class ClassFileReader implements Closeable { 63 /** 64 * Returns a ClassFileReader instance of a given path. 65 */ 66 public static ClassFileReader newInstance(Path path) throws IOException { 67 return newInstance(path, null); 68 } 69 70 /** 71 * Returns a ClassFileReader instance of a given path. 72 */ 73 public static ClassFileReader newInstance(Path path, Runtime.Version version) throws IOException { 74 if (Files.notExists(path)) { 75 throw new FileNotFoundException(path.toString()); 76 } 77 78 if (Files.isDirectory(path)) { 79 return new DirectoryReader(path); 80 } else if (path.getFileName().toString().endsWith(".jar")) { 81 return new JarFileReader(path, version); 82 } else { 83 return new ClassFileReader(path); 84 } 85 } 86 87 /** 88 * Returns a ClassFileReader instance of a given FileSystem and path. 89 * 90 * This method is used for reading classes from jrtfs. 91 */ 92 public static ClassFileReader newInstance(FileSystem fs, Path path) throws IOException { 93 return new DirectoryReader(fs, path); 94 } 95 96 protected final Path path; 97 protected final String baseFileName; 98 protected Set<String> entries; // binary names 99 100 protected final List<String> skippedEntries = new ArrayList<>(); 101 protected ClassFileReader(Path path) { 102 this.path = path; 103 this.baseFileName = path.getFileName() != null 104 ? path.getFileName().toString() 105 : path.toString(); 106 } 107 108 public String getFileName() { 109 return baseFileName; 110 } 111 112 public List<String> skippedEntries() { 113 return skippedEntries; 114 } 115 116 /** 117 * Returns all entries in this archive. 118 */ 119 public Set<String> entries() { 120 Set<String> es = this.entries; 121 if (es == null) { 122 // lazily scan the entries 123 this.entries = scan(); 124 } 125 return this.entries; 126 } 127 128 /** 129 * Returns the ClassFile matching the given binary name 130 * or a fully-qualified class name. 131 */ 132 public ClassFile getClassFile(String name) throws IOException { 133 if (name.indexOf('.') > 0) { 134 int i = name.lastIndexOf('.'); 135 String pathname = name.replace('.', File.separatorChar) + ".class"; 136 if (baseFileName.equals(pathname) || 137 baseFileName.equals(pathname.substring(0, i) + "$" + 138 pathname.substring(i+1, pathname.length()))) { 139 return readClassFile(path); 140 } 141 } else { 142 if (baseFileName.equals(name.replace('/', File.separatorChar) + ".class")) { 143 return readClassFile(path); 144 } 145 } 146 return null; 147 } 148 149 public Iterable<ClassFile> getClassFiles() throws IOException { 150 return FileIterator::new; 151 } 152 153 protected ClassFile readClassFile(Path p) throws IOException { 154 InputStream is = null; 155 try { 156 is = Files.newInputStream(p); 157 return ClassFile.read(is); 158 } catch (ConstantPoolException e) { 159 throw new ClassFileError(e); 160 } finally { 161 if (is != null) { 162 is.close(); 163 } 164 } 165 } 166 167 protected Set<String> scan() { 168 try { 169 ClassFile cf = ClassFile.read(path); 170 String name = cf.access_flags.is(AccessFlags.ACC_MODULE) 171 ? "module-info" : cf.getName(); 172 return Collections.singleton(name); 173 } catch (ConstantPoolException|IOException e) { 174 throw new ClassFileError(e); 175 } 176 } 177 178 static boolean isClass(Path file) { 179 String fn = file.getFileName().toString(); 180 return fn.endsWith(".class"); 181 } 182 183 @Override 184 public void close() throws IOException { 185 } 186 187 class FileIterator implements Iterator<ClassFile> { 188 int count; 189 FileIterator() { 190 this.count = 0; 191 } 192 public boolean hasNext() { 193 return count == 0 && baseFileName.endsWith(".class"); 194 } 195 196 public ClassFile next() { 197 if (!hasNext()) { 198 throw new NoSuchElementException(); 199 } 200 try { 201 ClassFile cf = readClassFile(path); 202 count++; 203 return cf; 204 } catch (IOException e) { 205 throw new ClassFileError(e); 206 } 207 } 208 209 public void remove() { 210 throw new UnsupportedOperationException("Not supported yet."); 211 } 212 } 213 214 public String toString() { 215 return path.toString(); 216 } 217 218 private static class DirectoryReader extends ClassFileReader { 219 protected final String fsSep; 220 DirectoryReader(Path path) throws IOException { 221 this(FileSystems.getDefault(), path); 222 } 223 DirectoryReader(FileSystem fs, Path path) throws IOException { 224 super(path); 225 this.fsSep = fs.getSeparator(); 226 } 227 228 protected Set<String> scan() { 229 try (Stream<Path> stream = Files.walk(path, Integer.MAX_VALUE)) { 230 return stream.filter(ClassFileReader::isClass) 231 .map(path::relativize) 232 .map(Path::toString) 233 .map(p -> p.replace(File.separatorChar, '/')) 234 .collect(Collectors.toSet()); 235 } catch (IOException e) { 236 throw new UncheckedIOException(e); 237 } 238 } 239 240 public ClassFile getClassFile(String name) throws IOException { 241 if (name.indexOf('.') > 0) { 242 int i = name.lastIndexOf('.'); 243 String pathname = name.replace(".", fsSep) + ".class"; 244 Path p = path.resolve(pathname); 245 if (Files.notExists(p)) { 246 p = path.resolve(pathname.substring(0, i) + "$" + 247 pathname.substring(i+1, pathname.length())); 248 } 249 if (Files.exists(p)) { 250 return readClassFile(p); 251 } 252 } else { 253 Path p = path.resolve(name + ".class"); 254 if (Files.exists(p)) { 255 return readClassFile(p); 256 } 257 } 258 return null; 259 } 260 261 public Iterable<ClassFile> getClassFiles() throws IOException { 262 final Iterator<ClassFile> iter = new DirectoryIterator(); 263 return () -> iter; 264 } 265 266 class DirectoryIterator implements Iterator<ClassFile> { 267 private final List<Path> entries; 268 private int index = 0; 269 DirectoryIterator() throws IOException { 270 List<Path> paths = null; 271 try (Stream<Path> stream = Files.walk(path, Integer.MAX_VALUE)) { 272 paths = stream.filter(ClassFileReader::isClass) 273 .collect(Collectors.toList()); 274 } 275 this.entries = paths; 276 this.index = 0; 277 } 278 279 public boolean hasNext() { 280 return index != entries.size(); 281 } 282 283 public ClassFile next() { 284 if (!hasNext()) { 285 throw new NoSuchElementException(); 286 } 287 Path path = entries.get(index++); 288 try { 289 return readClassFile(path); 290 } catch (IOException e) { 291 throw new ClassFileError(e); 292 } 293 } 294 295 public void remove() { 296 throw new UnsupportedOperationException("Not supported yet."); 297 } 298 } 299 } 300 301 static class JarFileReader extends ClassFileReader { 302 private final JarFile jarfile; 303 private final Runtime.Version version; 304 305 JarFileReader(Path path, Runtime.Version version) throws IOException { 306 this(path, openJarFile(path.toFile(), version), version); 307 } 308 309 JarFileReader(Path path, JarFile jf, Runtime.Version version) throws IOException { 310 super(path); 311 this.jarfile = jf; 312 this.version = version; 313 } 314 315 @Override 316 public void close() throws IOException { 317 jarfile.close(); 318 } 319 320 private static JarFile openJarFile(File f, Runtime.Version version) 321 throws IOException { 322 JarFile jf; 323 if (version == null) { 324 jf = new JarFile(f, false); 325 if (jf.isMultiRelease()) { 326 throw new MultiReleaseException("err.multirelease.option.notfound", f.getName()); 327 } 328 } else { 329 jf = new JarFile(f, false, ZipFile.OPEN_READ, version); 330 if (!jf.isMultiRelease()) { 331 throw new MultiReleaseException("err.multirelease.option.exists", f.getName()); 332 } 333 } 334 return jf; 335 } 336 337 protected Set<String> scan() { 338 try (JarFile jf = openJarFile(path.toFile(), version)) { 339 return VersionedStream.stream(jf).map(JarEntry::getName) 340 .filter(n -> n.endsWith(".class")) 341 .collect(Collectors.toSet()); 342 } catch (IOException e) { 343 throw new UncheckedIOException(e); 344 } 345 } 346 347 public ClassFile getClassFile(String name) throws IOException { 348 if (name.indexOf('.') > 0) { 349 int i = name.lastIndexOf('.'); 350 String entryName = name.replace('.', '/') + ".class"; 351 JarEntry e = jarfile.getJarEntry(entryName); 352 if (e == null) { 353 e = jarfile.getJarEntry(entryName.substring(0, i) + "$" 354 + entryName.substring(i + 1, entryName.length())); 355 } 356 if (e != null) { 357 return readClassFile(jarfile, e); 358 } 359 } else { 360 JarEntry e = jarfile.getJarEntry(name + ".class"); 361 if (e != null) { 362 return readClassFile(jarfile, e); 363 } 364 } 365 return null; 366 } 367 368 protected ClassFile readClassFile(JarFile jarfile, JarEntry e) throws IOException { 369 try (InputStream is = jarfile.getInputStream(e)) { 370 ClassFile cf = ClassFile.read(is); 371 if (jarfile.isMultiRelease()) { 372 VersionHelper.add(jarfile, e, cf); 373 } 374 return cf; 375 } catch (ConstantPoolException ex) { 376 throw new ClassFileError(ex); 377 } 378 } 379 380 public Iterable<ClassFile> getClassFiles() throws IOException { 381 final Iterator<ClassFile> iter = new JarFileIterator(this, jarfile); 382 return () -> iter; 383 } 384 } 385 386 Enumeration<JarEntry> versionedEntries(JarFile jf) { 387 Iterator<JarEntry> it = VersionedStream.stream(jf).iterator(); 388 return new Enumeration<>() { 389 @Override 390 public boolean hasMoreElements() { 391 return it.hasNext(); 392 } 393 394 @Override 395 public JarEntry nextElement() { 396 return it.next(); 397 } 398 }; 399 } 400 401 class JarFileIterator implements Iterator<ClassFile> { 402 protected final JarFileReader reader; 403 protected Enumeration<JarEntry> entries; 404 protected JarFile jf; 405 protected JarEntry nextEntry; 406 protected ClassFile cf; 407 JarFileIterator(JarFileReader reader) { 408 this(reader, null); 409 } 410 JarFileIterator(JarFileReader reader, JarFile jarfile) { 411 this.reader = reader; 412 setJarFile(jarfile); 413 } 414 415 void setJarFile(JarFile jarfile) { 416 if (jarfile == null) return; 417 418 this.jf = jarfile; 419 this.entries = versionedEntries(jf); 420 this.nextEntry = nextEntry(); 421 } 422 423 public boolean hasNext() { 424 if (nextEntry != null && cf != null) { 425 return true; 426 } 427 while (nextEntry != null) { 428 try { 429 cf = reader.readClassFile(jf, nextEntry); 430 return true; 431 } catch (ClassFileError | IOException ex) { 432 skippedEntries.add(String.format("%s: %s (%s)", 433 ex.getMessage(), 434 nextEntry.getName(), 435 jf.getName())); 436 } 437 nextEntry = nextEntry(); 438 } 439 return false; 440 } 441 442 public ClassFile next() { 443 if (!hasNext()) { 444 throw new NoSuchElementException(); 445 } 446 ClassFile classFile = cf; 447 cf = null; 448 nextEntry = nextEntry(); 449 return classFile; 450 } 451 452 protected JarEntry nextEntry() { 453 while (entries.hasMoreElements()) { 454 JarEntry e = entries.nextElement(); 455 String name = e.getName(); 456 if (name.endsWith(".class")) { 457 return e; 458 } 459 } 460 return null; 461 } 462 463 public void remove() { 464 throw new UnsupportedOperationException("Not supported yet."); 465 } 466 } 467 private static final String MODULE_INFO = "module-info.class"; 468 }